# Files changed · PR \#9729 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/9729/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(threads\): persist manual active thread order](https://git.cafe/pingdotgg/t3code/pull/9729.md)

Version: 1

Head commit: d8f4d8673ec546b002ee3776bc91676cf5d895f8

Base commit: 29d03ec556e94d5f847644730daf75ba4aa20678

Comparison base: 29d03ec556e94d5f847644730daf75ba4aa20678

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/environment/ServerEnvironment.test.ts

Status: modified; +1; −0

```diff
@@ -167,6 +167,7 @@
       expect(second.capabilities.fileAttachments).toEqual({ maxUploadBytes: 50 * 1024 * 1024 });
       expect(second.capabilities.pullRequests).toBe(true);
       expect(second.capabilities.usagePriceOverrides).toBe(true);
+      expect(second.capabilities.threadActiveReorder).toBe(true);
       expect(second.capabilities.threadTitleRegeneration).toBe(true);
       expect(second.capabilities.threadPullRequestLinking).toBe(true);
       expect(second.capabilities.agentActivityPublishing).toBe(false);
```

## apps/server/src/environment/ServerEnvironment.ts

Status: modified; +1; −0

```diff
@@ -226,6 +226,7 @@
       usagePriceOverrides: true,
       threadPinning: true,
       threadPinReorder: true,
+      threadActiveReorder: true,
       threadTitleRegeneration: true,
       threadPullRequestLinking: true,
       environmentIcon: true,
```

## apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts

Status: modified; +55; −3

```diff
@@ -488,6 +488,48 @@
       yield* sql`DROP TRIGGER count_thread_shell_updates`;
       yield* sql`DROP TABLE thread_shell_updates`;
 
+      // Replayed order events must survive later lifecycle upserts, whose
+      // complete SQL row writes otherwise risk dropping the placement.
+      const orderUpdatedAt = "2026-01-01T00:00:00.200Z";
+      const orderEvents = [
+        { type: "thread.meta-updated", payload: { activeOrderKey: "gm" } },
+        { type: "thread.pinned", payload: { pinnedAt: now, pinOrderKey: "m" } },
+        {
+          type: "thread.snoozed",
+          payload: { snoozedAt: now, snoozedUntil: "2026-01-02T00:00:00.000Z" },
+        },
+        { type: "thread.unsnoozed", payload: { reason: "user" } },
+        { type: "thread.unpinned", payload: {} },
+        { type: "thread.meta-updated", payload: { title: "Renamed" } },
+      ] as const;
+      for (const [index, event] of orderEvents.entries()) {
+        yield* eventStore.append({
+          type: event.type,
+          eventId: EventId.make(`evt-active-order-${index}`),
+          aggregateKind: "thread",
+          aggregateId: ThreadId.make("thread-1"),
+          occurredAt: "2026-01-01T00:00:00.500Z",
+          commandId: CommandId.make(`cmd-active-order-${index}`),
+          causationEventId: null,
+          correlationId: null,
+          metadata: {},
+          payload: {
+            ...event.payload,
+            threadId: ThreadId.make("thread-1"),
+            updatedAt: orderUpdatedAt,
+          },
+        });
+        yield* projectionPipeline.bootstrap;
+        const rows = yield* sql<{
+          readonly activeOrderKey: string | null;
+          readonly updatedAt: string;
+        }>`
+          SELECT active_order_key AS "activeOrderKey", updated_at AS "updatedAt"
+          FROM projection_threads WHERE thread_id = 'thread-1'
+        `;
+        assert.deepEqual(rows, [{ activeOrderKey: "gm", updatedAt: orderUpdatedAt }]);
+      }
+
       // Settled lifecycle through the DB pipeline: thread.settled writes the
       // override + timestamp, thread.unsettled(user) flips to the active pin.
       yield* eventStore.append({
@@ -512,16 +554,23 @@
         readonly settledOverride: string | null;
         readonly settledAt: string | null;
         readonly unsettledAt: string | null;
+        readonly activeOrderKey: string | null;
       }>`
         SELECT
           settled_override AS "settledOverride",
           settled_at AS "settledAt",
+          unsettled_at AS "unsettledAt",
-          unsettled_at AS "unsettledAt"
+          active_order_key AS "activeOrderKey"
         FROM projection_threads
         WHERE thread_id = 'thread-1'
       `;
       assert.deepEqual(settledRows, [
+        {
+          settledOverride: "settled",
+          settledAt: "2026-01-01T00:00:01.000Z",
+          unsettledAt: null,
-        { settledOverride: "settled", settledAt: "2026-01-01T00:00:01.000Z", unsettledAt: null },
+          activeOrderKey: null,
+        },
       ]);
 
       yield* eventStore.append({
@@ -546,11 +595,13 @@
         readonly settledOverride: string | null;
         readonly settledAt: string | null;
         readonly unsettledAt: string | null;
+        readonly activeOrderKey: string | null;
       }>`
         SELECT
           settled_override AS "settledOverride",
           settled_at AS "settledAt",
+          unsettled_at AS "unsettledAt",
-          unsettled_at AS "unsettledAt"
+          active_order_key AS "activeOrderKey"
         FROM projection_threads
         WHERE thread_id = 'thread-1'
       `;
@@ -561,6 +612,7 @@
           settledOverride: "active",
           settledAt: null,
           unsettledAt: "2026-01-01T00:00:02.000Z",
+          activeOrderKey: null,
         },
       ]);
     }),
```

## apps/server/src/orchestration/Layers/ProjectionPipeline.ts

Status: modified; +5; −0

```diff
@@ -617,6 +617,7 @@
             snoozedAt: null,
             pinnedAt: null,
             pinOrderKey: null,
+            activeOrderKey: null,
             titleRegenerationRequestId: null,
             titleRegenerationStartedAt: null,
             latestUserMessageAt: null,
@@ -671,6 +672,7 @@
             settledOverride: "settled",
             settledAt: event.payload.settledAt,
             unsettledAt: null,
+            activeOrderKey: null,
             updatedAt: event.payload.updatedAt,
           });
           return;
@@ -790,6 +792,9 @@
           yield* projectionThreadRepository.upsert({
             ...existingRow.value,
             ...(event.payload.title !== undefined ? { title: event.payload.title } : {}),
+            ...(event.payload.activeOrderKey !== undefined
+              ? { activeOrderKey: event.payload.activeOrderKey }
+              : {}),
             ...(event.payload.titleRegeneration !== undefined
               ? {
                   titleRegenerationRequestId: event.payload.titleRegeneration?.requestId ?? null,
```

## apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.test.ts

Status: modified; +6; −0

```diff
@@ -110,6 +110,7 @@
           has_actionable_proposed_plan,
           pinned_at,
           pin_order_key,
+          active_order_key,
           created_at,
           updated_at,
           deleted_at
@@ -132,6 +133,7 @@
           0,
           '2026-02-24T00:00:01.000Z',
           'gm',
+          'hq',
           '2026-02-24T00:00:02.000Z',
           '2026-02-24T00:00:03.000Z',
           NULL
@@ -358,6 +360,7 @@
           snoozedAt: null,
           pinnedAt: "2026-02-24T00:00:01.000Z",
           pinOrderKey: "gm",
+          activeOrderKey: "hq",
           titleRegeneration: null,
           deletedAt: null,
           messages: [
@@ -487,6 +490,7 @@
           snoozedAt: null,
           pinnedAt: "2026-02-24T00:00:01.000Z",
           pinOrderKey: "gm",
+          activeOrderKey: "hq",
           titleRegeneration: null,
           session: {
             threadId: ThreadId.make("thread-1"),
@@ -513,6 +517,7 @@
       }
 
       const commandSnapshot = yield* snapshotQuery.getCommandReadModel();
+      assert.equal(commandSnapshot.threads[0]?.activeOrderKey, "hq");
       assert.deepEqual(commandSnapshot.threads[0]?.branchPullRequest, branchPullRequest);
       const threadShell = yield* snapshotQuery.getThreadShellById(ThreadId.make("thread-1"));
       assert.equal(threadShell._tag, "Some");
@@ -560,6 +565,7 @@
       );
       assert.equal(detailWithoutActivities._tag, "Some");
       if (detailWithoutActivities._tag === "Some") {
+        assert.equal(detailWithoutActivities.value.activeOrderKey, "hq");
         assert.deepEqual(detailWithoutActivities.value.activities, []);
         assert.deepEqual(detailWithoutActivities.value.messages, snapshot.threads[0]?.messages);
         assert.deepEqual(
```

## apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts

Status: modified; +10; −0

```diff
@@ -513,6 +513,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
@@ -552,6 +553,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
@@ -593,6 +595,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
@@ -1083,6 +1086,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
@@ -2082,6 +2086,7 @@
                 snoozedAt: row.snoozedAt,
                 pinnedAt: row.pinnedAt,
                 pinOrderKey: row.pinOrderKey ?? null,
+                activeOrderKey: row.activeOrderKey ?? null,
                 titleRegeneration: mapTitleRegeneration(row),
                 deletedAt: row.deletedAt,
                 messages: messagesByThread.get(row.threadId) ?? [],
@@ -2296,6 +2301,7 @@
                   snoozedAt: row.snoozedAt,
                   pinnedAt: row.pinnedAt,
                   pinOrderKey: row.pinOrderKey ?? null,
+                  activeOrderKey: row.activeOrderKey ?? null,
                   titleRegeneration: mapTitleRegeneration(row),
                   deletedAt: row.deletedAt,
                   messages: [],
@@ -2437,6 +2443,7 @@
                       snoozedAt: row.snoozedAt,
                       pinnedAt: row.pinnedAt,
                       pinOrderKey: row.pinOrderKey ?? null,
+                      activeOrderKey: row.activeOrderKey ?? null,
                       titleRegeneration: mapTitleRegeneration(row),
                       session: sessionByThread.get(row.threadId) ?? null,
                       latestUserMessageAt: row.latestUserMessageAt,
@@ -2586,6 +2593,7 @@
                 snoozedAt: row.snoozedAt,
                 pinnedAt: row.pinnedAt,
                 pinOrderKey: row.pinOrderKey ?? null,
+                activeOrderKey: row.activeOrderKey ?? null,
                 titleRegeneration: mapTitleRegeneration(row),
                 session: sessionByThread.get(row.threadId) ?? null,
                 latestUserMessageAt: row.latestUserMessageAt,
@@ -2908,6 +2916,7 @@
         snoozedAt: threadRow.value.snoozedAt,
         pinnedAt: threadRow.value.pinnedAt,
         pinOrderKey: threadRow.value.pinOrderKey ?? null,
+        activeOrderKey: threadRow.value.activeOrderKey ?? null,
         titleRegeneration: mapTitleRegeneration(threadRow.value),
         session: Option.isSome(sessionRow) ? mapSessionRow(sessionRow.value) : null,
         latestUserMessageAt: threadRow.value.latestUserMessageAt,
@@ -3190,6 +3199,7 @@
         snoozedAt: threadRow.value.snoozedAt,
         pinnedAt: threadRow.value.pinnedAt,
         pinOrderKey: threadRow.value.pinOrderKey ?? null,
+        activeOrderKey: threadRow.value.activeOrderKey ?? null,
         titleRegeneration: mapTitleRegeneration(threadRow.value),
         deletedAt: null,
         messages: messageRows.map((row) => {
```

## apps/server/src/orchestration/decider.active-order.test.ts

Status: added; +200; −0

```diff
@@ -1,0 +1,200 @@
+import {
+  CommandId,
+  ProjectId,
+  ProviderInstanceId,
+  ThreadId,
+  type OrchestrationCommand,
+  type OrchestrationReadModel,
+  type OrchestrationThread,
+} from "@t3tools/contracts";
+import * as NodeServices from "@effect/platform-node/NodeServices";
+import { expect, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+
+import { decideOrchestrationCommand } from "./decider.ts";
+import { projectEvent } from "./projector.ts";
+
+const NOW = "2026-01-01T00:00:00.000Z";
+// The Effect test clock starts at the epoch.
+const BEFORE_NOW = "1969-12-30T00:00:00.000Z";
+const SNOOZED_AT = "1969-12-31T00:00:00.000Z";
+const FUTURE_WAKE = "1970-01-02T00:00:00.000Z";
+const THREAD_ID = ThreadId.make("thread-1");
+
+function makeReadModel(overrides: Partial<OrchestrationThread> = {}): OrchestrationReadModel {
+  return {
+    snapshotSequence: 0,
+    projects: [],
+    threads: [
+      {
+        id: THREAD_ID,
+        projectId: ProjectId.make("project-1"),
+        title: "Thread",
+        modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
+        runtimeMode: "full-access",
+        interactionMode: "default",
+        branch: null,
+        worktreePath: null,
+        latestTurn: null,
+        createdAt: NOW,
+        updatedAt: NOW,
+        archivedAt: null,
+        settledOverride: null,
+        settledAt: null,
+        unsettledAt: null,
+        activeOrderKey: null,
+        snoozedUntil: null,
+        snoozedAt: null,
+        pinnedAt: null,
+        pinOrderKey: null,
+        deletedAt: null,
+        messages: [],
+        proposedPlans: [],
+        activities: [],
+        checkpoints: [],
+        session: null,
+        ...overrides,
+      },
+    ],
+    updatedAt: NOW,
+  };
+}
+
+const reorderCommand = {
+  type: "thread.active.reorder",
+  commandId: CommandId.make("cmd-active-reorder"),
+  threadId: THREAD_ID,
+  orderKey: "m",
+} as const;
+
+it.layer(NodeServices.layer)("active thread ordering", (it) => {
+  it.effect("persists changed and repeated slots without changing thread activity timestamps", () =>
+    Effect.gen(function* () {
+      let readModel = makeReadModel({ unsettledAt: BEFORE_NOW });
+      for (const orderKey of ["m", "m", "g"]) {
+        const decided = yield* decideOrchestrationCommand({
+          command: { ...reorderCommand, orderKey },
+          readModel,
+        });
+        const events = Array.isArray(decided) ? decided : [decided];
+        expect(events).toHaveLength(1);
+        expect(events[0]).toMatchObject({
+          type: "thread.meta-updated",
+          payload: { threadId: THREAD_ID, activeOrderKey: orderKey, updatedAt: NOW },
+        });
+        for (const event of events) {
+          readModel = yield* projectEvent(readModel, {
+            ...event,
+            sequence: readModel.snapshotSequence + 1,
+          });
+        }
+        expect(readModel.threads[0]).toMatchObject({
+          activeOrderKey: orderKey,
+          updatedAt: NOW,
+          createdAt: NOW,
+          unsettledAt: BEFORE_NOW,
+        });
+      }
+    }),
+  );
+
+  for (const [label, overrides] of [
+    ["archived", { archivedAt: NOW }],
+    ["deleted", { deletedAt: NOW }],
+    ["pinned", { pinnedAt: NOW }],
+    ["settled", { settledOverride: "settled", settledAt: NOW }],
+  ] satisfies ReadonlyArray<readonly [string, Partial<OrchestrationThread>]>) {
+    it.effect(`rejects reordering a ${label} thread`, () =>
+      Effect.gen(function* () {
+        const error = yield* decideOrchestrationCommand({
+          command: reorderCommand,
+          readModel: makeReadModel(overrides),
+        }).pipe(Effect.flip);
+        expect(error._tag).toBe("OrchestrationCommandInvariantError");
+      }),
+    );
+  }
+
+  it.effect("reorders a running thread without affecting its session", () =>
+    Effect.gen(function* () {
+      const readModel = makeReadModel({
+        session: {
+          threadId: THREAD_ID,
+          status: "running",
+          providerName: "codex",
+          runtimeMode: "full-access",
+          activeTurnId: null,
+          lastError: null,
+          updatedAt: NOW,
+        },
+      });
+      const decided = yield* decideOrchestrationCommand({ command: reorderCommand, readModel });
+      const events = Array.isArray(decided) ? decided : [decided];
+      expect(events).toHaveLength(1);
+      for (const event of events) {
+        const projected = yield* projectEvent(readModel, { ...event, sequence: 1 });
+        expect(projected.threads[0]).toEqual({ ...readModel.threads[0], activeOrderKey: "m" });
+      }
+    }),
+  );
+
+  it.effect(
+    "changes a snoozed thread's retained slot without waking it or changing timestamps",
+    () =>
+      Effect.gen(function* () {
+        const readModel = makeReadModel({
+          activeOrderKey: "g",
+          snoozedAt: SNOOZED_AT,
+          snoozedUntil: FUTURE_WAKE,
+          unsettledAt: BEFORE_NOW,
+        });
+        const decided = yield* decideOrchestrationCommand({ command: reorderCommand, readModel });
+        const events = Array.isArray(decided) ? decided : [decided];
+        expect(events).toHaveLength(1);
+        for (const event of events) {
+          const projected = yield* projectEvent(readModel, { ...event, sequence: 1 });
+          expect(projected.threads[0]).toEqual({ ...readModel.threads[0], activeOrderKey: "m" });
+        }
+      }),
+  );
+
+  it.effect("keeps placement through metadata, pin and snooze, then resets it on settlement", () =>
+    Effect.gen(function* () {
+      let readModel = makeReadModel();
+      const steps = [
+        [reorderCommand, "m"],
+        [{ type: "thread.meta.update", title: "Renamed" }, "m"],
+        [{ type: "thread.pin", orderKey: "g" }, "m"],
+        [{ type: "thread.snooze", snoozedUntil: FUTURE_WAKE }, "m"],
+        [{ type: "thread.unsnooze", reason: "user" }, "m"],
+        [{ type: "thread.unpin" }, "m"],
+        [{ type: "thread.settle" }, null],
+        [{ type: "thread.unsettle", reason: "user" }, null],
+        [{ type: "thread.active.reorder", orderKey: "s" }, "s"],
+      ] as const;
+      for (const [index, [step, expectedKey]] of steps.entries()) {
+        const command: OrchestrationCommand = {
+          ...step,
+          commandId: CommandId.make(`lifecycle-${index}`),
+          threadId: THREAD_ID,
+        };
+        const decided = yield* decideOrchestrationCommand({ command, readModel });
+        const events = Array.isArray(decided) ? decided : [decided];
+        for (const event of events) {
+          readModel = yield* projectEvent(readModel, {
+            ...event,
+            sequence: readModel.snapshotSequence + 1,
+          });
+        }
+        expect(readModel.threads[0]?.activeOrderKey, command.type).toBe(expectedKey);
+      }
+      expect(readModel.threads[0]).toMatchObject({
+        title: "Renamed",
+        settledOverride: "active",
+        settledAt: null,
+        snoozedUntil: null,
+        pinnedAt: null,
+      });
+    }),
+  );
+});
```

## apps/server/src/orchestration/decider.ts

Status: modified; +36; −0

```diff
@@ -788,6 +788,42 @@
       };
     }
 
+    case "thread.active.reorder": {
+      const thread = yield* requireThreadNotArchived({
+        readModel,
+        command,
+        threadId: command.threadId,
+      });
+      const occurredAt = yield* nowIso;
+      // Snooze retains this slot. Changing it cannot wake the thread, and
+      // accepting it handles races with snooze and retained wake timestamps.
+      if (
+        thread.deletedAt !== null ||
+        thread.pinnedAt != null ||
+        thread.settledOverride === "settled"
+      ) {
+        return yield* new OrchestrationCommandInvariantError({
+          commandType: command.type,
+          detail: `thread ${command.threadId} is not active and cannot be reordered`,
+        });
+      }
+      return {
+        ...(yield* withEventBase({
+          aggregateKind: "thread",
+          aggregateId: command.threadId,
+          occurredAt,
+          commandId: command.commandId,
+        })),
+        type: "thread.meta-updated",
+        payload: {
+          threadId: command.threadId,
+          activeOrderKey: command.orderKey,
+          // Arranging the list is not thread activity or a lifecycle transition.
+          updatedAt: thread.updatedAt,
+        },
+      };
+    }
+
     case "thread.meta.update": {
       const thread = yield* requireThread({
         readModel,
```

## apps/server/src/orchestration/projector.test.ts

Status: modified; +1; −0

```diff
@@ -91,6 +91,7 @@
         createdAt: now,
         updatedAt: now,
         archivedAt: null,
+        activeOrderKey: null,
         settledOverride: null,
         settledAt: null,
         unsettledAt: null,
```

## apps/server/src/orchestration/projector.ts

Status: modified; +5; −0

```diff
@@ -344,6 +344,7 @@
             settledOverride: null,
             settledAt: null,
             unsettledAt: null,
+            activeOrderKey: null,
             snoozedUntil: null,
             snoozedAt: null,
             deletedAt: null,
@@ -406,6 +407,7 @@
             settledOverride: "settled",
             settledAt: payload.settledAt,
             unsettledAt: null,
+            activeOrderKey: null,
             updatedAt: payload.updatedAt,
           }),
         })),
@@ -500,6 +502,9 @@
           ...nextBase,
           threads: updateThread(nextBase.threads, payload.threadId, {
             ...(payload.title !== undefined ? { title: payload.title } : {}),
+            ...(payload.activeOrderKey !== undefined
+              ? { activeOrderKey: payload.activeOrderKey }
+              : {}),
             ...(payload.titleRegeneration !== undefined
               ? { titleRegeneration: payload.titleRegeneration }
               : {}),
```

## apps/server/src/persistence/Layers/ProjectionThreads.ts

Status: modified; +5; −0

```diff
@@ -54,6 +54,7 @@
           snoozed_at,
           pinned_at,
           pin_order_key,
+          active_order_key,
           title_regeneration_request_id,
           title_regeneration_started_at,
           latest_user_message_at,
@@ -84,6 +85,7 @@
           ${row.snoozedAt},
           ${row.pinnedAt},
           ${row.pinOrderKey ?? null},
+          ${row.activeOrderKey ?? null},
           ${row.titleRegenerationRequestId ?? null},
           ${row.titleRegenerationStartedAt ?? null},
           ${row.latestUserMessageAt},
@@ -114,6 +116,7 @@
           snoozed_at = excluded.snoozed_at,
           pinned_at = excluded.pinned_at,
           pin_order_key = excluded.pin_order_key,
+          active_order_key = excluded.active_order_key,
           title_regeneration_request_id = excluded.title_regeneration_request_id,
           title_regeneration_started_at = excluded.title_regeneration_started_at,
           latest_user_message_at = excluded.latest_user_message_at,
@@ -151,6 +154,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
@@ -190,6 +194,7 @@
           snoozed_at AS "snoozedAt",
           pinned_at AS "pinnedAt",
           pin_order_key AS "pinOrderKey",
+          active_order_key AS "activeOrderKey",
           title_regeneration_request_id AS "titleRegenerationRequestId",
           title_regeneration_started_at AS "titleRegenerationStartedAt",
           latest_user_message_at AS "latestUserMessageAt",
```

## apps/server/src/persistence/Migrations.ts

Status: modified; +2; −0

```diff
@@ -60,6 +60,7 @@
 import Migration0046 from "./Migrations/046_RepairAutomaticSettlementTimestamps.ts";
 import Migration0047 from "./Migrations/047_ProjectionProjectIcon.ts";
 import Migration0048 from "./Migrations/048_ProjectionThreadBranchPullRequest.ts";
+import Migration0049 from "./Migrations/049_ProjectionThreadsActiveOrderKey.ts";
 
 /**
  * Migration loader with all migrations defined inline.
@@ -120,6 +121,7 @@
   [46, "RepairAutomaticSettlementTimestamps", Migration0046],
   [47, "ProjectionProjectIcon", Migration0047],
   [48, "ProjectionThreadBranchPullRequest", Migration0048],
+  [49, "ProjectionThreadsActiveOrderKey", Migration0049],
 ] as const;
 
 export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const);
```

## apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.test.ts

Status: added; +44; −0

```diff
@@ -1,0 +1,44 @@
+import { assert, it } from "@effect/vitest";
+import * as Effect from "effect/Effect";
+import * as SqlClient from "effect/unstable/sql/SqlClient";
+import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
+
+import { runMigrations } from "../Migrations.ts";
+import migrateActiveOrderKey from "./049_ProjectionThreadsActiveOrderKey.ts";
+
+it.layer(NodeSqliteClient.layerMemory())("049_ProjectionThreadsActiveOrderKey", (it) => {
+  it.effect("migrates old threads without changing their timestamps or assigning an order", () =>
+    Effect.gen(function* () {
+      const sql = yield* SqlClient.SqlClient;
+      yield* runMigrations({ toMigrationInclusive: 48 });
+      const now = "2026-01-01T00:00:00.000Z";
+      yield* sql`
+        INSERT INTO projection_threads (
+          thread_id, project_id, title, model_selection_json, runtime_mode,
+          created_at, updated_at
+        ) VALUES (
+          'thread-1', 'project-1', 'Existing thread',
+          '{"instanceId":"codex","model":"gpt-5.4"}', 'full-access', ${now}, ${now}
+        )
+      `;
+      yield* runMigrations({ toMigrationInclusive: 49 });
+      const migrated = yield* sql<{ readonly activeOrderKey: string | null }>`
+        SELECT active_order_key AS "activeOrderKey" FROM projection_threads WHERE thread_id = 'thread-1'
+      `;
+      assert.deepEqual(migrated, [{ activeOrderKey: null }]);
+      // Recovery may run the same migration against a database that already
+      // has the column, including a placement written after the upgrade.
+      yield* sql`UPDATE projection_threads SET active_order_key = 'gm' WHERE thread_id = 'thread-1'`;
+      yield* migrateActiveOrderKey;
+      const rows = yield* sql<{
+        readonly activeOrderKey: string | null;
+        readonly createdAt: string;
+        readonly updatedAt: string;
+      }>`
+        SELECT active_order_key AS "activeOrderKey", created_at AS "createdAt", updated_at AS "updatedAt"
+        FROM projection_threads WHERE thread_id = 'thread-1'
+      `;
+      assert.deepEqual(rows, [{ activeOrderKey: "gm", createdAt: now, updatedAt: now }]);
+    }),
+  );
+});
```

## apps/server/src/persistence/Migrations/049_ProjectionThreadsActiveOrderKey.ts

Status: added; +15; −0

```diff
@@ -1,0 +1,15 @@
+import * as Effect from "effect/Effect";
+import * as SqlClient from "effect/unstable/sql/SqlClient";
+
+export default Effect.gen(function* () {
+  const sql = yield* SqlClient.SqlClient;
+  const columns = yield* sql<{ readonly name: string }>`
+    PRAGMA table_info(projection_threads)
+  `;
+  if (!columns.some((column) => column.name === "active_order_key")) {
+    yield* sql`
+      ALTER TABLE projection_threads
+      ADD COLUMN active_order_key TEXT
+    `;
+  }
+});
```

## apps/server/src/persistence/Services/ProjectionThreads.ts

Status: modified; +1; −0

```diff
@@ -47,6 +47,7 @@
   snoozedAt: Schema.NullOr(IsoDateTime),
   pinnedAt: Schema.NullOr(IsoDateTime),
   pinOrderKey: Schema.optional(Schema.NullOr(Schema.String)),
+  activeOrderKey: Schema.optional(Schema.NullOr(Schema.String)),
   titleRegenerationRequestId: Schema.optional(Schema.NullOr(CommandId)),
   titleRegenerationStartedAt: Schema.optional(Schema.NullOr(IsoDateTime)),
   latestUserMessageAt: Schema.NullOr(IsoDateTime),
```

## packages/client-runtime/src/operations/commands.test.ts

Status: modified; +21; −0

```diff
@@ -24,6 +24,7 @@
 import {
   archiveThread,
   createProject,
+  reorderActiveThread,
   settleThread,
   stopThreadSession,
   unsettleThread,
@@ -172,4 +173,24 @@
       ]);
     }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),
   );
+
+  it.effect("sends an active order key without changing activity timestamps", () =>
+    Effect.gen(function* () {
+      const dispatched: ClientOrchestrationCommand[] = [];
+      const supervisor = yield* makeSupervisor(dispatched);
+      yield* reorderActiveThread({
+        commandId: CommandId.make("reorder-command"),
+        threadId: ThreadId.make("thread-1"),
+        orderKey: "mf",
+      }).pipe(Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor));
+      expect(dispatched).toEqual([
+        {
+          type: "thread.active.reorder",
+          commandId: "reorder-command",
+          threadId: "thread-1",
+          orderKey: "mf",
+        },
+      ]);
+    }).pipe(Effect.provide(TEST_CRYPTO_LAYER)),
+  );
 });
```

## packages/client-runtime/src/operations/commands.ts

Status: modified; +11; −0

```diff
@@ -42,6 +42,7 @@
 export type PinThreadInput = CommandInput<"thread.pin">;
 export type UnpinThreadInput = CommandInput<"thread.unpin">;
 export type ReorderPinnedThreadInput = CommandInput<"thread.pin.reorder">;
+export type ReorderActiveThreadInput = CommandInput<"thread.active.reorder">;
 export type UpdateThreadMetadataInput = CommandInput<"thread.meta.update">;
 export type SetThreadRuntimeModeInput = CommandInput<"thread.runtime-mode.set">;
 export type SetThreadInteractionModeInput = CommandInput<"thread.interaction-mode.set">;
@@ -230,6 +231,16 @@
   });
 });
 
+export const reorderActiveThread: (input: ReorderActiveThreadInput) => CommandEffect = Effect.fn(
+  "EnvironmentCommands.reorderActiveThread",
+)(function* (input) {
+  return yield* dispatch({
+    ...input,
+    type: "thread.active.reorder",
+    commandId: yield* commandId(input),
+  });
+});
+
 export const updateThreadMetadata: (input: UpdateThreadMetadataInput) => CommandEffect = Effect.fn(
   "EnvironmentCommands.updateThreadMetadata",
 )(function* (input) {
```

## packages/client-runtime/src/state/entities.test.ts

Status: modified; +6; −0

```diff
@@ -208,6 +208,8 @@
       title: "Cached thread",
       branch: "stale-branch",
       worktreePath: "/repo/stale-worktree",
+      activeOrderKey: "t",
+      unsettledAt: "2026-03-09T10:00:00.000Z",
       deletedAt: null,
       messages,
       proposedPlans: [],
@@ -220,6 +222,8 @@
       title: "Current thread",
       branch: "current-branch",
       worktreePath: "/repo/current-worktree",
+      activeOrderKey: "f",
+      unsettledAt: "2026-03-09T12:00:00.000Z",
     };
 
     const merged = mergeEnvironmentThread(detail, shell);
@@ -228,6 +232,8 @@
       title: "Current thread",
       branch: "current-branch",
       worktreePath: "/repo/current-worktree",
+      activeOrderKey: "f",
+      unsettledAt: "2026-03-09T12:00:00.000Z",
     });
     expect(merged?.messages).toBe(messages);
   });
```

## packages/client-runtime/src/state/threadCommands.ts

Status: modified; +9; −0

```diff
@@ -19,6 +19,7 @@
   type SetThreadRuntimeModeInput,
   type PinThreadInput,
   type ReorderPinnedThreadInput,
+  type ReorderActiveThreadInput,
   type SettleThreadInput,
   type SnoozeThreadInput,
   type StartThreadTurnInput,
@@ -39,6 +40,7 @@
   setThreadRuntimeMode,
   pinThread,
   reorderPinnedThread,
+  reorderActiveThread,
   settleThread,
   snoozeThread,
   startThreadTurn,
@@ -63,6 +65,7 @@
   SetThreadRuntimeModeInput,
   PinThreadInput,
   ReorderPinnedThreadInput,
+  ReorderActiveThreadInput,
   SettleThreadInput,
   SnoozeThreadInput,
   StartThreadTurnInput,
@@ -150,6 +153,12 @@
       scheduler,
       concurrency,
     }),
+    reorderActive: createEnvironmentCommand(runtime, {
+      label: "environment-data:commands:thread:reorder-active",
+      execute: (input: ReorderActiveThreadInput) => reorderActiveThread(input),
+      scheduler,
+      concurrency,
+    }),
     updateMetadata: createEnvironmentCommand(runtime, {
       label: "environment-data:commands:thread:update-metadata",
       execute: (input: UpdateThreadMetadataInput) => updateThreadMetadata(input),
```

## packages/client-runtime/src/state/threadDetail.ts

Status: modified; +2; −0

```diff
@@ -58,6 +58,8 @@
     archivedAt: shell.archivedAt,
     settledOverride: shell.settledOverride,
     settledAt: shell.settledAt,
+    unsettledAt: shell.unsettledAt,
+    activeOrderKey: shell.activeOrderKey,
     snoozedUntil: shell.snoozedUntil,
     snoozedAt: shell.snoozedAt,
     pinnedAt: shell.pinnedAt,
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/9729/diffs?after=InBhY2thZ2VzL2NsaWVudC1ydW50aW1lL3NyYy9zdGF0ZS90aHJlYWREZXRhaWwudHMi&expectedVersion=1&format=markdown)
