Skip to content
Source
Sign inSign up

[Bug]: Cursor Origin reports no personal namespace when it is on a later page #35905

Send to my agent
Opencoygeek(GitHub profile, opens in a new tab) opened this issue 0 comments

Summary

Cursor Origin can tell a signed-in user that their personal namespace does not exist even though Origin returns it on a later page. Personal-namespace discovery stops after the first page and discards the continuation token. An isolated reproduction through the actual authentication and response-decoding layer confirms the false missing-namespace error; discovery should continue until it finds the personal namespace or exhausts the list.

Before submitting

  • Related issues: no duplicate found in open and closed issues or pull requests for Cursor Origin namespace pagination. The related auth-status fix changes command fields and nullable token validity, not namespace pagination.
  • Removed credentials, tokens, private paths, and sensitive data.

Area

apps/server

Observed behavior

With 30 team namespaces on the first response and nextPageToken: fixture-next, OriginCli.personalNamespace returns an OriginCliError with reason not-found after one request to /v1/origin/namespaces. The second fixture page contains the personal namespace zz-personal, but no request retrieves it. The error detail says: This Cursor account has no Origin namespace. Claim one at cursor.com/codebase. When the same personal namespace is on the first page, the control succeeds.

Reproduction

  1. Check out the commit in Version.
  2. Save the script below as repro.sh and run sh repro.sh /path/to/checkout.
  3. Observe the first-page control succeed and the paginated case fail with not-found after only one HTTP request.

The recorded run reproduces the failure deterministically. Prerequisites are Node.js with TypeScript stripping, npm and registry access for the pinned Effect packages. The script loads unmodified OriginCli.layer, substitutes only its Node HTTP transport with in-memory responses and the process host with a synthetic token result, and decodes responses through the real production path. It isolates HOME/XDG/T3 state, creates dependencies and fixtures under mktemp -d, removes scratch on exit, and leaves the checkout unchanged. It executes no actual Origin CLI and sends no forge requests. Exit 0 means the personal namespace is found on the second page, exit 1 means the false missing-namespace error is reproduced, and exit 2 means setup or the first-page control is inconclusive.

Reproduction script: repro.sh
#!/bin/sh
set -u
repo=$(cd "$1" && pwd) || { echo 'RESULT: inconclusive'; exit 2; }
scratch=$(mktemp -d) || { echo 'RESULT: inconclusive'; exit 2; }
trap 'rm -rf "$scratch"' EXIT HUP INT TERM
export HOME="$scratch/home" TMPDIR="$scratch/tmp"
export XDG_CONFIG_HOME="$scratch/config" XDG_DATA_HOME="$scratch/data" XDG_STATE_HOME="$scratch/state" T3CODE_HOME="$scratch/t3"
export GIT_CONFIG_NOSYSTEM=1 GIT_CONFIG_GLOBAL=/dev/null
mkdir -p "$HOME" "$TMPDIR" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$T3CODE_HOME"
node --version
npm --version
if ! npm install --prefix "$scratch" --ignore-scripts --no-audit --no-fund --no-package-lock --save-exact effect@4.1.3 @effect/platform-node@4.1.2 >"$scratch/install.log" 2>&1; then
  cat "$scratch/install.log"
  echo 'RESULT: inconclusive'
  exit 2
fi
node -e 'for (const n of ["effect", "@effect/platform-node"]) console.log(n + " " + require(process.argv[1] + "/node_modules/" + n + "/package.json").version)' "$scratch"
cat >"$scratch/transport.mjs" <<'JS'
import * as Effect from 'effect/Effect';
import * as Layer from 'effect/Layer';
import {HttpClient,HttpClientResponse} from 'effect/http';
export const layerNodeHttpOptions=()=>Layer.succeed(HttpClient.HttpClient,HttpClient.make(request=>Effect.sync(()=>HttpClientResponse.fromWeb(request,globalThis.originFixtureResponse(request)))));
JS
cat >"$scratch/probe.mjs" <<'JS'
import { registerHooks, createRequire } from 'node:module';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = resolve(process.argv[2]);
const runtime = import.meta.url;
registerHooks({ resolve(specifier, context, nextResolve) {
  if (specifier === '@effect/platform-node/NodeHttpClient') return nextResolve(new URL('./transport.mjs', runtime).href, context);
  if (specifier.startsWith('@t3tools/')) {
    const [name, ...rest] = specifier.slice('@t3tools/'.length).split('/');
    const folder = `${repo}/packages/${name}`;
    const manifest = JSON.parse(readFileSync(`${folder}/package.json`, 'utf8'));
    const key = rest.length ? `./${rest.join('/')}` : '.';
    const target = manifest.exports[key];
    return nextResolve(pathToFileURL(resolve(folder, typeof target === 'string' ? target : target.import)).href, context);
  }
  if (specifier === 'effect' || specifier.startsWith('effect/') || specifier.startsWith('@effect/')) return nextResolve(specifier, {...context, parentURL: runtime});
  return nextResolve(specifier, context);
}});
const Effect = await import('effect/Effect');
const Schema = await import('effect/Schema');
const { OriginCli, layer } = await import(pathToFileURL(`${repo}/packages/source-control-cursor-origin/src/server/OriginCli.ts`).href);
const { SourceControlHost } = await import(pathToFileURL(`${repo}/packages/source-control-core/src/server/SourceControlHost.ts`).href);
const unused=()=>Effect.die(new Error('unexpected host call'));
const host=SourceControlHost.of({
 settings:{get:Effect.die(new Error('unexpected settings read')),getWithSecrets:unused,update:unused},
 process:{run:input=>input.command==='origin'&&input.args.join(' ')==='auth token'
  ? Effect.succeed({exitCode:0,stdout:'fixture-session',stderr:'',stdoutTruncated:false,stderrTruncated:false})
  : unused()},
 git:Object.fromEntries(['execute','resolveCommit','remotes','readConfigValue','resolvePrimaryRemoteName','ensureRemote','listLocalBranchNames','fetchRemoteBranch','fetchRemoteTrackingBranch','setBranchUpstream','createBranch','writeConfigValue','switchRef'].map(x=>[x,unused])),
});
let requests=[], paged=false;
const personal={namespace:{slug:'zz-personal',type:'user'}};
globalThis.originFixtureResponse=request=>{
 const url=new URL(request.url); requests.push(url.pathname+url.search);
 if(url.pathname!=='/v1/origin/namespaces') throw new Error('unexpected HTTP path');
 if(!paged) return Response.json({namespaces:[personal]});
 if(url.searchParams.get('pageToken')==='fixture-next') return Response.json({namespaces:[personal]});
 return Response.json({namespaces:Array.from({length:30},(_,i)=>({namespace:{slug:`team-${String(i).padStart(2,'0')}`,type:'team'}})),nextPageToken:'fixture-next'});
};
const run=()=>Effect.runPromise(Effect.scoped(Effect.gen(function*(){
 const cli=yield* OriginCli;
 return yield* Effect.result(cli.personalNamespace(process.cwd()));
}).pipe(Effect.provide(layer),Effect.provideService(SourceControlHost,host))));
try {
 const control=await run();
 const controlCalls=requests.length;
 requests=[];paged=true;
 const result=await run();
 console.log(`first-page control: ${control._tag}, namespace ${control.success ?? 'none'}, requests ${controlCalls}`);
 console.log(`personal namespace on page two: ${result._tag}, reason ${result.failure?.reason ?? 'none'}, requests ${requests.length}`);
 console.log(`paths: ${JSON.stringify(requests)}`);
 if(control._tag!=='Success'||control.success!=='zz-personal'||controlCalls!==1) {console.log('RESULT: inconclusive');process.exitCode=2;}
 else if(result._tag==='Success'&&result.success==='zz-personal') {console.log('RESULT: expected behavior');process.exitCode=0;}
 else if(result._tag==='Failure'&&result.failure.reason==='not-found'&&requests.length===1) {console.log('RESULT: defect reproduced');process.exitCode=1;}
 else {console.log('RESULT: inconclusive');process.exitCode=2;}
} catch(error){console.error(error);console.log('RESULT: inconclusive');process.exitCode=2;}

JS
node "$scratch/probe.mjs" "$repo"
status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then
  echo 'RESULT: inconclusive'
  exit 2
fi
exit "$status"

Expected behavior

The personal namespace zz-personal is found when it appears after the first page. Cursor's Origin OpenAPI document, GET /v1/origin/namespaces, orders accessible namespaces by slug and includes both team and personal namespaces. ListNamespacesRequest defaults to 30 results and ListNamespacesResponse.nextPageToken identifies the next page. Thirty lexically earlier team or granted namespaces therefore form a valid first page before a personal namespace. The fetched OpenAPI 3.1.0 / v1alpha1 artifact has SHA-256 1e53a3e45bc1f52cb0b22ff3b2e57cc41849b4471c85725d75e59a68ef6aa610.

Impact and scope

Impact: P2 - Accounts whose personal namespace falls after the first page receive a false missing-namespace error, affecting discovery's owner information and ownerless repository creation.

Source shows that discovery uses this lookup for the displayed account/owner, and repository creation with a bare repository name uses it to choose the personal owner. Existing explicitly named repositories can still be addressed separately. NOT_ENOUGH_INFO: the full discovery UI, an actual repository-creation attempt and a live account with this namespace count were not exercised. An explicit owner/name bypasses this lookup in source, but its end-to-end workaround behavior was not tested.

Version

T3 Code commit 59e64eef881632a23f89c4138bcf37f23d92bf93.

Runtime and platform

Darwin arm64; Node.js v26.12.1; npm 11.20.0 in the isolated script; Effect 4.1.3; @effect/platform-node 4.1.2. No installed Origin CLI or account is required by this fixture.

Interface

Library: OriginCli.personalNamespace, called by Cursor Origin discovery and repository creation.

Earliest failure stage

Core processing

Relevant output or evidence

Actual output from the script above, exit 1:

v26.12.1
11.20.0
effect 4.1.3
@effect/platform-node 4.1.2
first-page control: Success, namespace zz-personal, requests 1
personal namespace on page two: Failure, reason not-found, requests 1
paths: ["/v1/origin/namespaces"]
RESULT: defect reproduced

The packages/source-control-cursor-origin/src/server/OriginCli.ts#L78-L86 response schema omits nextPageToken. The packages/source-control-cursor-origin/src/server/OriginCli.ts#L464-L484 implementation searches only the first result. Callers appear at packages/source-control-cursor-origin/src/server/OriginSourceControlProvider.ts#L64-L72 and packages/source-control-cursor-origin/src/server/OriginSourceControlProvider.ts#L244-L258. All namespaces and credentials in the script are synthetic fixture values.

  1. tanstack-source added labelsbug

Sign in to join this conversation.