Skip to content

fix(workspace): name a workspace by its resolved path - #1798

Open
ningmao-hlyz wants to merge 9 commits into
Agent-Field:devfrom
ningmao-hlyz:fix/1761-symlink-workspace-keys
Open

ningmao-hlyz wants to merge 9 commits into
Agent-Field:devfrom
ningmao-hlyz:fix/1761-symlink-workspace-keys

Conversation

@ningmao-hlyz

Copy link
Copy Markdown

Fixes #1761.

What changed

A folder reached through a symlink became two projects, two conversation
histories and two engines. Both doors where a person's path becomes a key now
resolve symlinks with filepath.EvalSymlinks before keying:

  • engineWorkspace (cmd/codeaf/engine.go) — the resolver every engine
    command goes through (--daemon, --status, --stop) and the source of
    the host's workspace name. It stopped at filepath.Clean, so
    sha256(Clean(...)) in enginehost.where minted one host directory per
    spelling: --status <link> missed the engine started at <dir>.
  • /project (internal/tui3/projectcmd.go) — the pin stored the raw typed
    spelling, so home keyed the folder under the alias while a launch from the
    resolved path (already canonicalized by v3OpenTarget) keyed it under the
    target: two project rows, one folder.

Regression test: cmd/codeaf/engine_symlink_test.go drives a symlink through
engineWorkspace and asserts both spellings land on the same directory, and
that a plain path still resolves to itself.

Changelog entry: docs/rules/changelog.md names the entry for its pull
request, and the number exists only once the pull request does — so it
arrives in the next push as
docs/changes/unreleased/<this-pr>-symlink-workspace-keys.md
(kind: fixed, surface: [engine, workspace]). The first red on check
is that workflow, not a mistake.

How it was checked

  • go build ./cmd/codeaf ./internal/tui3 — exit 0 (go1.26.5).
  • go test ./cmd/codeaf -run TestEngineWorkspace -count=1 -v — both tests
    pass: TestEngineWorkspaceResolvesSymlink,
    TestEngineWorkspaceKeepsNonSymlinkPathUnchanged (ok … 2.434s). To run
    this on Windows the other cmd/codeaf test files were moved aside for the
    run and put back — see the limitation below.
  • GOOS=linux go vet ./cmd/codeaf ./internal/tui3 — exit 0, the platform CI
    runs, so every test file (including the added one) typechecks there.
  • gofmt on the committed blobs — clean (engine.go, projectcmd.go,
    engine_symlink_test.go).
  • Reproduction at base 8761488a: a stdlib probe mirroring enginehost.where
    showed sha256(Clean(<link>)) ≠ sha256(Clean(<real>)) — two spellings,
    two host directories — and that EvalSymlinks collapses both spellings to
    one host directory.

Honest limitation: cmd/codeaf's test package does not build on Windows at
all — carried_child_test.go uses fakeFolderProgram, which lives behind
//go:build !windows; the identical failure exists at base 8761488a. So
the full suite cannot run on my Windows checkout and defers to CI on Linux
(where GOOS=linux go vet passes).

Checklist

  • A change entry — arrives in the next push, named for this pull request
    (the number does not exist until the pull request does; see
    docs/rules/changelog.md).
  • The manual knows about it — no slash command, key, tool, default, limit
    or refusal moved (keying fix only), so internal/manual/chat/ is
    unchanged.
  • No new line in .github/known-red.txt.
  • Only my own paths are staged — cmd/codeaf/engine.go,
    cmd/codeaf/engine_symlink_test.go, internal/tui3/projectcmd.go.

@CLAassistant

CLAassistant commented Oct 9, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Member

Hey, thanks for the PR. Could you sign the CLA when you get a chance so we can review it?

ningmao-hlyz added a commit to ningmao-hlyz/CodeAF that referenced this pull request Oct 9, 2026
A folder reached through a symlink became two projects, two conversation
histories and two engines: engineWorkspace and /project stopped at
filepath.Clean, so each spelling hashed to its own enginehost host
directory and its own project row.

Resolve symlinks (filepath.EvalSymlinks) at both doors where a person's
path becomes a key: engineWorkspace (--daemon/--status/--stop and the
host's workspace name) and runProjectCommand (the project pin).

Fixes Agent-Field#1761
@ningmao-hlyz
ningmao-hlyz force-pushed the fix/1761-symlink-workspace-keys branch from fd94fdf to fa7d438 Compare October 9, 2026 16:50
@ningmao-hlyz

Copy link
Copy Markdown
Author

Hey, thanks for the PR. Could you sign the CLA when you get a chance so we can review it?

done

@ZeroPoint95 ZeroPoint95 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hi @ningmao-hlyz, thanks for working on this fix!

I’m requesting changes because resolving the path in only some entry points introduces inconsistent workspace identities.

  1. Resolve the local chat workspace before dialing or spawning. In a non-Git folder reached through a symlink, PWD can preserve the alias. The local surface still hashes that alias, while the daemon now resolves it and listens under a different key. The connection fails and chat falls back to running in the terminal. Please use the same resolved workspace for host lookup, dialing and spawning.

  2. Resolve the default home directory too. engineWorkspace("") returns HOME before reaching the new canonicalization. When HOME is a symlink, omitting --workspace and explicitly passing the same home now select different hosts. Please route the default through the same resolution.

  3. Update the affected tests and cover these entry points. Five existing macOS tests now fail because their fixtures or expectations use unresolved paths, including two failures in make test-laws. Please normalize the fixture paths and expected results, and add symlink regressions for local host startup, default HOME, and typed /project.

Both functional reproductions fail at this head and pass on the base. Builds, vet, changelog validation and packed-manual checks passed. CI is green, but the local laws and race runs expose the failures above; unrelated failures were separately confirmed on the base.

@ningmao-hlyz

Copy link
Copy Markdown
Author

Thanks for the thorough review, and for taking the time to run the repros at this head.

All three points are confirmed as real gaps in my patch:

  1. Local chat hashing the unresolved PWD alias while the daemon listens on the resolved key - agreed, host lookup, dialing and spawning need to share one resolved workspace.
  2. The default HOME path skipping the canonicalization entirely - agreed, engineWorkspace("") needs to go through the same resolution.
  3. The five macOS test failures and missing coverage - I will normalize the fixtures and add symlink regressions for local host startup, default HOME and typed /project.

Working on the fixes now; I will push as incremental commits and follow up here with the new SHA once all three points are addressed.

Every identity question in this package — host lookup, dial, spawn, lock,
status, stop — funnels through where, so where is the one place a symlinked
spelling must resolve. A surface that reached the folder through a link and a
daemon named by the resolved path hashed to two host directories, and the
launch spent its wait failing to meet (Agent-Field#1761). A path that does not resolve —
a workspace not on disk yet, a test's imagined directory — keeps its spelling.
engineWorkspace("") returned home without the resolution a typed home goes
through, so a home behind a symlink keyed two hosts depending on whether
--workspace was omitted or spelled out (Agent-Field#1761). The default now walks the same
door.

And the launch-side regression: a surface that dials through a symlinked
spelling must meet the host standing at the folder it reaches.
A pin names the folder the link reaches, not the spelling that was typed —
the typed-path half of Agent-Field#1761's regression, beside the daemon and host halves
already on this branch.
@ningmao-hlyz

Copy link
Copy Markdown
Author

All three points are addressed at d26cae9, pushed as three incremental commits on top of the dev merge.

  1. Local chat hashing the alias. The split is gone at the identity layer rather than at any entry point: every host question in internal/enginehost - lookup, dial, spawn, lock, status, stop - funnels through one place, and that place now resolves the spelling before it hashes. A surface dialing through a symlink and a daemon named by the resolved path can no longer build two host directories. Regression: a host standing at the folder answers a dial that arrives through the link (cmd/codeaf), and the two spellings get one socket and one lock (internal/enginehost).

  2. The default home. engineWorkspace("") walks the same resolution a typed home does now, so omitting --workspace and naming the same home cannot key two hosts. Regression covers exactly that pair.

  3. The five macOS failures. I believe they share the same root as point 1 rather than being fixture bugs: the tests hand one temp-directory spelling to the child they spawn and compute the socket from the same unresolved string themselves, so the child daemon (which resolves through engineWorkspace) listened one key away from where the test attached. The identity unification removes that split for every such test, and I could not run macOS locally to verify - could you re-run make test-laws and the race run at this SHA? If any failure survives, it is a genuine fixture mismatch and I will fix those files separately.

Also added the typed /project regression the review asked for (internal/tui3). Builds, gofmt, GOOS=linux vet and the touched suites pass here; the full cmd/codeaf suite still defers to Linux CI for the same Windows limitations stated in the PR description.

The surface's own answer — v3Workspace — minted the session bucket and the
tools root from whatever spelling the terminal walked in with, so on the
in-process doors one folder reached through a symlink was still a second
project beside the same folder named by its resolved path. The review of
Agent-Field#1798 caught the host half of that split; this closes the surface half the
same way, at the one place a project key is minted.
ProjectKey's own doc calls the fallback "the canonical path's hash", but
filepath.Abs canonicalizes nothing about links — a folder reached through a
symlink keyed two memory projects (Agent-Field#1761). EvalSymlinks after Abs, with the
absolute spelling kept when the path does not resolve.
The entry described the engineWorkspace door alone. The review pass that
followed spread the resolution across the host identity, the default home,
the surface's project key and the memory path fallback — the entry says so,
because a memory that says "fixed at engineWorkspace" would send the next
reader looking for one door and finding four.
@ningmao-hlyz

Copy link
Copy Markdown
Author

A second pass over the identity question before you re-run the suites, and it caught two doors the first round left split - both now resolved at ac09e71.

  1. The surface's own project key. v3Workspace minted the session bucket and the tools root from whatever spelling the terminal walked in with, so on the in-process doors (--once, --no-host, the fallback road) one folder reached through a symlink was still a second project beside the same folder named by its resolved path - the exact "two histories" complaint of workspace: a folder reached through a symlink becomes two projects and two engines #1761, one layer up. The answer now resolves at that one mint, which also keeps the projects panel and the drafts keys on one spelling, since everything downstream reads the workspace from here.

  2. Memory's path fallback. gitidentity.ProjectKey's doc calls its fallback "the canonical path's hash", but filepath.Abs canonicalizes nothing about links - a non-git folder behind a symlink keyed two memory projects. The fallback now resolves, matching what the doc already promised.

Both carry regressions that fail on the previous head and pass here, and the changelog entry now names every door that mints a key instead of just the first one. tui3's full suite, the touched cmd/codeaf suites, gofmt and the changelog check are green here; the Windows-only failures this machine shows are the same set the base shows.

That closes every place a path becomes a key that I could find: engineWorkspace (named and default), enginehost.where, v3Workspace, gitidentity, and the /project pin. If your macOS run still surfaces a fixture mismatch, it will be a genuine one now.

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.

workspace: a folder reached through a symlink becomes two projects and two engines

4 participants