Skip to content

One project, however its path is spelled - #259

Merged
scgopi merged 1 commit into
mainfrom
fix/duplicate-project-paths
Sep 3, 2026
Merged

One project, however its path is spelled#259
scgopi merged 1 commit into
mainfrom
fix/duplicate-project-paths

Conversation

@scgopi

@scgopi scgopi commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What was happening

Creating a child loop from inside a codespace added a second project to the sidebar — same name, empty graph — and the child loop went into it, where nothing was watching.

Why

Opening a project is create-if-missing. That is deliberate: graphcode status <folder> from a shell is how a folder gets added. But every path a client named went straight through it, and a loop names paths constantly — its own worktree, its working directory, its project path with a trailing slash.

A codespace turned that from possible into certain. A remote path cannot be checked against this filesystem, so unlike a local folder, every spelling of one was openable. Reproduced against a real daemon before the fix:

$ graphcode projects
repo @ demo-cs  codespace://demo-cs/workspaces//repo
repo @ demo-cs  codespace://demo-cs/workspaces/repo/
repo @ demo-cs  codespace://demo-cs/workspaces/repo

Locally the same bug is bounded by the existence check: only a real subfolder or worktree of a project could become a second project (graphcode status /repo/docs added "docs").

Naming a path that could not be opened was worse than useless: the refusal was dropped, the CLI waited out its socket timeout, and then reported the command as possibly applied when nothing past the open had been sent.

The fix

  • canonicalize normalizes remote spellings the way it has always normalized local ones. Trailing slashes, doubled separators and . segments collapse. Different hosts stay different projects.
  • Routing a named path is a policy, not a lookup. For a client that is not the app: a folder inside an existing project is that project, and a remote path the daemon has never seen is refused. Remote projects are added in the app, which validates the connection over ssh first. The app keeps both powers — opening a nested folder and adding a host are deliberate human acts there, and Add Folder still adds a genuinely new folder from a shell.
  • Refusals are sent back as errors and both CLIs read them, so a wrong path is one line, immediately, naming the path.
  • The remote shim resolves before it opens. It is the one place that knows a bare absolute path is this host's spelling, so /workspaces/repo and worktrees under it address the codespace project they belong to, and it says so on stderr.
  • Rows a pre-fix daemon left behind clear themselves out on the next launch — but only the empty ones. A twin that collected loops stays where it is; those loops are somebody's work.

Verified

After the fix, against a live daemon with one codespace project added the way the app adds it:

Command Before After
status codespace://cs/workspaces/widget/ second project the project, instantly
node create codespace://cs/workspaces/widget/worktrees/fix-215 … third project, loop inside it loop in the real project
status codespace://other-space/workspaces/widget fourth project refused, exit 1, names graphcode projects
status /workspaces/widget (Mac shell) 10s wait, "may have been applied" refused, exit 1
status /workspaces/widget (shim, on the host) second project resolves to the codespace project
status /brand/new/folder adds it adds it

Test run: 1518 tests, 157 suites, all passing (10 new, in DuplicateProjectPathTests and RemoteCLIShimTests). swiftlint clean, swift-format clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WYzCYpAW75pcCCDD1eq7wV

Creating a child loop from inside a codespace added a second project to the
sidebar and put the loop in it, where nothing was watching.

Opening a project is create-if-missing — that is how `graphcode status <folder>`
adds a folder from a shell — and every path a client named went straight through
it. A loop names paths constantly: its own worktree, its working directory, its
project with a trailing slash. Each became another project with its own graph,
its own recents entry and its own sidebar row under the same name.

A codespace turned that from possible into certain. A remote path cannot be
checked against this filesystem, so unlike a local folder *every* spelling of one
was openable: `codespace://cs/workspaces/repo/`, `…//repo`, and every subpath of
it were four projects.

Three changes, from the daemon outwards:

- `canonicalize` now normalizes a remote path's spelling the way it has always
  normalized a local one. Trailing slashes, doubled separators and `.` segments
  collapse; different hosts stay different projects.
- Routing a named path is now a policy rather than a lookup. For a client that is
  not the app, a folder inside a project that already exists *is* that project,
  and a remote path the daemon has never seen is refused — remote projects are
  added in the app, which validates the connection over ssh first. The app keeps
  both powers: opening a nested folder and adding a host are deliberate human
  acts there.
- A refusal is now sent back as an error instead of dropped. Naming a path that
  was not a project used to be silence: the CLI waited out its socket timeout and
  then reported the command as *possibly applied*, when nothing past the open had
  been sent. Both CLIs read it and print the one line saying which path was wrong.

The remote shim resolves before it opens. It is the one place that knows a bare
absolute path is *this host's* spelling, so `/workspaces/repo` and a worktree
under it now address the codespace project they belong to, and say so on stderr.

Sidebar rows a pre-fix daemon left behind clear themselves out on the next
launch, but only the empty ones: a twin that collected loops stays exactly where
it is, because those loops are somebody's work and a merged-away row is one
nobody can find again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WYzCYpAW75pcCCDD1eq7wV
@scgopi
scgopi force-pushed the fix/duplicate-project-paths branch from 24075fc to e0a0043 Compare September 3, 2026 03:04
@scgopi
scgopi merged commit 6aa6e26 into main Sep 3, 2026
1 check 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.

1 participant