feat(tags): resolve project root from a workspace marker file#148
Open
black-desk wants to merge 1 commit into
Open
feat(tags): resolve project root from a workspace marker file#148black-desk wants to merge 1 commit into
black-desk wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an opt-in configuration knob to force project identity/root to a user-specified workspace root, enabling shared memory across multi-repo workspaces (e.g., repo-managed trees) by bypassing git-derived identity.
Changes:
- Introduces
projectRootOverride(andOPENCODE_MEM_PROJECT_ROOTenv var precedence) with canonical absolute path resolution in config build. - Short-circuits project root/identity (and git repo URL detection) when the override is set.
- Adds tests and documentation describing the override behavior and usage (including a direnv example).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
tests/project-scope.test.ts |
Adds coverage for override behavior, env precedence, empty handling, and path normalization. |
src/services/tags.ts |
Applies override to project root/identity resolution and skips git URL detection under override. |
src/config.ts |
Adds new config field, resolves/normalizes it once in buildConfig, and exports buildConfig for testing. |
README.md |
Documents projectRootOverride and OPENCODE_MEM_PROJECT_ROOT usage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Project identity is normally derived from the enclosing git repository (git-common-dir, then remote origin URL, then the directory path), which splits memory into one shard per physical git repo. That is wrong for multi-repo workspaces managed by orchestrators such as Google `repo` that lay out many git repositories under a single workspace root: each sub-repository gets its own isolated memory store instead of sharing one project-wide store. The obvious fix is a manual override, but carrying that override in an environment variable or a global config value is unsound here. opencode-mem runs across multiple opencode processes that share a single web server and a single storage database, and only some of those processes carry a given env var -- the one launched from a direnv shell does; the long-lived daemon launched by systemd, and sessions started elsewhere, do not. The web server is a singleton whose owner is non-deterministic: processes detect EADDRINUSE and take it over on a health-check loop, so project identity flaps depending on which process happens to own the port. A process-scoped value cannot back a shared identity. Make identity directory-driven instead. Drop an empty `.opencode-mem-project` marker file at the workspace root; every session started anywhere underneath then resolves onto that root. The marker is looked up by walking up from the working directory that every code path already passes in (the plugin's ctx.directory, the web API's process.cwd()), so identity is bound to where the session runs, not to which process it runs in -- stable across the whole process pool regardless of env vars or web-server ownership. The walk lives in getProjectRoot/getProjectIdentity, the lowest-level entry points, so plugin load, auto-capture, user-profile learning, compaction and the web API all pick it up without per-callsite changes. The marker takes precedence over git detection; when it hits, the underlying sub-repo's git remote is intentionally left unset, since it would describe only one nested repository and be misleading for the grouped workspace. Without a marker, behaviour is unchanged (the existing worktree and nested-path tests still pass). The marker lookup is resolved exactly once per getProjectTagInfo call: the git-only fallbacks are factored into private helpers so root and identity derive from a single ancestry walk instead of three. Tests cover the collapse of sibling git repos onto one marker root, deep nested resolution, the marker winning over an inner git repo and dropping its remote, the innermost-marker-wins case, the null case, and the backward- compatible no-marker behaviour. The marker is documented in the README, including why the env-var/config approach was rejected. Typical usage at the workspace root: touch ~/my-workspace/.opencode-mem-project Assisted-by: opencode:glm-5.2 Signed-off-by: Chen Linxuan <me@black-desk.cn>
6b102f5 to
a850cc5
Compare
Comment on lines
+168
to
+171
| export function getProjectIdentity(directory: string): string { | ||
| const markerRoot = findMarkerProjectRoot(directory); | ||
| return markerRoot ? `path:${markerRoot}` : getGitProjectIdentity(directory); | ||
| } |
Comment on lines
+207
to
212
| // When a marker pins the project root, any git remote belongs to a single | ||
| // nested sub-repo and would be misleading for the grouped workspace, so | ||
| // leave it unset. | ||
| const gitRepoUrl = markerRoot ? null : getGitRepoUrl(directory); | ||
| const projectIdentity = markerRoot ? `path:${markerRoot}` : getGitProjectIdentity(projectRoot); | ||
|
|
Comment on lines
+149
to
+159
| it("findMarkerProjectRoot returns null without a marker, the ancestor when present", () => { | ||
| const { workspaceDir, repoA } = createMultiRepoWorkspace(); | ||
|
|
||
| expect(findMarkerProjectRoot(repoA)).toBeNull(); | ||
|
|
||
| writeFileSync(join(workspaceDir, ".opencode-mem-project"), ""); | ||
| expect(findMarkerProjectRoot(repoA)).toBe(workspaceDir); | ||
| // A session started exactly at the marker root still resolves to itself. | ||
| expect(findMarkerProjectRoot(workspaceDir)).toBe(workspaceDir); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Project identity is derived from the enclosing git repository (git-common-dir, then remote origin URL, then the directory path), which splits memory into one shard per physical git repo. That is wrong for multi-repo workspaces managed by orchestrators such as Google
repothat lay out many git repositories under a single workspace root: each sub-repository gets its own isolated memory store instead of sharing one project-wide store.Solution: directory-driven workspace marker
Drop an empty
.opencode-mem-projectmarker file at the workspace root. Every session started anywhere underneath resolves onto that root.The marker is looked up by walking up from the working directory that every code path already passes in — the plugin's
ctx.directory, the web API'sprocess.cwd(). Identity is therefore bound to where the session runs, not to ambient process state, so it stays consistent regardless of which process serves a given request.touch ~/my-workspace/.opencode-mem-projectBehavior
getProjectRoot/getProjectIdentity(the lowest-level entry points), so plugin load, auto-capture, user-profile learning, compaction, and the web API all pick it up with no per-callsite changes.getProjectTagInfocall; the git-only fallbacks are factored into private helpers so root and identity derive from a single ancestry walk.Testing
New cases in
tests/project-scope.test.tscover: sibling git repos collapse onto one marker root; deep nested paths resolve up to the marker; the marker wins over an inner git repo and drops its remote;findMarkerProjectRootnull / ancestor / self cases; plus the backward-compatible no-marker behaviour (the existing worktree and nested-path tests still pass).tsc --noEmit,prettier, andbun run buildare clean. The pre-commit hook (typecheck + lint-staged) passes.