Skip to content

Canonicalize workspace roots for session visibility#158

Open
YuMS wants to merge 6 commits into
friuns2:mainfrom
YuMS:feature/canonicalize-workspace-roots
Open

Canonicalize workspace roots for session visibility#158
YuMS wants to merge 6 commits into
friuns2:mainfrom
YuMS:feature/canonicalize-workspace-roots

Conversation

@YuMS
Copy link
Copy Markdown

@YuMS YuMS commented May 11, 2026

Summary

canonicalize saved workspace roots with local realpath
canonicalize thread/list cwd values before sidebar filtering
keep sessions visible when symlink and target paths are mixed
add regression tests and manual test coverage

Verification

npx vitest run src/server/codexAppServerBridge.archive.test.ts
npx tsc -p tsconfig.server.json --noEmit
npm run build:frontend
git diff --check
PROFILE_BASE_URL=http://127.0.0.1:4173 PROFILE_WAIT_MS=7000 node scripts/profile-browser-runtime.cjs

Summary by CodeRabbit

  • Bug Fixes

    • Normalize symlinked workspace root paths so sessions, project filters, labels, and thread CWDs remain consistent and readable.
  • Tests

    • Added tests covering workspace-root canonicalization, thread-list CWD normalization, and reuse of realpath results within responses.
  • Documentation

    • Added a manual regression procedure for verifying symlinked workspace root behavior.

Review Change Stack

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Canonicalize workspace roots for consistent session visibility

✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Canonicalize workspace roots and thread cwd values through realpath
• Ensures sessions remain visible with symlinked workspace paths
• Exports canonicalization functions for workspace roots state
• Adds comprehensive test coverage and manual regression tests
Diagram
flowchart LR
  A["Thread/List Response"] -->|canonicalizeThreadListResponseForRead| B["Canonicalized CWD Values"]
  C["Workspace Roots State"] -->|canonicalizeWorkspaceRootsStateForRead| D["Canonicalized Paths"]
  B --> E["Sessions Visible Regardless of Symlink"]
  D --> E
Loading

Grey Divider

File Changes

1. src/server/codexAppServerBridge.ts ✨ Enhancement +76/-5

Add path canonicalization for workspace roots and threads

• Added realpath import from node:fs/promises for path canonicalization
• Exported WorkspaceRootsState type for external use
• Modified callRpcWithArchiveRecovery to canonicalize thread/list responses
• Implemented canonicalizeWorkspaceRootsStateForRead function to realpath workspace roots, labels,
 and project orders
• Implemented canonicalizeThreadCwdRecord and canonicalizeThreadListResponseForRead to
 canonicalize thread cwd values
• Updated readWorkspaceRootsState to apply canonicalization when reading persisted state

src/server/codexAppServerBridge.ts


2. src/server/codexAppServerBridge.archive.test.ts 🧪 Tests +58/-1

Add canonicalization function tests

• Added imports for canonicalizeThreadListResponseForRead and
 canonicalizeWorkspaceRootsStateForRead
• Added test suite for canonicalizeWorkspaceRootsStateForRead verifying symlink paths are
 realpathd
• Added test suite for canonicalizeThreadListResponseForRead verifying thread cwd values are
 canonicalized
• Tests verify that mixed symlink and canonical paths are normalized consistently

src/server/codexAppServerBridge.archive.test.ts


3. tests.md 📝 Documentation +30/-0

Add manual regression test for symlink workspace roots

• Added manual regression test section "Sidebar sessions survive symlinked workspace roots"
• Documents prerequisites, test steps, and expected results for symlink path handling
• Verifies sessions are visible regardless of whether recorded through symlink or canonical path
• Includes verification of API response returning canonical real paths
• Tests both light and dark theme rendering

tests.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 11, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Label overwrite race ✓ Resolved 🐞 Bug ≡ Correctness
Description
canonicalizeWorkspaceRootsStateForRead concurrently assigns canonicalized label keys into a shared
object; if multiple original keys (e.g., symlink + target) realpath to the same canonical key, the
winning label becomes dependent on async completion order and can flip between reads. This is
user-visible because the sidebar hydrates display names from rootsState.labels.
Code

src/server/codexAppServerBridge.ts[R3905-3909]

+  const labels: Record<string, string> = { ...state.labels }
+  await Promise.all(Object.entries(state.labels).map(async ([key, label]) => {
+    const canonicalKey = await canonicalizeWorkspaceRootPath(key, pathRealpath)
+    labels[canonicalKey] = label
+  }))
Evidence
The server currently copies labels then concurrently assigns canonical keys, which can collide when
both canonical and symlink forms exist; the state writer can introduce such mixed keys, and the UI
consumes labels to set visible project display names.

src/server/codexAppServerBridge.ts[3896-3909]
src/server/codexAppServerBridge.ts[4006-4022]
src/composables/useDesktopState.ts[3853-3865]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`canonicalizeWorkspaceRootsStateForRead` builds `labels` by copying `state.labels` and then `Promise.all`-writing `labels[canonicalKey] = label` concurrently. If two different keys canonicalize to the same `canonicalKey` (common when both symlink and target were saved), the final label is nondeterministic (depends on which `realpath` resolves last).
## Issue Context
This function is used on every `/codex-api/workspace-roots-state` read, and the UI iterates `rootsState.labels` to hydrate project display names.
## Fix Focus Areas
- src/server/codexAppServerBridge.ts[3896-3917]
## Suggested fix approach
- Build a **new** `labels` map deterministically instead of mutating a shared object from concurrent tasks.
- Make collision policy explicit, e.g.:
- Prefer an entry whose original key is already canonical (`key === canonicalKey`) over a symlink-derived key.
- Or process entries sequentially in stable key order.
- Consider dropping non-canonical keys from the returned `labels` to avoid returning both forms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. No realpath memoization ✓ Resolved 🐞 Bug ➹ Performance
Description
canonicalizeThreadListResponseForRead calls realpath once per thread item (per absolute cwd) without
memoizing identical cwd strings, so a thread/list page with many sessions from the same project
triggers redundant filesystem syscalls. This runs on every thread/list RPC and thread/list is
paginated (50/100 items per page) and repeatedly requested during background loading.
Code

src/server/codexAppServerBridge.ts[R3935-3940]

+  const record = asRecord(payload)
+  if (!record || !Array.isArray(record.data)) return payload
+  return {
+    ...record,
+    data: await Promise.all(record.data.map((item) => canonicalizeThreadCwdRecord(item, pathRealpath))),
+  }
Evidence
The server now canonicalizes thread/list results and does a per-item async mapping; thread/list is
called with up to 100 items per page and paginated in the UI, so redundant per-item realpath calls
can accumulate across pages.

src/server/codexAppServerBridge.ts[1225-1234]
src/server/codexAppServerBridge.ts[3920-3940]
src/api/codexGateway.ts[688-704]
src/composables/useDesktopState.ts[4080-4091]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`canonicalizeThreadListResponseForRead` maps over every `record.data` entry and calls `canonicalizeWorkspaceRootPath` (which calls `realpath`) per item. If multiple items share the same `cwd` string, we perform redundant `realpath` calls.
## Issue Context
The server applies this transformation for every `thread/list` RPC. The client calls `thread/list` with limits up to 100 and performs background pagination.
## Fix Focus Areas
- src/server/codexAppServerBridge.ts[3920-3941]
- src/server/codexAppServerBridge.ts[1225-1234]
## Suggested fix approach
- Add a per-invocation cache in `canonicalizeThreadListResponseForRead`, e.g. `Map<string, Promise<string>>`, keyed by the original `cwd` string.
- Resolve each unique `cwd` at most once, then reuse the cached promise for subsequent items.
- Keep the existing behavior of skipping non-absolute values and swallowing `realpath` failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/server/codexAppServerBridge.ts Outdated
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c418923-9dc6-4cb9-8499-fa30555cf724

📥 Commits

Reviewing files that changed from the base of the PR and between 5a33063 and 4895d26.

📒 Files selected for processing (1)
  • src/server/codexAppServerBridge.archive.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/codexAppServerBridge.archive.test.ts

📝 Walkthrough

Walkthrough

This PR resolves symlinked workspace-root paths and thread cwd values to canonical real paths via an async realpath resolver, applies the canonicalization to thread/list RPC results and the workspace-roots-state API, and adds tests plus a manual test plan.

Changes

Symlink canonicalization for workspace roots

Layer / File(s) Summary
Type exports and symlink resolver imports
src/server/codexAppServerBridge.ts
Export WorkspaceRootsState and import realpath from Node.js fs/promises to support path canonicalization.
Workspace root canonicalization implementation
src/server/codexAppServerBridge.ts
Add PathRealpathResolver and canonicalizeWorkspaceRootsStateForRead to resolve and rewrite workspace-root path lists, active/project order, and label keys using the resolver.
RPC response canonicalization and workspace roots endpoint
src/server/codexAppServerBridge.ts
Have callRpcWithArchiveRecovery post-process thread/list via canonicalizeThreadListResponseForRead, and return canonicalized results from readWorkspaceRootsState.
Canonicalization helper tests
src/server/codexAppServerBridge.archive.test.ts
Import and test canonicalizeWorkspaceRootsStateForRead and canonicalizeThreadListResponseForRead, verifying symlink→realpath rewriting and reuse of resolver results within a single response.
Manual regression test documentation
tests.md
Add manual test plan "Sidebar sessions survive symlinked workspace roots" covering verification steps, expected behavior, and rollback guidance.

Sequence Diagram

sequenceDiagram
  participant Client
  participant callRpcWithArchiveRecovery
  participant canonicalizeThreadListResponseForRead
  participant readWorkspaceRootsState
  participant canonicalizeWorkspaceRootsStateForRead
  participant realpath

  Client->>callRpcWithArchiveRecovery: thread/list RPC call
  callRpcWithArchiveRecovery->>canonicalizeThreadListResponseForRead: raw response with symlinked cwd
  canonicalizeThreadListResponseForRead->>realpath: resolve cwd paths
  realpath-->>canonicalizeThreadListResponseForRead: canonical paths
  canonicalizeThreadListResponseForRead-->>callRpcWithArchiveRecovery: canonicalized thread list
  callRpcWithArchiveRecovery-->>Client: thread list with real paths

  Client->>readWorkspaceRootsState: /codex-api/workspace-roots-state request
  readWorkspaceRootsState->>canonicalizeWorkspaceRootsStateForRead: persisted workspace roots state
  canonicalizeWorkspaceRootsStateForRead->>realpath: resolve order, active, label paths
  realpath-->>canonicalizeWorkspaceRootsStateForRead: canonical paths
  canonicalizeWorkspaceRootsStateForRead-->>readWorkspaceRootsState: canonicalized state
  readWorkspaceRootsState-->>Client: workspace roots with real paths
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hop through links and tangled trails,
Realpath guides where each path prevails.
Roots now match, no doubles to fight,
Threads find home in canonical light.
Hooray — sidebar sessions sleep tight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: canonicalizing workspace roots to improve session visibility when symlinks and target paths are mixed.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@src/server/codexAppServerBridge.archive.test.ts`:
- Around line 135-183: The test file is missing the closing braces for the inner
"it('reuses cwd realpath results within one thread list response'...)" test and
the outer describe("canonicalizeThreadListResponseForRead", ...) block; add the
missing "})" to close the failing it block and then add another "})" to close
the describe block so the next describe can start cleanly. Locate the describe
block named "canonicalizeThreadListResponseForRead" and the two it blocks inside
it and append the necessary closing braces in order to properly terminate the it
and then the describe.

In `@src/server/codexAppServerBridge.ts`:
- Around line 4385-4391: The state is being canonicalized only on read but not
before persisting, so writeWorkspaceRootsState() receives raw symlink paths
(nextState) and the file can contain mixed entries; update
persistWorkspaceRoot() and the handler that serves PUT
/codex-api/workspace-roots-state to canonicalize the outgoing state before
calling writeWorkspaceRootsState()—e.g., call
canonicalizeWorkspaceRootsStateForRead(nextState) (or extract a small
canonicalizeForWrite helper) and pass its result into writeWorkspaceRootsState()
instead of nextState so the on-disk file stores canonical paths and avoids
reintroducing duplicates.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f1813ad-67c6-4045-92ea-aeeb994d4ab0

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9dacd and 5a33063.

📒 Files selected for processing (3)
  • src/server/codexAppServerBridge.archive.test.ts
  • src/server/codexAppServerBridge.ts
  • tests.md

Comment thread src/server/codexAppServerBridge.archive.test.ts
Comment on lines +4385 to +4391
return await canonicalizeWorkspaceRootsStateForRead({
order: normalizeStringArray(payload['electron-saved-workspace-roots']),
labels: normalizeStringRecord(payload['electron-workspace-root-labels']),
active: normalizeStringArray(payload['active-workspace-roots']),
projectOrder: normalizeStringArray(payload['project-order']),
remoteProjects: normalizeRemoteProjects(payload['remote-projects']),
}
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Canonicalize before persisting, not only when reading.

This normalizes the API response, but new roots written through persistWorkspaceRoot() and PUT /codex-api/workspace-roots-state still hit disk as raw symlink paths because writeWorkspaceRootsState() stores nextState unchanged. That leaves the state file with mixed canonical/non-canonical entries and can reintroduce duplicates for any code path that reads the file directly.

Suggested direction
async function writeWorkspaceRootsState(nextState: WorkspaceRootsState): Promise<void> {
+  const canonicalState = await canonicalizeWorkspaceRootsStateForRead(nextState)
   const statePath = getCodexGlobalStatePath()
   let payload: Record<string, unknown> = {}
   try {
     const raw = await readFile(statePath, 'utf8')
     payload = asRecord(JSON.parse(raw)) ?? {}
   } catch {
     payload = {}
   }

-  payload['electron-saved-workspace-roots'] = normalizeStringArray(nextState.order)
-  payload['electron-workspace-root-labels'] = normalizeStringRecord(nextState.labels)
-  payload['active-workspace-roots'] = normalizeStringArray(nextState.active)
-  payload['project-order'] = normalizeStringArray(nextState.projectOrder)
+  payload['electron-saved-workspace-roots'] = normalizeStringArray(canonicalState.order)
+  payload['electron-workspace-root-labels'] = normalizeStringRecord(canonicalState.labels)
+  payload['active-workspace-roots'] = normalizeStringArray(canonicalState.active)
+  payload['project-order'] = normalizeStringArray(canonicalState.projectOrder)

   await writeFile(statePath, JSON.stringify(payload), 'utf8')
}
🤖 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 `@src/server/codexAppServerBridge.ts` around lines 4385 - 4391, The state is
being canonicalized only on read but not before persisting, so
writeWorkspaceRootsState() receives raw symlink paths (nextState) and the file
can contain mixed entries; update persistWorkspaceRoot() and the handler that
serves PUT /codex-api/workspace-roots-state to canonicalize the outgoing state
before calling writeWorkspaceRootsState()—e.g., call
canonicalizeWorkspaceRootsStateForRead(nextState) (or extract a small
canonicalizeForWrite helper) and pass its result into writeWorkspaceRootsState()
instead of nextState so the on-disk file stores canonical paths and avoids
reintroducing duplicates.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@YuMS
Copy link
Copy Markdown
Author

YuMS commented May 13, 2026

@friuns2 Could you help take a look at this PR?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant