The mechanism
The per-worktree journal is stored at <data-root>/worktrees/<name>/journal.jsonl, where <name> comes from _worktree_name():
return (project_dir or Path.cwd()).resolve().name
That is the basename of the current working directory. It identifies where the process was standing, not which worktree it belongs to.
Store-root resolution walks upward to find the enclosing workspace, so it is stable across subdirectories. Bucket naming does not walk at all. The two halves of the same path are derived by different rules, and only one of them is location-independent.
Consequence
Running from different directories inside one workspace writes to different journal buckets:
cd ~/work/myproject # bucket: "myproject"
cd ~/work/myproject/server # bucket: "server"
Both resolve to the same data root, because root resolution walks up and finds the workspace. Neither bucket can see the other's entries.
Because recall_journal action=read reads the current bucket, a read performed from a subdirectory returns only the slice of history recorded while standing in a directory of that name. It returns entries, so it looks like it worked. Nothing distinguishes "this is your history" from "this is the part of your history that happens to share a name with your cwd."
Two directories anywhere on the machine sharing a basename also share a bucket, which is the same defect pointed the other way.
Why this is worse than a merely-split file
read_entries sorts by timestamp and returns the most recent n. A partial bucket therefore yields a plausible, recent-looking, incomplete answer rather than an error or an empty result. The failure mode produces good-looking output, so it survives every check that asks whether something came back rather than whether it is complete.
Reproduction
A workspace marker is what holds the data root steady across subdirectories, so the reproduction needs one. Without it, root resolution also falls back to the working directory and both halves of the path vary at once, which obscures the bucket effect rather than showing it.
cd /tmp && mkdir -p proj/sub proj/.grip && cd proj
python -c "from synapt.recall.core import project_worktree_dir; print(project_worktree_dir())"
cd sub
python -c "from synapt.recall.core import project_worktree_dir; print(project_worktree_dir())"
/tmp/proj/.synapt/recall/worktrees/proj
/tmp/proj/.synapt/recall/worktrees/sub
One data root, two buckets — the root walked up and found the workspace; the bucket did not walk at all.
Existing mitigation
SYNAPT_RECALL_WORKTREE overrides the basename and is the documented companion to SYNAPT_RECALL_ROOT. Its own docstring names this hazard: redirecting only the store root files per-worktree data "under its cwd basename inside the shared store — potentially another workspace's namespace, which is cross-attribution rather than sharing."
So the escape hatch exists. The defect is that the default is wrong and fails silently, and the variable has to be known about in advance to avoid a problem the user has no way to detect.
Suggested directions
Not a fix proposal, since the right answer depends on how much existing layout must be preserved:
- Derive the bucket from the resolved worktree/repo root rather than the cwd — the information is already computed for store resolution.
- Record the intended bucket in the workspace so it is a property of the workspace, not of the invocation.
- At minimum, make it detectable: when a data root contains multiple buckets that resolve from the same workspace, say so on read instead of returning a partial answer silently.
Any migration of existing buckets should be treated as separate work: buckets accumulated under this defect can overlap heavily, so consolidation is closer to a dedupe than a merge, and the population should be characterized before anything is moved.
The mechanism
The per-worktree journal is stored at
<data-root>/worktrees/<name>/journal.jsonl, where<name>comes from_worktree_name():That is the basename of the current working directory. It identifies where the process was standing, not which worktree it belongs to.
Store-root resolution walks upward to find the enclosing workspace, so it is stable across subdirectories. Bucket naming does not walk at all. The two halves of the same path are derived by different rules, and only one of them is location-independent.
Consequence
Running from different directories inside one workspace writes to different journal buckets:
Both resolve to the same data root, because root resolution walks up and finds the workspace. Neither bucket can see the other's entries.
Because
recall_journal action=readreads the current bucket, a read performed from a subdirectory returns only the slice of history recorded while standing in a directory of that name. It returns entries, so it looks like it worked. Nothing distinguishes "this is your history" from "this is the part of your history that happens to share a name with your cwd."Two directories anywhere on the machine sharing a basename also share a bucket, which is the same defect pointed the other way.
Why this is worse than a merely-split file
read_entriessorts by timestamp and returns the most recent n. A partial bucket therefore yields a plausible, recent-looking, incomplete answer rather than an error or an empty result. The failure mode produces good-looking output, so it survives every check that asks whether something came back rather than whether it is complete.Reproduction
A workspace marker is what holds the data root steady across subdirectories, so the reproduction needs one. Without it, root resolution also falls back to the working directory and both halves of the path vary at once, which obscures the bucket effect rather than showing it.
One data root, two buckets — the root walked up and found the workspace; the bucket did not walk at all.
Existing mitigation
SYNAPT_RECALL_WORKTREEoverrides the basename and is the documented companion toSYNAPT_RECALL_ROOT. Its own docstring names this hazard: redirecting only the store root files per-worktree data "under its cwd basename inside the shared store — potentially another workspace's namespace, which is cross-attribution rather than sharing."So the escape hatch exists. The defect is that the default is wrong and fails silently, and the variable has to be known about in advance to avoid a problem the user has no way to detect.
Suggested directions
Not a fix proposal, since the right answer depends on how much existing layout must be preserved:
Any migration of existing buckets should be treated as separate work: buckets accumulated under this defect can overlap heavily, so consolidation is closer to a dedupe than a merge, and the population should be characterized before anything is moved.