user-scoped channel sessions, whatsapp disconnect cleanup, browser relay token fix

Channel session IDs now include userId (channel-{provider}-{userId}-{contextId})
to prevent cross-user contamination in multi-user setups. WhatsApp disconnect
properly logs out and clears cached auth. Browser relay uses server-derived token
directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 14:14:47 +00:00
co-authored by Claude Opus 4.6
parent 209d58a525
commit 8f1963fedb
7 changed files with 45 additions and 28 deletions
+16 -11
View File
@@ -39,6 +39,10 @@ const sessionCallbacks = new Map<string, EventCallback>();
// Channel model overrides — survive session eviction/recreation
const channelModelOverrides = new Map<string, string>();
function buildSessionId(context: string, userId: number, contextId: string): string {
return `channel-${context}-${userId}-${contextId}`;
}
async function getUserDefaultModel(userId: number): Promise<string | null> {
try {
const settings = await getUserSettings(userId);
@@ -49,14 +53,14 @@ async function getUserDefaultModel(userId: number): Promise<string | null> {
}
}
export function getSessionModel(context: string, contextId: string): string | null {
const sessionId = `channel-${context}-${contextId}`;
export function getSessionModel(context: string, userId: number, contextId: string): string | null {
const sessionId = buildSessionId(context, userId, contextId);
const session = sessionManager.getSession(sessionId);
return session?.model ?? channelModelOverrides.get(sessionId) ?? null;
}
export function setSessionModel(context: string, contextId: string, model: string): void {
const sessionId = `channel-${context}-${contextId}`;
export function setSessionModel(context: string, userId: number, contextId: string, model: string): void {
const sessionId = buildSessionId(context, userId, contextId);
// Store override independently of session — survives idle eviction
channelModelOverrides.set(sessionId, model);
@@ -76,8 +80,8 @@ export function setSessionModel(context: string, contextId: string, model: strin
}
export async function sendAndAwait(params: SendAndAwaitParams): Promise<SendAndAwaitResult> {
const { context, contextId } = params;
const sessionId = `channel-${context}-${contextId}`;
const { userId, context, contextId } = params;
const sessionId = buildSessionId(context, userId, contextId);
// Serialize per session — if two messages arrive at once, second waits for first
const existing = sessionLocks.get(sessionId) ?? Promise.resolve();
@@ -112,19 +116,20 @@ async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<Se
const homeDir = getHomeDir(email);
const cwd = homeDir;
// Resolve model: explicit param > channel override > existing session model > user default > system default
// Resolve model: explicit param > !model override > user default > existing session > system default
const existingSession = sessionManager.getSession(sessionId);
let model = params.model ?? channelModelOverrides.get(sessionId) ?? existingSession?.model;
const override = channelModelOverrides.get(sessionId);
let model = params.model ?? override;
if (!model) {
const userDefault = await getUserDefaultModel(userId);
model = userDefault ?? DEFAULT_MODEL;
model = userDefault ?? existingSession?.model ?? DEFAULT_MODEL;
}
const session = sessionManager.getOrCreate(sessionId, email, cwd, model, null, context, contextId);
session.model = model;
session.meta.model = model;
session.userId = userId;
logger.info('Channel doSend', { sessionId, model, hasProcess: !!session.piProcess });
logger.info('Channel doSend', { sessionId, model, hasProcess: !!session.piProcess, userId, email });
return new Promise<SendAndAwaitResult>((resolve, reject) => {
let resultText = '';
@@ -296,7 +301,7 @@ async function doSend(sessionId: string, params: SendAndAwaitParams): Promise<Se
}
});
logger.info('Spawned Pi for channel session', { sessionId, model, context });
logger.info('Spawned Pi for channel session', { sessionId, model, context, userId, email });
}
// Add user message