From e93a60264cc88ece7f26649259125b1aca39a5d5 Mon Sep 17 00:00:00 2001 From: DoSun Date: Fri, 15 May 2026 09:09:15 +0800 Subject: [PATCH] fix: resolve session status and cross-workspace leakage issues - Fix GET /session/status returning empty object due to missing await (c.json() was serializing Promise instead of resolved value) - Add getEffectiveBusyStatus() to prioritize actual prompting state - Supplement status responses with persisted sessions from loadedIndex - Add strict cwd filtering to GET /session list to prevent sessions from appearing across multiple workspaces - Add busy_status field to GET /session/:id response This closes the issue where /conversations/status returns {} even though /conversations shows running sessions, and prevents the same session from appearing in multiple workspace requests. Co-Authored-By: claude-sonnet-4-6 --- src/server/routes/session.ts | 41 +++++++++++++++++++++++++++++------- src/server/sessionHandle.ts | 10 +++++++++ src/server/sessionManager.ts | 19 ++++++++++++++++- 3 files changed, 61 insertions(+), 9 deletions(-) diff --git a/src/server/routes/session.ts b/src/server/routes/session.ts index baf51a361..5f13d1339 100644 --- a/src/server/routes/session.ts +++ b/src/server/routes/session.ts @@ -221,14 +221,12 @@ export function createSessionRoutes( const url = new URL(c.req.url) const limit = parseInt(url.searchParams.get('limit') ?? '50', 10) const offset = parseInt(url.searchParams.get('offset') ?? '0', 10) - // 优先从请求头 x-csc-directory 取工作目录(cs-cloud 通过此头传递), - // fallback 到 query string 的 dir 参数 const headerDir = c.req.header('x-csc-directory') const dir = (headerDir ? decodeURIComponent(headerDir) : undefined) ?? url.searchParams.get('dir') ?? undefined - // roots=true 时只返回没有 parentID 的顶层 session(csc 无 parent 概念,全部视为 root) - // const rootsOnly = url.searchParams.get('roots') === 'true' + + const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined // 从磁盘读取历史 session 列表 let historySessions: Awaited> = [] @@ -271,7 +269,6 @@ export function createSessionRoutes( // 补充内存中有但磁盘还没落盘的活跃 session(刚创建还没写过消息的) const historyIds = new Set(historySessions.map(s => s.sessionId)) - const canonicalDir = dir ? await canonicalizePath(dir).catch(() => dir) : undefined for (const handle of sessionManager.getAllSessions()) { if (!historyIds.has(handle.sessionId)) { if (canonicalDir) { @@ -283,7 +280,31 @@ export function createSessionRoutes( } } - // 过滤掉已知的无意义管理命令会话(用户直接输入后立即退出,没有实际对话内容) + // Pre-canonicalize all session cwds for consistent cwd filtering + const cwdCache = new Map() + const getCwd = async (p: string) => { + const hit = cwdCache.get(p) + if (hit !== undefined) return hit + const c = await canonicalizePath(p).catch(() => p) + cwdCache.set(p, c) + return c + } + + // Filter by cwd to prevent cross-workspace leakage + const cwdFiltered: typeof merged = [] + for (const s of merged) { + if (canonicalDir) { + const sessionCwd = s.cwd ?? s.directory ?? '' + if (sessionCwd) { + const c = await getCwd(sessionCwd) + if (c !== canonicalDir) continue + } else { + continue + } + } + cwdFiltered.push(s) + } + const BORING_COMMANDS = new Set([ '/exit', '/quit', '/bye', '/clear', '/reset', @@ -292,7 +313,7 @@ export function createSessionRoutes( '/help', '/version', '/compact', ]) - const filtered = merged.filter(s => { + const filtered = cwdFiltered.filter(s => { const t = s.title.trim() if (!t) return false if (BORING_COMMANDS.has(t)) return false @@ -308,7 +329,10 @@ export function createSessionRoutes( .get('/session/status', async c => { const headerDir = c.req.header('x-csc-directory') const dir = headerDir ? decodeURIComponent(headerDir) : undefined - const statuses = sessionManager.getSessionStatuses(dir) + const activeCount = sessionManager.getActiveCount() + const indexCount = sessionManager.getLoadedIndexCount() + const statuses = await sessionManager.getSessionStatuses(dir) + process.stderr.write(`[status] dir=${dir ?? '(none)'} active=${activeCount} index=${indexCount} result=${JSON.stringify(statuses)}\n`) return c.json(statuses) }) .get('/session/:sessionID', async c => { @@ -318,6 +342,7 @@ export function createSessionRoutes( const info = handle.getInfo() return c.json({ ...info, + busy_status: handle.getEffectiveBusyStatus(), message_count: handle.messageCount, usage: handle.usage, }) diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index 238b5e222..e101cffd5 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -141,6 +141,16 @@ export class SessionHandle implements DisposableChildProcess { return this._lastMessageUuid } + getEffectiveBusyStatus(): SessionBusyStatus { + if (this._prompting) { + return { type: 'busy' } + } + if (this._status === 'stopped') { + return { type: 'idle' } + } + return this._busyStatus + } + get ready(): boolean { return this._status === 'running' } diff --git a/src/server/sessionManager.ts b/src/server/sessionManager.ts index 30f50a7d7..78f01d562 100644 --- a/src/server/sessionManager.ts +++ b/src/server/sessionManager.ts @@ -147,6 +147,10 @@ export class SessionManager { return this.sessions.size } + getLoadedIndexCount(): number { + return this.loadedIndex.size + } + getSession(id: string): SessionHandle | undefined { return this.sessions.get(id) } @@ -163,7 +167,20 @@ export class SessionManager { const handleCwd = await canonicalizePath(handle.cwd) if (handleCwd !== canonicalCwd) continue } - result[id] = handle.busyStatus + result[id] = handle.getEffectiveBusyStatus() + } + // Supplement with persisted sessions that are no longer in memory + // (evicted by idle timeout / LRU). They are always idle. + if (canonicalCwd) { + for (const [id, entry] of this.loadedIndex) { + if (result[id]) continue + try { + const entryCwd = await canonicalizePath(entry.cwd) + if (entryCwd === canonicalCwd) { + result[id] = { type: 'idle' } + } + } catch {} + } } return result }