Skip to content

WIP: "review unsettled threads" view that summarizes in-progress work - #4417

Closed
t3dotgg wants to merge 23 commits into
mainfrom
t3code/sidebar-work-overview
Closed

WIP: "review unsettled threads" view that summarizes in-progress work#4417
t3dotgg wants to merge 23 commits into
mainfrom
t3code/sidebar-work-overview

Conversation

@t3dotgg

@t3dotgg t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member

Summary

Adds a one-click "Review unsettled work" sweep to sidebar v2. Clicking the new checklist button in the sidebar header opens /review-sweep and kicks off an agentic pass over every unsettled, unarchived thread across all connected environments:

  • an LLM summarizes each thread (what was asked, where it landed)
  • suggests a corrected title when the current one is misleading or a placeholder
  • recommends settling threads whose work clearly concluded

Results stream in progressively (client-side pool, concurrency 3, 50-thread cap favoring most-recently-active). Each card has Apply title / Settle / Dismiss / Retry; the header has Apply All. Nothing applies without a click. Results are ephemeral per run — no new persisted thread state.

Mock/design doc: https://b8x4s2sr11ko.postplan.dev

Current state:
image

Implementation

  • contracts: ReviewThreadSummaryInput/Result + ReviewThreadNotFoundError, new review.summarizeThread WS RPC, new reviewSweep capability flag so clients skip pre-feature servers under version skew.
  • server: new generateThreadReview op on the TextGeneration service, implemented in all five provider backends (Claude/Codex/Cursor/Grok/OpenCode) sharing buildThreadReviewPrompt (transcript capped to last 20 non-streaming messages, ~2k chars each) and a normalizeThreadReview sanitizer. ReviewService.summarizeThread reads the transcript via ProjectionSnapshotQuery and uses the server's textGenerationModelSelection.
  • client-runtime: summarizeThread RPC command atom in the review atom group.
  • web: reviewSweepStore.ts (non-persisted Zustand store + module-level runner with runId staleness guards; sweeps survive navigation), /review-sweep route + ReviewSweepView, SidebarV2 header button. Applying reuses the existing thread.meta.update and thread.settle commands.

Settle-safety invariant

A running / awaiting-approval / awaiting-input thread can never be settled through this feature, enforced at four independent layers:

  1. prompt rules (active threads: recommendSettle must be false)
  2. normalizeThreadReview forces it off for active threads
  3. the server re-derives activity from its own projection and never trusts the client's canSettleNow
  4. the client re-checks canSettle against the live shell at apply time

Testing

  • Full monorepo typecheck, lint, and fmt clean
  • New tests: ReviewService.summarizeThread (not-found, transcript extraction/streaming exclusion, stale-canSettleNow server override), buildThreadReviewPrompt (caps, active-thread rule), normalizeThreadReview (sanitization, placeholder-title drop)
  • Independent Codex (gpt-5.6-sol) review of the diff; both findings (server trusting client canSettleNow; cap sorting by createdAt instead of last activity) fixed with regression coverage
  • Not run: live end-to-end sweep in an authenticated browser session (pair-gated) — needs a manual pass: enable sidebar v2 in Beta settings, click the checklist icon, watch cards stream in, apply a title + settle

🤖 Generated with Claude Code


Note

Medium Risk
Touches thread transcripts sent to external LLMs from resolved worktrees, GitHub merge via gh, and settle recommendations—mitigated by server-side activity guards and click-to-apply, but merge/settle mistakes or prompt leakage remain possible if guards drift.

Overview
Introduces an end-to-end work review sweep for unsettled threads: the web app collects candidates (aligned with sidebar settlement rules and mirrored PR state), runs up to 50 reviews concurrently in the background, and triages results on /review-sweep into buckets (merge-ready, settle, title fix, attention, etc.) with opt-in apply actions.

Server & contracts: Advertises a reviewSweep capability and wires review.summarizeThread / review.mergePullRequest over WS. ReviewService.summarizeThread loads the thread from projections, resolves a strict workspace cwd (no process.cwd() fallback), re-derives activity (pending approvals/input, session state, queued turn start) so settle recommendations cannot trust the client alone, optionally enriches via gh pr view, calls new generateThreadReview across text-generation backends (shared prompt + normalizeThreadReview), and returns summary, nextStep, title/settle hints, checkpoint diff stats, PR status, and optional usage. mergePullRequest re-validates live GitHub state then gh pr merge --squash, returning structured outcomes (merged, conflict, not-ready, already-closed).

Web: reviewSweepStore (persisted run state, model picker defaulting to composer sticky selection), launch dialog + sidebar checklist control, progress/toasts, apply-all / per-card settle & rename, and a sequential merge queue that settles on success and startTurn handoff on conflicts.

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

Note

Add 'review unsettled threads' sweep view with AI summarization and PR merge support

  • Adds a new /review-sweep route and ReviewSweepView UI that groups unsettled threads into actionable buckets (merge-ready, settle, title, attention, failed, etc.) accessible via a new sidebar toolbar button.
  • Adds summarizeThread and mergePullRequest RPCs to ReviewService, backed by a new generateThreadReview text-generation operation implemented across all AI providers (Claude, Codex, Cursor, Grok, OpenCode).
  • summarizeThread builds a structured prompt from the thread transcript, optional PR context fetched via gh pr view, and diff stats; returns a summary, next step, suggested title, and settle recommendation.
  • mergePullRequest validates live PR state before invoking gh pr merge --squash, returning structured outcomes: merged, conflict, already-closed, or not-ready; on conflict, a rebase instruction is posted to the thread.
  • A new persisted Zustand store (reviewSweepStore.ts) manages sweep run lifecycle, concurrency-limited review workers, merge queue, and per-item actions (retry, apply title, settle, dismiss).
  • Risk: settle recommendations are suppressed server-side when activity is detected, but client-side canSettleNow can be overridden; stale runId guards prevent cross-run state corruption but require careful sequencing.

Macroscope summarized ec34a6f.

Summary by CodeRabbit

  • New Features

    • Added a Review Sweep workflow for quickly reviewing unsettled threads across projects.
    • View AI-generated summaries, suggested titles, and settle recommendations.
    • Apply recommendations individually or all at once, dismiss items, retry failed reviews, and re-run sweeps.
    • Added sidebar access through the “Review unsettled work” control.
    • Review results are generated through supported AI providers.
  • Bug Fixes

    • Settle recommendations are suppressed while a thread is still active or has queued work.
    • Review transcripts exclude streaming messages for more reliable summaries.

Adds a sidebar-v2 button that reviews every unsettled thread across all
connected environments: an LLM summarizes each thread, suggests a
corrected title when the current one is misleading, and recommends
settling threads whose work has concluded. Results stream into a new
/review-sweep page with per-item Apply buttons and Apply All — nothing
applies without a click.

- contracts: ReviewThreadSummaryInput/Result, review.summarizeThread WS
  RPC, reviewSweep capability flag (old servers are skipped)
- server: generateThreadReview TextGeneration op across all five
  providers, conservative prompt with transcript capping, and
  ReviewService.summarizeThread reading the projection
- client-runtime: summarizeThread RPC command atom
- web: reviewSweepStore runner (concurrency 3, runId-guarded, survives
  navigation), /review-sweep view, SidebarV2 entry button

Settle recommendations are blocked for active threads at four layers:
prompt rules, normalizeThreadReview, a server-side projection re-check
(never trusts the client's canSettleNow), and a client canSettle
re-check at apply time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is specific and matches the main change: a review sweep view that summarizes unsettled threads.
Description check ✅ Passed The description is detailed and covers what changed, why, UI, and testing, though it doesn't use the template headings exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch t3code/sidebar-work-overview

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 24, 2026
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts Outdated
Comment thread apps/server/src/review/ReviewService.ts
Comment thread apps/web/src/reviewSweepStore.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

8 blocking correctness issues found. This WIP PR introduces a significant new 'review unsettled threads' feature with new server RPCs, client state management, PR merge queues, and UI components. There are 25+ unresolved review comments identifying substantive bugs in merge handling, settle logic, and state management that warrant human attention.

You can customize Macroscope's approvability policy. Learn more.

@t3dotgg t3dotgg changed the title Add Review Sweep: one-click agentic review of unsettled work WIP: "review unsettled threads" view that summarizes in-progress work Jul 24, 2026
- macroscope: guard apply-title/apply-settle completions with the runId
  captured before the RPC so a Re-run mid-flight never stamps applied
  state onto the new run's cards; applyAll re-reads items from the
  store each step so mid-apply dismissals are respected, and bails out
  entirely when the run changes.
- cursor: the server's activity re-check now also treats a queued turn
  start (user message no session has adopted yet) as active, mirroring
  the decider's thread.settle guard; sweep candidate ordering uses
  NaN-safe toSortableTimestamp instead of raw Date.parse.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Addressed all four bot findings in 8aad08d:

  • macroscope (runId race on apply): applySweepTitle/applySweepSettle now capture the runId before the RPC and skip patchItem when it no longer matches, so a Re-run mid-flight never marks the new run's cards as already applied. applyAllSweepRecommendations bails out when the run changes.
  • macroscope (stale items snapshot in Apply All): both loops re-read the item from the store on each iteration, so dismissing a card mid-apply is respected.
  • cursor (queued turns miss settle block): the server-side activity re-check now also detects a queued turn start (user message no session has adopted yet, within the 2-minute grace window), mirroring the decider's thread.settle guard and the client's hasQueuedTurnStart. Regression test added.
  • cursor (unsafe Date.parse sort): candidate ordering now uses the NaN-safe toSortableTimestamp helper.

🤖 Generated with Claude Code

The sidebar button now only navigates to /review-sweep; the sweep no
longer auto-starts. The idle screen shows exactly what a run will do
before any model call happens: how many threads will be reviewed (and
how many the cap would skip), which text-generation model each
environment's server will use, and a cost/latency heads-up when the
run is large (15+ threads). Starting requires an explicit click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
t3dotgg and others added 2 commits July 23, 2026 19:42
The environment/model breakdown previously aggregated every sweep
candidate, so with more than SWEEP_MAX_THREADS unsettled threads it
could list environments whose threads would all be skipped and row
totals exceeding the actual call count. The summary now sorts and caps
candidates with the same helpers startReviewSweep uses, so the rows
always describe exactly the calls a run would make.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Environment labels can collide (e.g. identical hostnames); keying the
rows by label would make React mis-reconcile them. Key by the unique
environment id instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Latest bugbot pass (which reviewed dc66ff1):

  • Pre-run counts ignore truncation (medium): already fixed in f8b9d79, which landed just before this review posted — the summary now sorts and caps candidates with the same helpers startReviewSweep uses, so the per-environment rows describe exactly the calls a run would make.
  • Duplicate environment label keys (low): fixed in f8e63b2 — model rows are keyed by environment id instead of the potentially-colliding label.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/server/src/review/ReviewService.ts (1)

163-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mislabeled operation on non-generation errors.

mapProjectionError and the server-settings load failure both tag TextGenerationError.operation as "generateThreadReview", even though the actual failing step is a projection read or settings load, not the generation call. This will misdirect error triage toward the text-generation provider when the real fault is elsewhere.

Also applies to: 211-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/review/ReviewService.ts` around lines 163 - 168, Update the
TextGenerationError operation labels in mapProjectionError and the
server-settings load failure handling to identify their actual failing steps
rather than "generateThreadReview". Use distinct operation values for the thread
projection read and server-settings load, while preserving the existing error
details and cause propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/review/ReviewService.ts`:
- Around line 178-186: Update the cwd resolution in the review summary flow
around resolveThreadWorkspaceCwd so an unresolved thread workspace does not fall
back to process.cwd(). Instead, propagate a typed retryable error when the
project shell is unavailable or no workspace path can be resolved, preserving
the resolved workspace path for valid threads.

---

Nitpick comments:
In `@apps/server/src/review/ReviewService.ts`:
- Around line 163-168: Update the TextGenerationError operation labels in
mapProjectionError and the server-settings load failure handling to identify
their actual failing steps rather than "generateThreadReview". Use distinct
operation values for the thread projection read and server-settings load, while
preserving the existing error details and cause propagation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2fc3dff3-55aa-451c-833a-e81b598de55f

📥 Commits

Reviewing files that changed from the base of the PR and between bb38c33 and f8e63b2.

📒 Files selected for processing (26)
  • apps/server/src/environment/ServerEnvironment.ts
  • apps/server/src/git/GitManager.test.ts
  • apps/server/src/review/ReviewService.test.ts
  • apps/server/src/review/ReviewService.ts
  • apps/server/src/server.test.ts
  • apps/server/src/server.ts
  • apps/server/src/textGeneration/ClaudeTextGeneration.ts
  • apps/server/src/textGeneration/CodexTextGeneration.ts
  • apps/server/src/textGeneration/CursorTextGeneration.ts
  • apps/server/src/textGeneration/GrokTextGeneration.ts
  • apps/server/src/textGeneration/OpenCodeTextGeneration.ts
  • apps/server/src/textGeneration/TextGeneration.test.ts
  • apps/server/src/textGeneration/TextGeneration.ts
  • apps/server/src/textGeneration/TextGenerationPrompts.test.ts
  • apps/server/src/textGeneration/TextGenerationPrompts.ts
  • apps/server/src/textGeneration/TextGenerationUtils.ts
  • apps/server/src/ws.ts
  • apps/web/src/components/SidebarV2.tsx
  • apps/web/src/components/reviewSweep/ReviewSweepView.tsx
  • apps/web/src/reviewSweepStore.ts
  • apps/web/src/routeTree.gen.ts
  • apps/web/src/routes/review-sweep.tsx
  • packages/client-runtime/src/state/review.ts
  • packages/contracts/src/environment.ts
  • packages/contracts/src/review.ts
  • packages/contracts/src/rpc.ts

Comment on lines +178 to +186
const projectOption = yield* projectionSnapshotQuery
.getProjectShellById(thread.projectId)
.pipe(Effect.mapError(mapProjectionError));
const cwd =
resolveThreadWorkspaceCwd({
thread,
projects: Option.isSome(projectOption) ? [projectOption.value] : [],
}) ?? process.cwd();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't fall back to process.cwd() when the thread's workspace can't be resolved.

If getProjectShellById returns None (deleted/renamed project, projection lag, etc.) and the thread has no worktreePath, cwd silently becomes the server process's own working directory rather than the thread's project. That directory is then handed straight to the provider CLI spawn (see ClaudeTextGeneration.generateThreadReview / CodexTextGeneration.generateThreadReview), which can read local context files (e.g. AGENTS.md/CLAUDE.md) from that cwd and mix unrelated content into the prompt sent to an external LLM. Since review-sweep runs unattended over up to 50 threads with concurrency 3, this silent fallback could review "the server's own directory" instead of failing loudly.

Prefer failing the summary for that thread (surfacing a typed error the client can show as a retry-able failure) over guessing a cwd.

🔒 Proposed fix: fail instead of silently falling back
-    const cwd =
-      resolveThreadWorkspaceCwd({
-        thread,
-        projects: Option.isSome(projectOption) ? [projectOption.value] : [],
-      }) ?? process.cwd();
+    const cwd = resolveThreadWorkspaceCwd({
+      thread,
+      projects: Option.isSome(projectOption) ? [projectOption.value] : [],
+    });
+    if (cwd === undefined) {
+      return yield* new TextGenerationError({
+        operation: "generateThreadReview",
+        detail: `Unable to resolve a workspace cwd for thread '${input.threadId}'.`,
+      });
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/server/src/review/ReviewService.ts` around lines 178 - 186, Update the
cwd resolution in the review summary flow around resolveThreadWorkspaceCwd so an
unresolved thread workspace does not fall back to process.cwd(). Instead,
propagate a typed retryable error when the project shell is unavailable or no
workspace path can be resolved, preserving the resolved workspace path for valid
threads.

t3dotgg and others added 2 commits July 23, 2026 22:12
The provider CLI spawn reads local context files (AGENTS.md, CLAUDE.md)
from its cwd, so silently falling back to process.cwd() when a thread
has no worktree and its project shell is missing would mix the server's
own directory contents into an external LLM prompt. Return a typed
TextGenerationError instead — the sweep UI already renders it as a
retry-able error card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…verview

# Conflicts:
#	apps/server/src/environment/ServerEnvironment.ts
#	packages/contracts/src/environment.ts
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Two more updates:

  • coderabbit (major, security): fixed in 27a70df-range commit "Fail thread review loudly when workspace cwd is unresolvable" — summarizeThread no longer falls back to process.cwd() when a thread has no worktree and its project shell is missing. It now returns a typed TextGenerationError (rendered as a retry-able error card in the sweep UI), so the provider CLI can never read the server's own directory context into an external LLM prompt. Regression test added.
  • Conflicts with main resolved: merged origin/main (thread snoozing, feat(sidebar-v2): thread snoozing #4311). Both features add a capability flag and touch the SidebarV2 header; resolution keeps threadSnooze and reviewSweep side by side. Snoozed threads remain unsettled work, so the sweep intentionally still reviews them. All typechecks and the 31 touched-area tests pass post-merge.

🤖 Generated with Claude Code

Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/server/src/review/ReviewService.ts
- Group sweep cards and resolve project titles by scoped
  (environmentId, projectId) key — bare project ids can collide across
  connected environments.
- Withdraw a settle recommendation from the card when the apply-time
  canSettle guard refuses it, instead of leaving a CTA the guard will
  keep rejecting.
- Document why summarizeThread's two projection reads are ordered
  transcript-then-shell: reading the activity view last catches
  sessions that start mid-read, and the reverse skew only affects
  summary staleness — settles are re-validated at apply time and by
  the decider, so no consistency window can settle live work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 24, 2026

Copy link
Copy Markdown
Member Author

Cursor's post-merge review pass, addressed in c68e9fa:

  • Project groups ignore environment scope (medium): fixed — grouping and title lookup now key on scoped (environmentId, projectId) instead of the bare project id, which can collide across environments.
  • Stale settle CTA after activity block (medium): fixed — when the apply-time canSettle guard refuses a settle, the recommendation is withdrawn from the card (alongside the existing toast) instead of leaving a CTA the guard will keep rejecting.
  • Inconsistent projection reads for settle (medium): not a bug, documented instead. The transcript→shell read order is deliberate: reading the activity view last catches sessions that start between the reads. The reverse skew can only make summary text slightly stale — a settle recommendation still passes the client's apply-time canSettle re-check and the decider's thread.settle guards, so no consistency window can settle live work. Rationale is now in a comment at the read site.

🤖 Generated with Claude Code

Real-data testing surfaced the gap the plan had flagged: the sweep
passed changeRequestState: null, so threads the sidebar auto-settles
(merged/closed PR + idle) counted as unsettled — 18 candidates vs ~7
sidebar cards. SidebarV2 now mirrors its per-row PR-state map into the
sweep store, isSweepCandidate consults it, and the pre-run summary
subscribes to a version counter so its count tracks PR states as they
stream in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts
Real-data feedback: uniform gray cards grouped by project buried the
actionable items. The results view is now organized by what the user
should do, ordered easiest-to-clear first:

  1. Ready to settle (one-click, emerald accent)
  2. Title fixes (sky)
  3. Needs your attention (amber — awaiting review/input)
  4. In flight / review-failed / still-reviewing tails

Cards are denser, carry a per-bucket accent stripe and a single
primary action, and gain a metadata row: project favicon + name,
branch, thread age, and diff size. Diff stats (+adds/−dels/files) come
from the thread's latest ready checkpoint, returned by the review RPC
as a new optional diffStats field — no extra client requests. Within
each bucket, smallest diff sorts first to build clearing momentum.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/server/src/review/ReviewService.ts Outdated
additions: latestCheckpoint.files.reduce((sum, file) => sum + file.additions, 0),
deletions: latestCheckpoint.files.reduce((sum, file) => sum + file.deletions, 0),
}
: undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Diff stats use last turn only

Medium Severity

The diffStats calculation uses only the latest checkpoint's diff. Checkpoints store per-turn changes, not cumulative thread diffs. This understates the total diff for multi-turn threads, impacting features like sorting that assume it reflects the whole thread's effort.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f508f42. Configure here.

const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
if (blocked) return "attention";
if (working) return "inFlight";
return "attention";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Queued work mislabeled as attention

Low Severity

Live classification treats only session running/starting as in flight. A just-sent user message that has not been adopted yet (hasQueuedTurnStart) falls through to attention with “Waiting on you,” even though the next step is the agent, not the user.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f508f42. Configure here.

Real-data feedback round two:

- Results render as a responsive grid (1/2/3 columns) instead of a
  full-width list.
- While the sweep runs, the page shows only a progress count
  ("Reviewing your work — 7 of 18"); results reveal all at once when
  every thread is done, so triage happens in one pass over a stable
  layout instead of chasing cards as they pop in.
- The model now returns a one-sentence imperative nextStep ("Review
  and merge PR #4415.") which becomes the card body — a direct answer
  to "what should I do here". Summaries are capped at one sentence and
  demoted to an info-icon tooltip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The screenshot from real-data testing showed nextStep rambling into
status recaps ("The requested comparison and hosted write-up are
complete, and..."). The prompt now demands a verb-first command of at
most 10 words with explicit GOOD/BAD examples, and normalizeThreadReview
backstops drift by truncating to the first sentence, hard-capped at 80
chars. Attention-bucket cards fall back to the summary when an older
server omits nextStep, so they always say what's being waited on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx Outdated
Comment thread apps/server/src/textGeneration/TextGenerationUtils.ts
Comment thread apps/server/src/textGeneration/TextGenerationUtils.ts
Four features from real-data testing feedback:

- The sidebar button now opens a launch modal (scope + model + cost
  summary) instead of navigating. Starting closes the modal and shows
  a "running in background" toast; completion raises a toast with an
  actionable-count summary and a "View results" link. The sweep was
  already navigation-proof; now the UX matches.

- Reviews investigate deeper: when a thread's branch has a PR, the
  server fetches state, review decision, CI rollup, mergeability, and
  the last five comments via `gh pr view`, feeds them into the review
  prompt, and returns a prStatus with a computed mergeReady flag
  (open + clean merge + CI not failing + not blocked on review).
  Lookups degrade to "no PR context" on any failure.

- New "Merge ready" bucket at the top of the results grid with
  per-card "Merge & settle" and a section-level "Merge all (N)".

- Merging runs through a SEQUENTIAL queue: each PR is re-validated
  against live GitHub state before merging (an earlier queue entry may
  have just changed the base), merged PRs settle their thread
  immediately, and a conflict marks the card and hands the rebase to
  the thread's own agent via a turn message — the queue continues past
  it. review.mergePullRequest returns conflict/already-closed/not-ready
  as outcomes rather than errors so the queue can branch on them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +270 to +276
const recent = input.recentMessages.slice(-THREAD_REVIEW_RECENT_MESSAGE_COUNT);
const transcript = recent
.map(
(message) =>
`[${message.role}] ${limitSection(message.text, THREAD_REVIEW_MESSAGE_CHAR_LIMIT)}`,
)
.join("\n\n");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium textGeneration/TextGenerationPrompts.ts:270

limitSection(transcript, 24_000) truncates from the front, so when the selected messages exceed 24,000 characters the newest messages are dropped and only older ones remain. This can cause the model to miss the thread's final outcome and produce an incorrect summary or settle recommendation. Consider applying the aggregate cap from the tail so the newest messages are preserved.

  const recent = input.recentMessages.slice(-THREAD_REVIEW_RECENT_MESSAGE_COUNT);
+  let transcriptChars = 0;
  const transcript = recent
    .map(
      (message) =>
        `[${message.role}] ${limitSection(message.text, THREAD_REVIEW_MESSAGE_CHAR_LIMIT)}`,
    )
+    .reverse()
+    .filter((line) => (transcriptChars += line.length + 2) <= 24_000)
+    .reverse()
    .join("\n\n");
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/textGeneration/TextGenerationPrompts.ts around lines 270-276:

`limitSection(transcript, 24_000)` truncates from the front, so when the selected messages exceed 24,000 characters the newest messages are dropped and only older ones remain. This can cause the model to miss the thread's final outcome and produce an incorrect summary or settle recommendation. Consider applying the aggregate cap from the tail so the newest messages are preserved.

}

const outcome = result.value.outcome;
if (outcome === "merged" || outcome === "already-closed") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High src/reviewSweepStore.ts:486

mergeOne treats the server outcome "already-closed" as "merged", so a PR that was merely closed (not merged) gets its card marked merged and its thread automatically settled. A merge-ready PR can be closed without merging between the sweep review and this action, and this branch incorrectly concludes the work as merged and settles the thread. Consider only matching the "merged" outcome here and handling "already-closed" as a non-merged terminal state.

Suggested change
if (outcome === "merged" || outcome === "already-closed") {
if (outcome === "merged") {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 486:

`mergeOne` treats the server outcome `"already-closed"` as `"merged"`, so a PR that was merely closed (not merged) gets its card marked `merged` and its thread automatically settled. A merge-ready PR can be closed without merging between the sweep review and this action, and this branch incorrectly concludes the work as merged and settles the thread. Consider only matching the `"merged"` outcome here and handling `"already-closed"` as a non-merged terminal state.

after.patchItem(key, { mergeStatus: "conflicted", mergeDetail: result.value.detail });
// Hand the conflict to the thread's own agent: it has the branch
// checked out and the full context to rebase and resolve.
await runAtomCommand(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/reviewSweepStore.ts:509

When threadEnvironment.startTurn fails in the conflict branch, mergeOne still returns "conflict" and leaves the card marked conflicted, so the UI shows "agent rebasing" and the queue toast reports the handoff succeeded — but no agent was actually asked to resolve it. The PR can remain stuck with no signal for the user to intervene. Consider checking the startTurn result and, on failure, patching the card to failed with the error detail and returning "failed" instead of "conflict".

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 509:

When `threadEnvironment.startTurn` fails in the conflict branch, `mergeOne` still returns `"conflict"` and leaves the card marked `conflicted`, so the UI shows "agent rebasing" and the queue toast reports the handoff succeeded — but no agent was actually asked to resolve it. The PR can remain stuck with no signal for the user to intervene. Consider checking the `startTurn` result and, on failure, patching the card to `failed` with the error detail and returning `"failed"` instead of `"conflict"`.

Comment on lines +135 to +145
if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
if (item.result.recommendSettle && !item.settleApplied) return "settle";
if (item.result.suggestedTitle && !item.titleApplied) return "title";
// Live shell wins over review-time knowledge: a thread blocked on the user
// right now belongs in "attention" even if the review predates the block.
const shell = readThreadShell(item.ref);
const working = shell?.session?.status === "running" || shell?.session?.status === "starting";
const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
if (blocked) return "attention";
if (working) return "inFlight";
return "attention";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium reviewSweep/ReviewSweepView.tsx:135

A completed item with recommendSettle: true is classified into the settle bucket even when its live shell now has pending approvals, pending user input, or a running session. This contradicts the intended live-state precedence: the comment says "Live shell wins over review-time knowledge," but the recommendSettle check runs before the blocked/working checks, so a thread that became blocked after the review still shows a stale Settle action (rejected only after the user clicks). Reorder so the live-shell checks take priority over the recommendation checks.

-  if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
-  if (item.result.recommendSettle && !item.settleApplied) return "settle";
-  if (item.result.suggestedTitle && !item.titleApplied) return "title";
   // Live shell wins over review-time knowledge: a thread blocked on the user
   // right now belongs in "attention" even if the review predates the block.
   const shell = readThreadShell(item.ref);
   const working = shell?.session?.status === "running" || shell?.session?.status === "starting";
   const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
   if (blocked) return "attention";
   if (working) return "inFlight";
+  if (item.result.prStatus?.mergeReady && !item.settleApplied) return "mergeReady";
+  if (item.result.recommendSettle && !item.settleApplied) return "settle";
+  if (item.result.suggestedTitle && !item.titleApplied) return "title";
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/reviewSweep/ReviewSweepView.tsx around lines 135-145:

A completed item with `recommendSettle: true` is classified into the `settle` bucket even when its live shell now has pending approvals, pending user input, or a running session. This contradicts the intended live-state precedence: the comment says "Live shell wins over review-time knowledge," but the `recommendSettle` check runs before the `blocked`/`working` checks, so a thread that became blocked after the review still shows a stale Settle action (rejected only after the user clicks). Reorder so the live-shell checks take priority over the recommendation checks.

},
{ reportFailure: false },
);
if (settleResult._tag === "Success") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/reviewSweepStore.ts:499

When a PR merge succeeds but the subsequent threadEnvironment.settle call fails, mergeOne ignores the failure and returns "merged" with mergeStatus set to "merged". This leaves the thread unsettled with no error surfaced and no retry path — the card stops offering any action once mergeStatus becomes "merged". Consider surfacing the settle failure (e.g., via a toast or mergeDetail) so the user knows the thread still needs to be settled.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 499:

When a PR merge succeeds but the subsequent `threadEnvironment.settle` call fails, `mergeOne` ignores the failure and returns `"merged"` with `mergeStatus` set to `"merged"`. This leaves the thread unsettled with no error surfaced and no retry path — the card stops offering any action once `mergeStatus` becomes `"merged"`. Consider surfacing the settle failure (e.g., via a toast or `mergeDetail`) so the user knows the thread still needs to be settled.

Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/web/src/reviewSweepStore.ts
Comment thread apps/server/src/review/ReviewService.ts

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One error-handling convention issue found in the new review service code. See inline comment.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/server/src/review/ReviewService.ts
t3dotgg and others added 2 commits July 26, 2026 19:38
Merged origin/main (9 commits) and fixed review findings:

- Macroscope (CI failure): ReviewMergeError dropped the underlying
  failure by stringifying it into `detail`. It now carries an optional
  `cause` and uses a static detail, matching the sibling mapper in
  summarizeThread.
- Cursor (medium, real bug): diffStats used only the latest checkpoint,
  but CheckpointReactor diffs previous→current, so each checkpoint's
  files are that turn's delta. Now sums every ready checkpoint, deduping
  the file count by path. Regression test added.
- Cursor (medium): SidebarV2 remounting published an empty PR-state map
  and wiped the mirrored cache, making merged-PR threads look unsettled.
  publishSweepChangeRequestStates now merges instead of replacing.
- Macroscope (medium): the buckets memo read live shells via
  readThreadShell but only depended on items, so cards could stay in a
  stale bucket. It now depends on thread shells too.
- Cursor (low): sweep candidate selection used a raw wall clock while
  SidebarV2 quantizes to the minute, disagreeing near the merged-PR idle
  boundary. Both now use the same minute-quantized clock.
- Cursor (low): threads with a queued turn start are classified as
  in-flight rather than needing attention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@t3dotgg

t3dotgg commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Synced with main (9 commits, clean merge) and addressed the latest review round in c1b3390:

Fixed

  • Macroscope — Effect service conventions (CI failure): ReviewMergeError stringified the underlying failure into detail and dropped it. It now carries an optional cause with a static detail, matching summarizeThread's mapper.
  • Cursor — "Diff stats use last turn only" (medium, real bug): confirmed against CheckpointReactor, which diffs previous→current, so each checkpoint's files is that turn's delta — my "checkpoints stack" comment was wrong. Now sums every ready checkpoint and dedupes the file count by path. Regression test added.
  • Cursor — "Remount wipes mirrored PR states" (medium): publishSweepChangeRequestStates now merges instead of replacing, so a SidebarV2 remount publishing an empty map can't make merged-PR threads look unsettled again.
  • Macroscope — stale bucket memo (medium): classifySweepItem reads live shells, but the memo only depended on items. It now depends on thread shells too.
  • Cursor — "Sweep clock breaks settle parity" (low): candidate selection now uses the same minute-quantized clock as SidebarV2 (sweepNowIso), shared by the pre-run summary.
  • Cursor — "Queued work mislabeled as attention" (low): threads with a queued turn start classify as in-flight.

Already fixed in earlier commits (re-posted against the new diff): "Project groups ignore environment scope" (c68e9fa — grouping keys on scoped project refs), "Stale settle CTA after activity block" (c68e9fa — the recommendation is withdrawn when the apply-time guard refuses), and "Inconsistent projection reads for settle" (documented at the read site: the transcript→shell order deliberately catches sessions that start mid-read, and settles are re-validated at apply time and by the decider).

Typecheck, lint, and fmt clean; 14 ReviewService tests and 76 tests across the touched server areas pass.

🤖 Generated with Claude Code

},
{ reportFailure: false },
);
return "conflict";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conflict handoff failure goes unseen

High Severity

The merge queue prematurely updates an item's status to 'conflicted' and 'agent rebasing' when a merge conflict occurs. The subsequent startTurn command, intended to hand off the rebase to the agent, ignores its own failures. This can lead to silently unresolved conflicts, as the UI falsely indicates the agent is working without user notification or a retry path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c1b3390. Configure here.

if (settleResult._tag === "Success") {
useReviewSweepStore.getState().patchItem(key, { settleApplied: true });
}
return "merged";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale settle flag after re-run

Medium Severity

After a successful merge, settleApplied is written with no runId check. A Re-run during that settle RPC can stamp the new run’s card as already settled, hiding merge/settle CTAs for work that was never applied in the new sweep.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c1b3390. Configure here.

// Bump the store version so the pre-run summary recomputes its candidate
// count as PR states stream in from the sidebar rows.
useReviewSweepStore.getState().bumpCandidateVersion();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR state cache never clears

Medium Severity

publishSweepChangeRequestStates only merges keys and never deletes. When SidebarV2 clears a thread’s PR state to null, the sweep cache keeps a stale merged/closed value, so idle threads can be treated as settled and omitted from the candidate set while the sidebar still shows them unsettled.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c1b3390. Configure here.

Comment thread apps/web/src/reviewSweepStore.ts
if (settleResult._tag === "Success") {
useReviewSweepStore.getState().patchItem(key, { settleApplied: true });
}
return "merged";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge settle skips live guard

High Severity

mergeOne settles after merged or already-closed without re-checking canSettle on the live shell. That breaks the settle-safety invariant other apply paths enforce, so a thread that became active between review and merge can still be settled if the decider race window is missed, hiding in-flight work from the sidebar.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcd4dfa. Configure here.

const blocked = shell?.hasPendingApprovals === true || shell?.hasPendingUserInput === true;
if (blocked) return "attention";
if (working) return "inFlight";
return "attention";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completed cards mis-bucketed

Medium Severity

classifySweepItem exits the merge-ready and settle buckets once settleApplied is true, then falls through to attention. Successfully merged or settled cards therefore reappear under Needs your attention, and the Settled badge path in the settle bucket is unreachable.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bcd4dfa. Configure here.

autoSettleAfterDays: options.autoSettleAfterDays,
changeRequestState,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snoozed threads enter the sweep

Medium Severity

isSweepCandidate mirrors sidebar settlement via effectiveSettled but never checks effectiveSnoozed. SidebarV2 keeps snoozed threads on a separate shelf out of the active unsettled list, so the sweep still reviews, recommends, and can settle/merge work the user explicitly hid.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 900a40e. Configure here.

shell.session?.status === "running" ||
hasQueuedTurnStart(shell, DateTime.toEpochMillis(now));
const canSettleNow = input.canSettleNow && !serverSideActive;
const isActive = !canSettleNow;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client flag marks idle threads active

Medium Severity

isActive is derived as !canSettleNow, so a client canSettleNow: false forces the prompt’s ACTIVE rules even when the server’s own projection shows the thread idle. That suppresses settle recommendations and can produce misleading next-step guidance for settleable threads.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 900a40e. Configure here.

mergeable === true &&
checksPassing !== false &&
reviewDecision !== "CHANGES_REQUESTED" &&
reviewDecision !== "REVIEW_REQUIRED";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown CI still marks merge-ready

Medium Severity

toPrStatus sets mergeReady when checksPassing !== false, so a null/unknown CI rollup still counts as merge-ready. The contract describes mergeReady as requiring CI green, and the sweep will offer Merge & settle / Merge all for those PRs.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 900a40e. Configure here.

The server's textGenerationModelSelection is tuned for commit messages
and PR bodies — the wrong default for reasoning over a thread. The
sweep now sends an explicit modelSelection, defaulting to the composer
picker's sticky (last-used) model, and the launch modal embeds the same
ProviderModelPicker so it can be changed per run. The server falls back
to its text-gen setting only when a client sends no selection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
}),
),
);
if (!mergeResult.merged) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High review/ReviewService.ts:527

mergePullRequest returns outcome: "merged" whenever gh pr merge --squash exits successfully, but with a GitHub merge queue that command can enqueue the PR and exit while the PR is still open. The downstream caller then settles the thread even though the merge may later fail or conflict. Consider re-querying the PR with gh pr view after the merge command and only returning "merged" when the PR state is actually MERGED (or surfacing a separate "queued" outcome).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/review/ReviewService.ts around line 527:

`mergePullRequest` returns `outcome: "merged"` whenever `gh pr merge --squash` exits successfully, but with a GitHub merge queue that command can enqueue the PR and exit while the PR is still open. The downstream caller then settles the thread even though the merge may later fail or conflict. Consider re-querying the PR with `gh pr view` after the merge command and only returning `"merged"` when the PR state is actually `MERGED` (or surfacing a separate `"queued"` outcome).

return [...candidates].sort((a, b) => activityMs(b) - activityMs(a));
}

async function reviewOne(runId: number, item: SweepItem): Promise<void> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/reviewSweepStore.ts:224

reviewOne sends the same readSweepModelSelection() value to every target environment, but that selection originates from the primary environment's composer picker. Provider instance IDs are server-local, so when a secondary environment doesn't have that instance the review fails instead of falling back to that server's own default model. The same path also fails when the sticky instance was deleted from the primary server after being selected. Validate the selection against the target environment's available models, or omit modelSelection so each server uses its own default when the instance is unavailable.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/reviewSweepStore.ts around line 224:

`reviewOne` sends the same `readSweepModelSelection()` value to every target environment, but that selection originates from the primary environment's composer picker. Provider instance IDs are server-local, so when a secondary environment doesn't have that instance the review fails instead of falling back to that server's own default model. The same path also fails when the sticky instance was deleted from the primary server after being selected. Validate the selection against the target environment's available models, or omit `modelSelection` so each server uses its own default when the instance is unavailable.

Comment thread apps/web/src/components/reviewSweep/ReviewSweepView.tsx
threadId: item.ref.threadId,
canSettleNow,
...(modelSelection !== null ? { modelSelection } : {}),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-env model selection breaks reviews

Medium Severity

The sweep now applies a single client-derived modelSelection to all environments. Since ProviderInstanceId is server-specific, environments lacking that instance fail summarizeThread with a TextGenerationError instead of using their own default text-gen settings.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit a8c3047. Configure here.

The modal exposed only the model, so reasoning effort, thinking, and
fast mode weren't reachable — and worse, picking a model called
createModelSelection without options, silently clearing whatever traits
the sticky selection carried.

The modal now renders the composer's TraitsPicker beside the model
picker, and a model switch preserves the current options when the
instance is unchanged (a different instance's options aren't
transferable, so those start clean). No server change needed: the
provider CLIs already derive effort/thinking/fastMode/ultracode from
modelSelection.options.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
model,
instanceId === activeInstanceId ? activeOptions : undefined,
),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsanitized traits survive model switches

Medium Severity

Same-instance model changes reuse raw activeOptions in createModelSelection without the sanitization resolveAppModelSelectionState / getComposerProviderState apply in settings text-gen. Invalid effort or other trait values can remain in the stored sweep selection and be sent on summarizeThread, while the Traits UI may already show the resolved default.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c9dbfc. Configure here.

The in-progress view was a spinner and a counter, which wasted the most
interesting moment of a multi-minute sweep. It now shows a progress bar
plus a live list of every thread being reviewed — running ones first —
with project favicon, a per-thread elapsed clock that ticks while the
review runs, and the tokens and cost each review actually consumed.
The header aggregates run totals.

Usage is real, not estimated: the Claude CLI's `--output-format json`
envelope already reports `usage` and `total_cost_usd`, so
runClaudeJson now surfaces them through an optional onUsage sink,
generateThreadReview attaches them to its result, and the review RPC
returns them as an optional `usage` field. Input counts fold in
cache-creation and cache-read tokens so the number matches billing.
Providers that report nothing simply omit the field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
: {}),
durationMs: Math.round(envelope.duration_api_ms ?? 0),
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zero usage when absent

Low Severity

The onUsage callback in runClaudeJson defaults missing token/duration telemetry to zero. This causes generateThreadReview to always attach a usage object, even when the Claude CLI reports no telemetry, leading to "0 tok" in the UI where usage should be absent.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f70a4e5. Configure here.

cache_read_input_tokens: Schema.optionalKey(Schema.NullOr(Schema.Number)),
}),
),
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict Claude usage schema

Medium Severity

ClaudeOutputEnvelope now schema-validates optional usage, total_cost_usd, and duration_api_ms. That decoder is shared by every Claude JSON text-gen op, so a CLI envelope whose telemetry fields are present but differently typed or shaped fails the whole decode as “unexpected output format,” including commit/PR/title generation that previously ignored those extras. Elsewhere Claude usage is soft-parsed for the same reason.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f70a4e5. Configure here.

t3dotgg and others added 3 commits July 27, 2026 00:13
Results were ephemeral module state, so a reload or accidental
navigation threw away a multi-minute (and real-money) run. The store
now persists to localStorage. In-flight RPCs can't survive a reload,
so pending/running items rehydrate as retry-able errors and an
interrupted run reports as complete — the header offers Re-run rather
than a progress bar that would never advance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The results view used ad-hoc chrome: colored left-border card stripes,
uppercase micro-headers, and raw-div lists. Restyled to the same
patterns as the settings pages: section headers with tinted icon chips
and a title/hint stack, standard Card surfaces with the app's title
tracking and 13px muted body copy, hover-revealed card actions, the
settings scroll-fade container, and the progress list inside a Card
with row dividers. Cards drop the accent stripe — the section a card
sits in already says what kind it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
<Button
size="xs"
variant="ghost"
disabled={running}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium reviewSweep/ReviewSweepView.tsx:814

The Re-run button stays enabled during a merge, so clicking it mid-merge leaves successfully merged PRs unsettled. mergeOne sends the merge request, then checks the stale-run guard after startReviewSweep bumps runId; the guard aborts before the follow-up settle runs, so the merged thread never settles and its merge status is lost from the UI. Disable Re-run while any merge or apply operation is active — not just while phase is "running".

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/reviewSweep/ReviewSweepView.tsx around line 814:

The **Re-run** button stays enabled during a merge, so clicking it mid-merge leaves successfully merged PRs unsettled. `mergeOne` sends the merge request, then checks the stale-run guard after `startReviewSweep` bumps `runId`; the guard aborts before the follow-up settle runs, so the merged thread never settles and its merge status is lost from the UI. Disable Re-run while any merge or apply operation is active — not just while `phase` is `"running"`.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 15 total unresolved issues (including 14 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ec34a6f. Configure here.

...restored,
items,
phase: restored.phase === "running" ? "complete" : restored.phase,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stuck merge cards after reload

Medium Severity

The new persist merge recovers interrupted review RPCs (pending/running → error) but leaves mergeStatus of merging (and related in-flight merge lifecycle) untouched. After a reload, mergeQueueActive is gone, per-card Merge only appears for idle/failed, and Merge all skips merging, so those cards can stay on a permanent Merging spinner with no retry path short of Re-run.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ec34a6f. Configure here.

@juliusmarminge

Copy link
Copy Markdown
Member

Closing in favor of #2829 (orchestration V2).

#2829 deletes the V1 orchestration layer this PR builds on — apps/server/src/orchestration/**, provider/Layers/*Adapter.ts and provider/Services/** are removed and replaced by apps/server/src/orchestration-v2/**, with the IPC surface renamed to ORCHESTRATION_V2_WS_METHODS. The files this PR touches either no longer exist or are rewritten, so it can't be rebased — it would need reimplementing against the V2 adapters.

This is not a judgement on the change itself. Several of these are real gaps we still want fixed; the base just moved out from under them.

Once #2829 merges, please rebase onto main, port the change to the V2 equivalent, and reopen (or open a fresh PR). Ping me and I'll prioritise the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants