Skip to content

fix(session): honor Cursor CLI's two recorded tool roots - #1165

Merged
jeff-r2026 merged 1 commit into
Tencent:mainfrom
Smilewithoutfalling:fix/cursor-tool-roots
Oct 16, 2026
Merged

jeff-r2026 merged 1 commit into
Tencent:mainfrom
Smilewithoutfalling:fix/cursor-tool-roots

Conversation

@Smilewithoutfalling

@Smilewithoutfalling Smilewithoutfalling commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

teamai init records a relocated tool root so that every later run targets the directory the tool actually reads. Cursor CLI was not in RELOCATABLE_TOOLS, so a member with CURSOR_DATA_DIR / CURSOR_CONFIG_DIR set got teamai listing ~/.cursor/projects and writing the co-author setting to ~/.cursor/cli-config.json.

Cursor CLI moves two directories, and neither of them is the root its toolPaths entry hangs off.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

What Cursor CLI reads

Read off the cursor-agent 2026.09.28 bundle, the two directories the issue asks about:

Root Variable Fallback Holds
config CURSOR_CONFIG_DIR $XDG_CONFIG_HOME/cursor, then ~/.cursor cli-config.json, permissions.json, chats/, acp-sessions/
data CURSOR_DATA_DIR (also --data-dir) ~/.cursor projects/ — the transcripts

The user mcp.json, rules/ and commands/ stay in ~/.cursor whatever the variables say.

Why the two roots are not recorded as cursor

applyToolRoots re-roots the toolPaths entry of every key it finds in toolRoots, so a key named cursor would move toolPaths.cursor — the user mcp.json, rules/ and commands/ that have to stay put. The roots are therefore recorded as cursor-data and cursor-config: RELOCATABLE_TOOLS holds roots, not tool ids, and its doc block now says so. src/__tests__/cursor-tool-roots.test.ts pins that scopedToolPaths leaves cursor where it is.

What follows the record now

  • Transcripts. getCursorProjectsDir(toolRoots) resolves <data root>/projects. The cursor entry of ADAPTER_REGISTRY forwards toolRoots, and CursorAdapter takes the resolved store in its constructor instead of closing over the default — the shape #1135 gave ClaudeCodeAdapter and CodexAdapter.
  • cli-config.json. New cursorConfigDir() in types.ts; coauthor-reconcile.ts writes there instead of a bare path.join(userHome, '.cursor', ...). This is the one place toolRoots cannot express the answer — XDG_CONFIG_HOME names a parent directory, and ~/.config itself is refused as a root — so the fallback is read where the write happens.
  • The IDE half (item 3). Neither CLI variable applies to the Cursor IDE: it resolves its user-data dir the way VS Code does (VSCODE_PORTABLE → VSCODE_APPDATA → --user-data-dir, and on Linux $XDG_CONFIG_HOME or ~/.config, joined with Cursor). session-flow/cursor-store.ts was the one place that hard-coded ~/.config/Cursor; it now honors XDG_CONFIG_HOME, as ide-history.ts already does for the same IDE.

Test Plan

  • npx tsc --noEmit passes
  • npm run lint passes — 0 warnings, 0 errors
  • Added/updated tests for the change
  • npx vitest run — I could not run the full suite here; see "Regression check" for what I ran instead

New and updated tests

  • src/__tests__/cursor-tool-roots.test.ts (new, 11 cases) — the recorded keys, the transcript store (list, read and write under the recorded root, ~/.cursor left alone), the config root through the real reconcileCoAuthorForConfig, and the IDE store root (including a blank XDG_CONFIG_HOME). 53 passed together with the updated tool-roots.test.ts. Its cleanup hook declares 60_000: removing the fixture is I/O bound, and this box's temp volume is slow enough to miss the 15s default — the repo gives 84 other hooks an explicit budget for the same reason.
  • src/__tests__/e2e/cursor-tool-roots-1138.test.ts (new, 3 cases) — the real CLI end to end. 3 passed (below).
  • src/__tests__/tool-roots.test.ts — the refusal message now names the four supported keys.
  • src/__tests__/coauthor-reconcile.test.ts, src/__tests__/cursor-user-images.test.ts, src/__tests__/agent-session-copy.test.ts — pin XDG_CONFIG_HOME so the fixtures and the code agree whatever the runner exports. No assertion changed; see "Fixes after the first CI run".

Regression check. I ran the same selection on the change and on the unmodified tree (same tarball), because on this Windows box a failure cannot be attributed without that:

$ npx vitest run --pool=threads src/__tests__/agent-session-platform.test.ts \
    src/__tests__/agent-session-list.test.ts src/__tests__/agent-session-tool-roots.test.ts \
    src/__tests__/session-cmd.test.ts src/__tests__/doctor.test.ts \
    src/__tests__/skip-uninstalled-tools.test.ts src/__tests__/usage-tracking.test.ts

  with the change:      22 failed | 311 passed | 1 skipped (334)
  unmodified:           22 failed | 311 passed | 1 skipped (334)
  $ diff <FAIL list A> <FAIL list B>   ->  no output

Every one of the 22 fails on the unmodified tree too: POSIX file-mode and symlink assertions, a skill name containing :, reg.exe blocked by this machine's sandbox, the OpenCode CLI not installed here, and a Codex shim that needs a login shell.

Real-CLI verification. Cold $HOME, one business repo with an https origin, and a transcript planted under ~/.cursor/projects. The only variable across the six steps is the recorded toolRoots:

### 1) session platforms — nothing recorded yet
  cursor: ✓ installed

### 2) CURSOR_DATA_DIR=$HOME/cursordata CURSOR_CONFIG_DIR=$HOME/cursorconfig teamai init .
ℹ Recorded CURSOR_DATA_DIR as the Cursor transcripts root: <HOME>/cursordata
ℹ Recorded CURSOR_CONFIG_DIR as the Cursor config root: <HOME>/cursorconfig
    config.yaml:
      cursor-data: <HOME>/cursordata
      cursor-config: <HOME>/cursorconfig

### 3) session platforms — neither variable is set in this run
  cursor: ✗ not installed        <- the store moved; ~/.cursor/projects is ignored

### 4) after planting a transcript under the recorded root
  cursor: ✓ installed

### 5) CURSOR_DATA_DIR= CURSOR_CONFIG_DIR= teamai init . --force
ℹ Cleared the recorded Cursor transcripts root (CURSOR_DATA_DIR is blank); ...
ℹ Cleared the recorded Cursor config root (CURSOR_CONFIG_DIR is blank); ...
    config.yaml: no toolRoots key

### 6) session platforms — back to ~/.cursor/projects
  cursor: ✓ installed

The E2E file, run the way AGENTS.md says to:

$ npm run test:e2e -- src/__tests__/e2e/cursor-tool-roots-1138.test.ts
ESM ⚡️ Build success in 1067ms
 ✓ records both roots under cursor-data and cursor-config, never under cursor   54121ms
 ✓ resolves the transcript store from the record, not from the variable or ~/.cursor  56930ms
 ✓ clears the record when the variables go blank, and ~/.cursor comes back      69880ms
 Test Files  1 passed (1)
      Tests  3 passed (3)
     Errors  1 error     <- the known [vitest-worker] onTaskUpdate timeout; exit code 1

The three cases declare 120_000, following the ~237 cases in this suite that do. That is not decoration: on this machine a fixture that runs teamai init costs ~30s per call, so the two-init case lands at ~62-70s and misses the 60s default. The same machine, same default budget, on the unmodified src/__tests__/e2e/init-self-agent-filter.test.ts: 2 of its 6 cases fail on that timeout (re-running init… 62.1s, any other command still self-heals… 64.2s) and pass once raised. So the timeout is the machine, not the change.

Fixes after the first CI run

The first push came back Lint & Test failed on ubuntu-latest, passed on macos-latest (all three Node versions each way). Two causes, both in this change:

  1. Three existing test files inherited XDG_CONFIG_HOME. coauthor-reconcile.test.ts, cursor-user-images.test.ts and agent-session-copy.test.ts build their expectations under ~/.cursor / ~/.config/Cursor but never pin the variable — unlike the rest of the suite, where agent-session-list.test.ts:79 pins it and every E2E file does. With the new code reading that variable, they resolved a different directory than the fixture wrote and read back undefined. Each now pins it the way the rest of the repo does. No assertion changed; the variable-set path is covered by the new file instead.

    Evidence: on this Windows box, exporting XDG_CONFIG_HOME and re-running reproduces the ubuntu set — coauthor-reconcile.test.ts 3 failed and cursor-user-images.test.ts 1 failed, with the same case names and messages as the annotations. Unset, both pass. The four agent-session-copy.test.ts cases are skipIf(process.platform === 'win32'), so they cannot appear here at all; the annotations are the evidence for those. macOS never sets the variable — that is why it stayed green.

  2. A blank XDG_CONFIG_HOME was read as set. getCursorStateRoot() used ??, which keeps '', so path.join('', 'Cursor') returned the relative path Cursor — the working directory. It is ?.trim() || now, matching switch.ts and the $XDG_DATA_HOME read the OpenCode adapter already does (opencode.ts:307) — both treat a blank variable as unset — and cursor-tool-roots.test.ts pins the blank case. (ide-history.ts:139 has the same ?? shape for a different IDE; out of scope here, and I left it alone rather than widen the diff.)

Cause 1 is what CI showed; cause 2 is the P2 finding the bot review filed on the PR.

The E2E (fork-safe, no credentials) check is red on every fork PR here. It fails on #1062 (damenjs) exactly as it does on this one — a fork PR runs that variant and skips the other. Its annotations on this PR are workflow-subworkflow / workflow-inline (record: 'pending'), which is workflow code this change does not touch; #1161 and #1154 have since changed that code on main, so re-running may clear it.

Related Issues

Addresses items 1 and 3 of #1138. Item 2 is #1142's and item 4 is untouched.

Notes for Reviewers

  • Version checked, as the issue asks: cursor-agent 2026.09.28 for the two variables and their fallbacks, Cursor 3.16.29 for the IDE. If a newer build resolves differently, only two places move: RELOCATABLE_TOOLS (cursor-data, cursor-config) and cursorConfigDir().
  • doctor does not cover the two new keys. buildToolRootChecks compares where teamai would write a tool's toolPaths against where that tool reads them; these two roots have no toolPaths entry — that is the point — so the check skips them, and a stale record of either root goes unreported. Covering it needs a check that is not toolPaths-shaped; I left it out rather than widen this PR, and because that comment block sits inside #1119's insertion point.
  • releasePreviousToolRoots skips them too, since isAgentExcluded reads a cursor-data key as an agent that is not enabled. Nothing is lost: neither root holds hooks or the managed MCP files, which is what that pass cleans up.
  • Rebased onto c57da5af. #1148 (src/types.ts, +2 lines) and #1062 (src/init.ts, new import and push path) merged while this was open. The patch applied to both with a 2-line offset only, and tsc / lint pass on the rebased tree. Remaining open overlap is src/types.ts (feat(antigravity): add support for Antigravity CLI (agy) #1040, fix(doctor): report the default paths a custom toolPaths entry drops #1119, feat(dashboard): skills inventory, detail, and workspace exclusions on the Resources page (#1123) #1143). #1142 ("honor Muse XDG data home", by @SIBTAIN-ASAD) touches only dashboard-collector.ts and its tests — a different XDG consumer, no overlap. #1157 (Cursor skill-usage hook matcher) touches hooks, not the session store.

@jeff-r2026 jeff-r2026 self-assigned this Oct 16, 2026
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Handle a blank XDG_CONFIG_HOME as unset — src/session-flow/cursor-store.ts:40. On Linux, XDG_CONFIG_HOME='' makes getCursorStateRoot() return the relative path Cursor instead of ~/.config/Cursor, so IDE session operations target the working directory rather than the actual store. Use a non-empty-value fallback instead of ??.

The PR description documents sufficient testing: a representative real-CLI verification and an E2E build/run record. The disclosed worker timeout does not constitute a missing verification record.

Review was read-only; no PR code was run, built, or installed.

`teamai init` records a relocated tool root so every later run targets the
directory that tool reads. Cursor CLI was not in `RELOCATABLE_TOOLS`, so a
member with `CURSOR_DATA_DIR` / `CURSOR_CONFIG_DIR` set had teamai listing
`~/.cursor/projects` and writing the co-author setting to
`~/.cursor/cli-config.json`.

Cursor moves two directories and neither is the root its `toolPaths` entry
hangs off — the user `mcp.json`, `rules/` and `commands/` stay in `~/.cursor`
whatever the variables say. An `applyToolRoots` key re-roots the `toolPaths`
entry of the same name, so a key named `cursor` would move a layout that has
to stay put. The two roots are therefore recorded as `cursor-data` and
`cursor-config`: the table holds roots, not tool ids.

`getCursorProjectsDir` follows the recorded data root and the `cursor`
adapter takes its store in the constructor, as the Claude Code and Codex
adapters already do. `cursorConfigDir` adds the config root TeamAI had no
way to express, `$XDG_CONFIG_HOME/cursor` included. On the IDE side,
`cursor-store` stops hard-coding `~/.config/Cursor`.

A blank `XDG_CONFIG_HOME` counts as unset (`?.trim() ||`, matching
`switch.ts` and `opencode.ts`), and the three test files that build fixtures
under `~/.cursor` / `~/.config/Cursor` now pin the variable the way the rest
of the suite does.

Fixes the CLI and IDE halves of Tencent#1138.
@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Remove unrelated init push changes — src/init.ts:1443 and src/init.ts:2072. This diff also adds push timeouts and switches reviewer configuration from direct push to MR creation. Neither relates to Cursor roots; both violate the review rule requiring every changed line to trace directly to the PR’s stated goal. Split them into a separate PR.
  • [P2 non-blocking] Remove unrelated workflow configuration — src/types.ts:864. Adding workflows.max_parallel is unrelated to Cursor root resolution and violates the same surgical-change rule. Keep this schema change in its workflow PR.

The earlier blank-XDG_CONFIG_HOME finding is resolved: the current diff uses a non-empty fallback and adds a regression test.

The description documents sufficient real-CLI/E2E verification. The disclosed worker timeout does not constitute a missing verification record.

Review was read-only; no PR code was run, built, or installed.

@github-actions

Copy link
Copy Markdown

No new findings in the specified diff.

Earlier findings are resolved:

  • Blank XDG_CONFIG_HOME now falls back correctly, with a regression test.
  • Unrelated init push changes and workflows.max_parallel are absent from this diff.

The PR description documents sufficient testing: representative real-CLI verification and an E2E build/run record. The disclosed worker timeout does not constitute a missing verification record.

Review was read-only; no PR code was run, built, or installed.

@jeff-r2026
jeff-r2026 merged commit 4f2729f into Tencent:main Oct 16, 2026
14 checks passed
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.

2 participants