Edited to correct the original report. My first version (written from the compiled dist/ bundle) claimed project identity was the full filesystem path. Reading the actual source shows it's the git-toplevel basename, which is more correct than I first stated but still has real failure modes. Corrected analysis below. (Second edit: sharpened the Impact section with the exact source-level scoping behavior — memory_smart_search is global, but mem::context and the project profile are hard-scoped by exact project string.)
Summary
Project scope is derived from the git-toplevel basename (falling back to the cwd basename). This means:
- Collision: two unrelated repos whose top-level directory share a name (e.g. both checked out as
app, server, frontend, or two different forks both named agentmemory) collapse into one project bucket.
- No cross-checkout-name unification: the same repo checked out under different directory names — or under different absolute paths on two machines — resolves to different projects.
- There is no use of the git remote, and no canonicalization to a globally stable identity.
(Full absolute-path project values that exist in older stores predate the basename logic — they're legacy rows from an earlier version, which is a separate migration concern.)
Where
resolveProject() — src/hooks/_project.ts:
// Resolution order: AGENTMEMORY_PROJECT_NAME env → git toplevel basename → cwd basename.
export function resolveProject(cwd?: string): string {
const explicit = process.env["AGENTMEMORY_PROJECT_NAME"];
if (explicit && explicit.trim()) return explicit.trim();
const dir = cwd && cwd.trim() ? cwd : process.cwd();
try {
const top = execSync("git rev-parse --show-toplevel", { cwd: dir, /* … */ }).toString().trim();
if (top) return basename(top); // <-- basename only; "agentmemory", not the repo identity
} catch {}
return basename(dir);
}
This shared resolver is imported by all nine hooks (session-start, post-tool-use, prompt-submit, subagent-*, notification, task-completed, pre-compact, post-tool-failure). The server stores the value verbatim — api::session::start in src/triggers/api.ts persists body.project as-is and does not re-derive it. So identity is decided entirely client-side in resolveProject, which is the right single seam to fix.
Impact
The project tag's effect is path-dependent, which is what makes this subtle. After reading the search/context functions:
memory_smart_search / mem::smart-search — global, not affected. The hybrid observation search is called with (query, limit) only; project is never passed to the searcher (src/functions/smart-search.ts). It is forwarded solely to recallLessons. So explicit recall already spans every project tag — fragmented tags do not hide observations from search.
mem::context (session-start auto-injected context) — affected, and load-bearing. Related prior sessions are hard-filtered by exact string match — s.project === data.project (src/functions/context.ts). Two tags for the same repo means a session on one machine never surfaces the other machine's prior work in its startup context. (Note: injection is gated behind AGENTMEMORY_INJECT_CONTEXT=true, off by default since 0.8.10, so real-world impact depends on whether the user enabled it.)
- Project profile — affected. Keyed by the exact project string:
kv.get(KV.profiles, data.project) (src/functions/context.ts). Fragmented tags produce two separate rolling profiles instead of one unified one.
- Lessons — soft-scoped. Global lessons still surface; project-matching ones get a 1.5× ranking boost (
src/functions/context.ts). Degraded ranking, not exclusion.
mem::search (lower-level) — affected when scoped. It accepts a real projectFilter and drops non-matching observations (src/functions/search.ts). Any caller that passes a project gets partitioned results.
memory_sessions, patterns, profile views — affected. All group by exact project tag.
Net: observations are never lost (global search finds them), but the project-scoped surfaces that give a fresh session continuity — auto-context and the rolling profile — silently fragment across same-repo checkouts.
Proposed fix (implemented)
Derive a stable host/org/repo identity from the git remote when available, falling back to the existing basename behavior:
git config --get remote.origin.url → normalize scp-style SSH (git@host:org/repo.git), ssh://, git://, https://, and credentialed URLs → lowercase host, strip port/.git → host/org/repo.
- Fall back to git-toplevel basename, then cwd basename, when there's no remote.
- Keep
AGENTMEMORY_PROJECT_NAME as the explicit override.
To avoid changing behavior for existing users (and fragmenting their stored project keys on upgrade), I gated this behind an opt-in AGENTMEMORY_PROJECT_FROM_REMOTE flag — default behavior is unchanged. Could be promoted to default in a future major if desired.
Branch with the change (+ normalizeGitRemote unit tests and remote-mode coverage; all project-scoped suites pass): https://github.com/devon3000/agentmemory/tree/fix/git-remote-project-identity
A migration concern remains for existing stores: legacy rows (full-path and basename project values) won't unify with new remote-based identities. A one-time backfill/re-tag is needed to consolidate them. Happy to open a PR if the approach (and the opt-in gating) looks right.
Summary
Project scope is derived from the git-toplevel basename (falling back to the cwd basename). This means:
app,server,frontend, or two different forks both namedagentmemory) collapse into one project bucket.(Full absolute-path
projectvalues that exist in older stores predate the basename logic — they're legacy rows from an earlier version, which is a separate migration concern.)Where
resolveProject()—src/hooks/_project.ts:This shared resolver is imported by all nine hooks (
session-start,post-tool-use,prompt-submit,subagent-*,notification,task-completed,pre-compact,post-tool-failure). The server stores the value verbatim —api::session::startinsrc/triggers/api.tspersistsbody.projectas-is and does not re-derive it. So identity is decided entirely client-side inresolveProject, which is the right single seam to fix.Impact
The
projecttag's effect is path-dependent, which is what makes this subtle. After reading the search/context functions:memory_smart_search/mem::smart-search— global, not affected. The hybrid observation search is called with(query, limit)only;projectis never passed to the searcher (src/functions/smart-search.ts). It is forwarded solely torecallLessons. So explicit recall already spans every project tag — fragmented tags do not hide observations from search.mem::context(session-start auto-injected context) — affected, and load-bearing. Related prior sessions are hard-filtered by exact string match —s.project === data.project(src/functions/context.ts). Two tags for the same repo means a session on one machine never surfaces the other machine's prior work in its startup context. (Note: injection is gated behindAGENTMEMORY_INJECT_CONTEXT=true, off by default since 0.8.10, so real-world impact depends on whether the user enabled it.)kv.get(KV.profiles, data.project)(src/functions/context.ts). Fragmented tags produce two separate rolling profiles instead of one unified one.src/functions/context.ts). Degraded ranking, not exclusion.mem::search(lower-level) — affected when scoped. It accepts a realprojectFilterand drops non-matching observations (src/functions/search.ts). Any caller that passes a project gets partitioned results.memory_sessions, patterns, profile views — affected. All group by exact project tag.Net: observations are never lost (global search finds them), but the project-scoped surfaces that give a fresh session continuity — auto-context and the rolling profile — silently fragment across same-repo checkouts.
Proposed fix (implemented)
Derive a stable
host/org/repoidentity from the git remote when available, falling back to the existing basename behavior:git config --get remote.origin.url→ normalize scp-style SSH (git@host:org/repo.git),ssh://,git://,https://, and credentialed URLs → lowercase host, strip port/.git→host/org/repo.AGENTMEMORY_PROJECT_NAMEas the explicit override.To avoid changing behavior for existing users (and fragmenting their stored
projectkeys on upgrade), I gated this behind an opt-inAGENTMEMORY_PROJECT_FROM_REMOTEflag — default behavior is unchanged. Could be promoted to default in a future major if desired.Branch with the change (+
normalizeGitRemoteunit tests and remote-mode coverage; all project-scoped suites pass): https://github.com/devon3000/agentmemory/tree/fix/git-remote-project-identityA migration concern remains for existing stores: legacy rows (full-path and basename
projectvalues) won't unify with new remote-based identities. A one-time backfill/re-tag is needed to consolidate them. Happy to open a PR if the approach (and the opt-in gating) looks right.