Repository navigation
fix: cross-workspace memory search stays inside the caller's tenant (#415) - #421
Merged
Merged
Conversation
…415) fleet_recall, fleet_find and session.search scope:all searched every workspace in the memory store, which spans every tenant on a shared daemon. The store now records each workspace's owning tenant (workspace_tenants; registered by every session at construction, plus a one-time backfill from persisted sessions), and every cross-workspace primitive takes the caller's tenant and joins it. A workspace whose owner cannot be proven is invisible to cross-workspace search. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…han re-key - ftsSearchGlobal pins the loop order with CROSS JOIN; the plain JOIN made cross-workspace search 200-8000x slower on a real store (plan-guard test). - The backfill registers the workspace ids episodes actually carry under their session's tenant, and gives an orphan an owner only when workspaceIdFor(dir, tenant) reproduces its id. Path-only ids never get one. - The tenant migration no longer re-keys orphans: surviving sessions cannot prove a destroyed session's tenant. - NUL in a tenant id is refused; a workspace passed with a tenant must belong to it; a failed registration is retried before the next episode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…unded probes - The ownership backfill waits until the pre-tenant re-key has run: before it, a path-only id can hold several tenants' episodes. - The git probe behind a workspace id has a timeout and is memoized per directory, so hashing one directory for several tenants forks once. - Orphan probing stays under session workdirs and the user's home; file paths come from agents' tool calls. - The orphan query is chunked under SQLite's parameter limit, and one malformed tenant row no longer aborts every tenant's re-key. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
rsharath
approved these changes
Oct 24, 2026
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.
What this fixes
On a daemon shared by more than one tenant, conductor fleet tools and the search box could return other tenants' memory.
fleet_recall,fleet_find, andsession.searchwith scopeallsearched every workspace in the memory store, which spans every tenant on the daemon.fleet_findhad the same hole as the two paths named in #415.Now every search that spans workspaces is limited to the caller's tenant, and that limit is enforced in the memory store itself, not by each caller.
A workspace whose owner can't be proven is excluded from those searches.
Fixes #415.
What changes for users
Review
Two independent audit rounds, each with a security reviewer and a correctness reviewer, all findings folded in:
Full suite: 3709 pass / 0 fail. Typecheck and lint clean.
For agents: implementation
memory/store.ts: newworkspace_tenants (workspace_id PK, account_id, project_id)andmemory_migrations.listWorkspaceIds(tenant)andftsSearchGlobal(query, limit, tenant)join it; the FTS query usesCROSS JOINso the FTS index drives (explainFtsSearchGlobal+ plan test).registerWorkspaceTenant,isWorkspaceOwnedBy,listWorkspaceTenants.backfillWorkspaceTenants: one-time, waits untilneedsWorkspaceMigration()is false. Registers (a) episode-carried ids whose persisted sessions all share one tenant, excluding pre-tenant path ids, and (b) orphan ids only whenworkspaceIdFromPath(dir, tenant)reproduces them, for dirs under session workdirs or$HOME(≤400) and tenants from sessions pluslocal.migrateWorkspaceIdsToTenantno longer re-keys orphans (surviving sessions cannot prove a destroyed session's tenant), and skips a malformed row instead of aborting.workspaceIdFromPathrefuses NUL in tenant parts; the git probe has a 5 s timeout and a per-directory cache.memory/engine.ts:recallGlobalrequirestenant(throws without);searchSessionstakesSearchSessionsScope— a workspace, or a tenant for cross-workspace; a workspace given with a tenant must belong to it;registerWorkspace.session.ts: registers its workspace at construction and retries before the next episode if that failed.fleet.ts:FleetDeps.tenant, used byfleet_find/fleet_recall.session-manager.ts: conductor + orchestrator deps set it;session.searchpasses the caller's tenant.server.ts: runs the backfill after the re-key migration, withlocaltenant and$HOMEprobe root.🤖 Generated with Claude Code