thedotmack/claude-mem · error · Error
server_session_id must belong to project_id and team_id
Error message
server_session_id must belong to project_id and team_id
What it means
assertSessionOwnership verifies that a server_session_id belongs to both the supplied project and team before writes proceed. The SELECT matches on id, project_id, and team_id simultaneously; any mismatch means the session is not scoped to that project/team and the error is thrown. It enforces the full three-level ownership chain (team -> project -> session).
Solutions
- Confirm the server_session_id was created under the same projectId and teamId in the request
- Re-query server_sessions for the project and use a live session ID
- Check for cross-environment (dev/staging/prod) configuration mixing
- Recreate the session under the correct project if it was deleted
Example fix
// before
await addSource({ teamId, projectId: 'projA', serverSessionId: sessionOfProjB });
// after
const sessions = await listSessions(teamId, 'projA');
await addSource({ teamId, projectId: 'projA', serverSessionId: sessions[0].id }); Defensive patterns
Strategy: validation
Validate before calling
const row = await queryOne('SELECT id FROM server_sessions WHERE id = $1 AND project_id = $2 AND team_id = $3', [serverSessionId, projectId, teamId]);
if (!row) throw new Error('session not in project/team'); Try / catch
try { await addSource(args); }
catch (e) { if (e.message.includes('server_session_id must belong')) { args.serverSessionId = await getDefaultSession(args.teamId, args.projectId); await addSource(args); } else throw e; } Prevention
- Keep session IDs together with their project/team scope in one config object
- Never share session IDs across environments
- Refresh session IDs after any project migration or deletion
When it happens
Trigger: Calling create, validateSource, or addSource with a serverSessionId that belongs to a different project, a different team, or that no longer exists.
Common situations: Reusing a session ID across projects; passing a session ID from a dev database to a prod team; sessions deleted between listing and use; mixing IDs from two tenants in one request.
Understand the failure class
Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.
Related errors
- project_id must belong to team_id
- Adapter rejected input
- agent_event source_id must belong to project_id and team_id
- API key is scoped to a different project
- Cannot bootstrap server API key…
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/8a96bec8ab8a7a2f.
Report an issue: GitHub.
Appendix: source
Thrown at src/storage/postgres/utils.ts:78
);
if (!row) {
throw new Error('project_id must belong to team_id');
}
}
export async function assertSessionOwnership(
client: PostgresQueryable,
serverSessionId: string,
projectId: string,
teamId: string
): Promise<void> {
const row = await queryOne<{ id: string }>(
client,
'SELECT id FROM server_sessions WHERE id = $1 AND project_id = $2 AND team_id = $3',
[serverSessionId, projectId, teamId]
);
if (!row) {
throw new Error('server_session_id must belong to project_id and team_id');
}
}
export function canonicalJson(value: unknown): string {
return JSON.stringify(sortJson(value));
}
export function deterministicKey(parts: readonly unknown[]): string {
const fingerprint = createHash('sha256')
.update(canonicalJson(parts))
.digest('hex');
return fingerprint;
}
function sortJson(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(sortJson);
}View on GitHub (pinned to d8bc9755e7)