Move archived threads off the main read path#1663
Move archived threads off the main read path#1663juliusmarminge wants to merge 2 commits intomainfrom
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Snapshot sync bypasses recovery coordinator sequence tracking
- Removed
refreshActiveSnapshotfromunarchiveThreadso the store is no longer updated outside the recovery coordinator; the EventRouter already handlesthread.unarchivedviafallbackToSnapshotRecoverywhich properly advanceslatestSequence.
- Removed
- ✅ Fixed: Loading state unreachable due to placeholderData always provided
- Changed
placeholderDatafrom(previous) => previous ?? EMPTY_ARCHIVED_THREADSto(previous) => previousso that on first load there is no placeholder data, allowingisPendingto betrueand the loading UI to render.
- Changed
Or push these changes by commenting:
@cursor push 95dd2b671c
Preview (95dd2b671c)
diff --git a/apps/web/src/hooks/useThreadActions.ts b/apps/web/src/hooks/useThreadActions.ts
--- a/apps/web/src/hooks/useThreadActions.ts
+++ b/apps/web/src/hooks/useThreadActions.ts
@@ -18,7 +18,6 @@
export function useThreadActions() {
const appSettings = useSettings();
- const syncServerReadModel = useStore((store) => store.syncServerReadModel);
const clearComposerDraftForThread = useComposerDraftStore((store) => store.clearDraftThread);
const clearProjectDraftThreadById = useComposerDraftStore(
(store) => store.clearProjectDraftThreadById,
@@ -33,13 +32,6 @@
const queryClient = useQueryClient();
const removeWorktreeMutation = useMutation(gitRemoveWorktreeMutationOptions({ queryClient }));
- const refreshActiveSnapshot = useCallback(async () => {
- const api = readNativeApi();
- if (!api) return;
- const snapshot = await api.orchestration.getActiveSnapshot();
- syncServerReadModel(snapshot);
- }, [syncServerReadModel]);
-
const archiveThread = useCallback(
async (threadId: ThreadId) => {
const api = readNativeApi();
@@ -73,10 +65,9 @@
commandId: newCommandId(),
threadId,
});
- await refreshActiveSnapshot();
void queryClient.invalidateQueries({ queryKey: orchestrationQueryKeys.archivedThreads() });
},
- [queryClient, refreshActiveSnapshot],
+ [queryClient],
);
const deleteThread = useCallback(
diff --git a/apps/web/src/lib/orchestrationReactQuery.ts b/apps/web/src/lib/orchestrationReactQuery.ts
--- a/apps/web/src/lib/orchestrationReactQuery.ts
+++ b/apps/web/src/lib/orchestrationReactQuery.ts
@@ -1,4 +1,3 @@
-import type { OrchestrationListArchivedThreadsResult } from "@t3tools/contracts";
import { queryOptions } from "@tanstack/react-query";
import { ensureNativeApi } from "~/nativeApi";
@@ -8,7 +7,6 @@
};
const DEFAULT_ARCHIVED_THREADS_STALE_TIME = 15_000;
-const EMPTY_ARCHIVED_THREADS: OrchestrationListArchivedThreadsResult = [];
export function archivedThreadsQueryOptions() {
return queryOptions({
@@ -18,6 +16,6 @@
return api.orchestration.listArchivedThreads();
},
staleTime: DEFAULT_ARCHIVED_THREADS_STALE_TIME,
- placeholderData: (previous) => previous ?? EMPTY_ARCHIVED_THREADS,
+ placeholderData: (previous) => previous,
});
}You can send follow-ups to this agent here.
ApprovabilityVerdict: Needs human review This PR changes the primary data read path by separating archived threads into a dedicated endpoint, affecting what data is returned to the UI by default. While well-tested and the refactoring is clean, the architectural change to data flow and new recovery logic for unarchiving warrants human review. You can customize Macroscope's approvability policy. Learn more. |
bbe5e14 to
097c3a3
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Skipped events permanently lost due to premature sequence advancement
- Split markEventBatchApplied into filterNewEvents (no side effects) and markEventBatchApplied (advances sequence), and deferred the sequence advancement in applyEventBatch until after the snapshot-recovery check passes, so events that trigger early return no longer inflate latestSequence.
- ✅ Fixed: Archived threads query not invalidated on snapshot-recovery path
- Added invalidateQueries for orchestrationQueryKeys.archivedThreads() in runSnapshotRecovery after syncing the snapshot, ensuring the ArchivedThreadsPanel reflects updated state immediately after snapshot recovery.
Or push these changes by commenting:
@cursor push 51aaa5d579
Preview (51aaa5d579)
diff --git a/apps/web/src/orchestrationRecovery.ts b/apps/web/src/orchestrationRecovery.ts
--- a/apps/web/src/orchestrationRecovery.ts
+++ b/apps/web/src/orchestrationRecovery.ts
@@ -62,10 +62,14 @@
return "apply";
},
- markEventBatchApplied<T extends SequencedEvent>(events: ReadonlyArray<T>): ReadonlyArray<T> {
- const nextEvents = events
+ filterNewEvents<T extends SequencedEvent>(events: ReadonlyArray<T>): ReadonlyArray<T> {
+ return events
.filter((event) => event.sequence > state.latestSequence)
.toSorted((left, right) => left.sequence - right.sequence);
+ },
+
+ markEventBatchApplied<T extends SequencedEvent>(events: ReadonlyArray<T>): ReadonlyArray<T> {
+ const nextEvents = this.filterNewEvents(events);
if (nextEvents.length === 0) {
return [];
}
diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx
--- a/apps/web/src/routes/__root.tsx
+++ b/apps/web/src/routes/__root.tsx
@@ -313,7 +313,7 @@
const applyEventBatch = (
events: ReadonlyArray<OrchestrationEvent>,
): "applied" | "snapshot-recovery-needed" => {
- const nextEvents = recovery.markEventBatchApplied(events);
+ const nextEvents = recovery.filterNewEvents(events);
if (nextEvents.length === 0) {
return "applied";
}
@@ -327,6 +327,8 @@
return "snapshot-recovery-needed";
}
+ recovery.markEventBatchApplied(nextEvents);
+
const batchEffects = deriveOrchestrationBatchEffects(nextEvents);
const needsProjectUiSync = nextEvents.some(
(event) =>
@@ -417,6 +419,9 @@
if (!disposed) {
syncServerReadModel(snapshot);
reconcileSnapshotDerivedState();
+ void queryClient.invalidateQueries({
+ queryKey: orchestrationQueryKeys.archivedThreads(),
+ });
if (recovery.completeSnapshotRecovery(snapshot.snapshotSequence)) {
void recoverFromSequenceGap();
}You can send follow-ups to this agent here.
097c3a3 to
07f4e5f
Compare
362933c to
8bb2822
Compare
07f4e5f to
0472757
Compare
8bb2822 to
379bef6
Compare
0472757 to
c21e9a8
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
c21e9a8 to
3b01bbe
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Loading state unreachable due to placeholderData
- Removed the
?? EMPTY_ARCHIVED_THREADSfallback fromplaceholderDataso it returnsundefinedon first load, allowing TanStack Query v5 to correctly start in pending status and render the loading UI.
- Removed the
Or push these changes by commenting:
@cursor push 5407e0b61e
Preview (5407e0b61e)
diff --git a/apps/web/src/lib/orchestrationReactQuery.ts b/apps/web/src/lib/orchestrationReactQuery.ts
--- a/apps/web/src/lib/orchestrationReactQuery.ts
+++ b/apps/web/src/lib/orchestrationReactQuery.ts
@@ -1,4 +1,3 @@
-import type { OrchestrationListArchivedThreadsResult } from "@t3tools/contracts";
import { queryOptions } from "@tanstack/react-query";
import { ensureNativeApi } from "~/nativeApi";
@@ -8,8 +7,6 @@
};
const DEFAULT_ARCHIVED_THREADS_STALE_TIME = 15_000;
-const EMPTY_ARCHIVED_THREADS: OrchestrationListArchivedThreadsResult = [];
-
export function archivedThreadsQueryOptions() {
return queryOptions({
queryKey: orchestrationQueryKeys.archivedThreads(),
@@ -18,6 +15,6 @@
return api.orchestration.listArchivedThreads();
},
staleTime: DEFAULT_ARCHIVED_THREADS_STALE_TIME,
- placeholderData: (previous) => previous ?? EMPTY_ARCHIVED_THREADS,
+ placeholderData: (previous) => previous,
});
}You can send follow-ups to this agent here.
| return api.orchestration.listArchivedThreads(); | ||
| }, | ||
| staleTime: DEFAULT_ARCHIVED_THREADS_STALE_TIME, | ||
| placeholderData: (previous) => previous ?? EMPTY_ARCHIVED_THREADS, |
There was a problem hiding this comment.
Loading state unreachable due to placeholderData
Medium Severity
The placeholderData in archivedThreadsQueryOptions always provides EMPTY_ARCHIVED_THREADS (an empty array) on first render. In TanStack Query v5, when placeholderData is set, the query status becomes "success" immediately, so isPending is always false. This makes the "Loading archived threads" UI in ArchivedThreadsPanel dead code — it can never render. Instead, users briefly see "No archived threads" before real data arrives, which is misleading when archived threads exist.
Additional Locations (1)
Co-authored-by: codex <codex@users.noreply.github.com>



Summary
Validation
bun fmtbun lintcd apps/server && bun run test src/orchestration/Layers/ProjectionSnapshotQuery.test.ts src/server.test.ts src/serverRuntimeStartup.test.ts src/checkpointing/Layers/CheckpointDiffQuery.test.ts src/orchestration/Layers/OrchestrationEngine.test.tsbun typecheck(still fails in pre-existing web files:apps/web/src/rpc/atomRegistry.tsx,apps/web/src/rpc/client.ts,apps/web/src/rpc/serverState.ts)Notes
cd apps/web && bun run test src/wsNativeApi.test.ts src/components/ChatView.browser.tsx src/components/KeybindingsToast.browser.tsxis currently blocked by the same existing@effect/atom-reactresolution issue fromapps/web/src/rpc/serverState.ts.ProjectionSnapshotQuery.getSnapshot()semantics are unchanged for orchestration engine bootstrap; the active-only snapshot is a separate UI read path.Note
Medium Risk
Introduces new WebSocket/Native API surface and changes the web bootstrap/recovery path to use an active-only snapshot, so mismatches between snapshot filtering and event-driven state could cause missing threads or stale UI until recovery.
Overview
Adds a new UI-facing orchestration read path:
ProjectionSnapshotQuery.getActiveSnapshot()hydrates a snapshot that excludes archived/deleted threads and their child rows, alongsidelistArchivedThreads()for lightweight archive page data.Plumbs these through contracts/RPC (
orchestration.getActiveSnapshot,orchestration.listArchivedThreads+ new error types), serverws.ts, and the web Native API/client; web bootstrap and snapshot-recovery now usegetActiveSnapshotand invalidate the archived-threads query on archive/unarchive/delete events (with a recovery fallback whenthread.unarchivedreferences an unknown thread).Refactors the archived threads settings panel to fetch via a dedicated React Query (
archivedThreadsQueryOptions) with loading/error states and server-derived grouping, and updates tests/mocks accordingly (including excluding archived threads ingetFirstActiveThreadIdByProjectId).Written by Cursor Bugbot for commit 4b155db. This will update automatically on new commits. Configure here.
Note
Move archived threads off the main read path by adding
getActiveSnapshotandlistArchivedThreadsRPCsgetSnapshotRPC withgetActiveSnapshot, which returns a read model containing only non-archived, non-deleted threads and projects, keeping archived data off the hot path.listArchivedThreadsRPC that returnsOrchestrationArchivedThreadSummaryrecords with project metadata, ordered byarchivedAtdescending.ArchivedThreadsPanelsettings UI now fetches archived threads via React Query (archivedThreadsQueryOptions) instead of reading from the store, and shows explicit loading/error states.EventRouternow callsgetActiveSnapshotfor snapshot recovery and invalidates thearchivedThreadsquery on thread archive/unarchive/delete events; it also triggers snapshot recovery when anthread.unarchivedevent references an unknown thread.getSnapshotis removed from all clients and server handlers; callers must usegetActiveSnapshotorlistArchivedThreadsinstead.Macroscope summarized 4b155db.