fix(app): separate session and execution scopes#515
Conversation
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughThis PR implements Stage 2 of session identity and execution scope separation. It introduces ChangesSession & Execution Scope Separation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Code Review
This pull request implements a comprehensive scoping system for sessions and execution environments, introducing ExecutionScope and SessionScope to track server, directory, and epoch changes. Key improvements include blocking slash command submissions until command hydration is complete and ensuring that follow-ups, reverts, and review states are correctly scoped to prevent stale data application during context shifts like worktree transitions. Review feedback suggests removing trimStart() from the command detection logic to maintain consistency with actual submission behavior and updating a mock signature in tests for better alignment with the new API.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/app/src/pages/session/use-session-followups.ts (1)
147-176:⚠️ Potential issue | 🟠 Major | ⚡ Quick winTrack pending sends independently of
useMutation.variables.This busy check is unsafe when multiple sessions have concurrent follow-up sends. TanStack Query v5 allows overlapping
mutateAsync()calls on the same mutation instance to run in parallel, and thevariablesfield only reflects the last call. If a user sends a follow-up in Session A (isPending=true, variables.key="A"), then quickly switches to Session B and sends there (variables.key becomes "B"), the check at line 231 (followupBusy(key)for A) will return false even though A's mutation is still in flight. This allows duplicate sends.Maintain a local pending-by-key set or use
useMutationStatewithmutationKeyto track all in-flight mutations independently of the singlevariablesobject. Alternatively, serialize follow-up sends globally via the mutation'sscopeoption.🤖 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 `@packages/app/src/pages/session/use-session-followups.ts` around lines 147 - 176, The busy check using followupMutation.isPending and followupMutation.variables is unsafe for concurrent sends; update followupBusy to track in-flight sends per session key instead of relying on useMutation.variables by adding a local Set (e.g., pendingFollowups) or using useMutationState/mutationKey to record each mutateAsync start and finish for followupMutation; ensure followupMutation's mutationFn (and callers) add the params.key to pendingFollowups before sending and remove it on both success and error, and change followupBusy(key) to check pendingFollowups.has(key) (or use the per-mutation state) so concurrent sends for different keys are tracked independently from followupMutation.variables.
🧹 Nitpick comments (1)
packages/app/src/context/global-sync/bootstrap.test.ts (1)
372-428: ⚡ Quick winAdd a rejection-path test for command hydration.
This test covers the pending/success path well, but not the failure path where bootstrap should keep
command_ready === falseand clearcommand. Adding that assertion would lock in the error-handling contract frombootstrapDirectory.Proposed test addition
+ test("keeps command readiness false and clears commands when command list fails", async () => { + const directory = "/tmp/project" + const queryClient = new QueryClient() + const [store, setStore] = createStore(createState()) + setStore("command", [{ name: "stale" }] as State["command"]) + const commandError = new Error("command list failed") + + const sdk = { + app: { agents: async () => ({ data: [] }) }, + config: { get: async () => ({ data: {} as Config }) }, + session: { status: async () => ({ data: {} }), get: async () => ({ data: undefined }) }, + project: { current: async () => ({ data: { id: "project-1" } }) }, + path: { get: async () => ({ data: { state: "", config: "", worktree: "", directory, home: "" } as Path }) }, + vcs: { get: async () => ({ data: undefined }) }, + command: { list: async () => { throw commandError } }, + permission: { list: async () => ({ data: [] }) }, + question: { list: async () => ({ data: [] }) }, + blocker: { list: async () => ({ data: [] }) }, + mcp: { status: async () => ({ data: {} }) }, + provider: { list: async () => ({ data: { all: [], connected: [], default: {} } }) }, + } as any + + await bootstrapDirectory({ + directory, + sdk, + store, + setStore, + vcsCache: createVcsCache(), + loadSessions: () => undefined, + translate: (key) => key, + global: { + config: {} as Config, + path: { state: "", config: "", worktree: "", directory: "", home: "" } as Path, + project: [] as Project[], + provider: { all: [], connected: [], default: {} }, + }, + queryClient, + }) + + await waitFor(() => store.command_ready === false) + expect(store.command).toEqual([]) + })🤖 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 `@packages/app/src/context/global-sync/bootstrap.test.ts` around lines 372 - 428, Add a new test in the same suite that simulates the command.list rejecting: reuse the same setup from the "resets command readiness until command list hydrates" test (the deferred command and sdk.command.list) but instead of resolving the deferred call command.reject(new Error("...")); after bootstrapDirectory has started, then assert that store.command_ready remains false and store.command is cleared/empty (matching how bootstrapDirectory should handle errors). Ensure you reference the existing deferred variable named command, the bootstrapDirectory call, and the store properties command_ready and command so the test targets the rejection path and verifies the error-handling contract.
🤖 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 `@packages/app/e2e/session/session-renderer-diagnostics.spec.ts`:
- Around line 311-338: Before sending the follow-up, call readPromptSent() and
store its value, then after sendVisiblePrompt({ page, text: followupText })
re-call readPromptSent() and assert that the relevant counter/sequence field
(the property that indicates progression in the returned object from
readPromptSent()) has advanced and still matches session.id and
project.directory; update the assertions around sent to compare the "before" and
"after" results to ensure the follow-up caused a new send rather than reading
stale metadata from the prior "exit diagnostics worktree" submit, referencing
readPromptSent, sendVisiblePrompt, session.id and project.directory to locate
the checks.
- Around line 158-174: The helper expandRenderedTimeline uses a fixed sleep
(page.waitForTimeout(100)) which makes the test timing-dependent; remove that
sleep and instead wait for observable state after the wheel action by polling
the count of sessionMessageItemSelector until it increases (e.g., capture the
previous count, call page.mouse.wheel(0, -2400), then use expect.poll or a
locator.waitFor pattern to wait until
page.locator(sessionMessageItemSelector).count() is > previousCount or >=
target); update the loop in expandRenderedTimeline (which references
scrollViewportSelector and sessionMessageItemSelector) to rely solely on polling
the message count after the wheel event rather than a fixed timeout.
In `@packages/app/src/pages/session/execution-scope.ts`:
- Around line 3-7: ExecutionScope currently only tracks serverKey, directory and
epoch which allows stale async tasks to appear current if client/store/setter
instances change; add an explicit owner identity field (e.g., ownerId or
executionOwner) to the ExecutionScope type and populate it wherever
ExecutionScope instances are created, then update shouldApplyExecutionResult to
compare this owner field in addition to serverKey/directory/epoch so late work
cannot write into the wrong owner; alternatively, if you prefer not to add a
field, ensure every code path that recreates client/store/setter reliably
increments epoch whenever those references change and update all places that
construct ExecutionScope and call shouldApplyExecutionResult accordingly.
In `@packages/app/src/pages/session/session-layout.test.ts`:
- Around line 7-10: Add a test case that passes dir: undefined to
sessionRouteLayoutKey to ensure the function doesn't produce the string
"undefined"; specifically update the test in session-layout.test.ts to call
sessionRouteLayoutKey({ dir: undefined, id: "ses_1" }) (and optionally { dir:
undefined, id: undefined }) and assert the expected fallback key (e.g., "ses_1"
for the first case) so the layout key behavior is locked down for undefined dir
values.
In `@packages/app/src/pages/session/session-layout.ts`:
- Around line 6-8: sessionRouteLayoutKey is producing the string "undefined"
when params.dir is undefined; change the function to coalesce params.dir to an
empty string (e.g. use params.dir ?? "" or String(params.dir || "")) before
building the key so you never stringify undefined, and keep the existing id
concatenation (use params.id ? "/" + params.id : "") so keys become "" or "/id"
instead of "undefined" or "undefined/id".
---
Outside diff comments:
In `@packages/app/src/pages/session/use-session-followups.ts`:
- Around line 147-176: The busy check using followupMutation.isPending and
followupMutation.variables is unsafe for concurrent sends; update followupBusy
to track in-flight sends per session key instead of relying on
useMutation.variables by adding a local Set (e.g., pendingFollowups) or using
useMutationState/mutationKey to record each mutateAsync start and finish for
followupMutation; ensure followupMutation's mutationFn (and callers) add the
params.key to pendingFollowups before sending and remove it on both success and
error, and change followupBusy(key) to check pendingFollowups.has(key) (or use
the per-mutation state) so concurrent sends for different keys are tracked
independently from followupMutation.variables.
---
Nitpick comments:
In `@packages/app/src/context/global-sync/bootstrap.test.ts`:
- Around line 372-428: Add a new test in the same suite that simulates the
command.list rejecting: reuse the same setup from the "resets command readiness
until command list hydrates" test (the deferred command and sdk.command.list)
but instead of resolving the deferred call command.reject(new Error("..."));
after bootstrapDirectory has started, then assert that store.command_ready
remains false and store.command is cleared/empty (matching how
bootstrapDirectory should handle errors). Ensure you reference the existing
deferred variable named command, the bootstrapDirectory call, and the store
properties command_ready and command so the test targets the rejection path and
verifies the error-handling contract.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: d9f1026e-9daa-4ff7-9302-0e5592e57578
📒 Files selected for processing (34)
packages/app/e2e/session/session-renderer-diagnostics.spec.tspackages/app/src/components/prompt-input/submit.test.tspackages/app/src/components/prompt-input/submit.tspackages/app/src/context/global-sync/bootstrap.test.tspackages/app/src/context/global-sync/bootstrap.tspackages/app/src/context/global-sync/child-store.tspackages/app/src/context/global-sync/event-reducer.test.tspackages/app/src/context/global-sync/types.tspackages/app/src/pages/session.tsxpackages/app/src/pages/session/composer/session-composer-region.tsxpackages/app/src/pages/session/execution-scope.test.tspackages/app/src/pages/session/execution-scope.tspackages/app/src/pages/session/file-tabs.tsxpackages/app/src/pages/session/message-timeline.tsxpackages/app/src/pages/session/prompt-route-scope.test.tspackages/app/src/pages/session/prompt-route-scope.tspackages/app/src/pages/session/session-action-readiness.test.tspackages/app/src/pages/session/session-action-readiness.tspackages/app/src/pages/session/session-layout.test.tspackages/app/src/pages/session/session-layout.tspackages/app/src/pages/session/session-scope.test.tspackages/app/src/pages/session/session-scope.tspackages/app/src/pages/session/session-side-panel.test.tsxpackages/app/src/pages/session/session-side-panel.tsxpackages/app/src/pages/session/session-view-controller.test.tspackages/app/src/pages/session/session-view-controller.tspackages/app/src/pages/session/use-session-followups.test.tspackages/app/src/pages/session/use-session-followups.tspackages/app/src/pages/session/use-session-revert.test.tspackages/app/src/pages/session/use-session-revert.tspackages/app/src/pages/session/use-session-review-state.test.tspackages/app/src/pages/session/use-session-review-state.tspackages/app/src/pages/session/use-session-timeline-data.test.tspackages/app/src/pages/session/use-session-timeline-data.ts
|
CodeRabbit non-inline follow-up handled in 7f04bcd:
Verification run:
|
Summary
SessionScope,ExecutionScope,PromptRouteScope, and submit/follow-up readiness helpers.enter-worktree -> exit-worktree -> follow-uppath with a long rendered timeline.Why
#441 requires session identity and execution directory to be modeled separately. The old code mixed bare
sessionID, route directory, and async result ownership in several places, which could reuse timeline/follow-up/review state across the wrong server or execution context and could collapse long timelines after worktree execution changes.Related Issue
Closes #441.
Human Review Status
Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.
Review Focus
packages/app/src/pages/session/use-session-timeline-data.ts: last-good timeline cache now follows durableSessionScope.packages/app/src/pages/session/use-session-followups.ts: v2 scoped queue plus scoped failed/paused/edit/busy/mutation state.packages/app/src/components/prompt-input/submit.ts: command hydration gates slash text without blocking shell/bin/..., and rollback uses captured prompt route scope.packages/app/src/pages/session/use-session-review-state.tsanduse-session-revert.ts: stale async results are guarded byExecutionScope.packages/app/e2e/session/session-renderer-diagnostics.spec.ts: long timeline regression should prove no remount, no rendered-count reset, no post-follow-up top jump, and correct post-exit send directory.Risk Notes
session-followup.v2; old v1 unscoped queued drafts are intentionally not auto-migrated.How To Verify
Long-timeline E2E evidence:
Screenshots or Recordings
Not included. This PR changes session state ownership and scroll behavior; the visible regression is covered by diagnostics-backed E2E rather than a static screenshot.
Checklist
dev, and my PR title and commit messages use Conventional Commits in EnglishSummary by CodeRabbit
New Features
Improvements