diff --git a/src/server/routes/info.ts b/src/server/routes/info.ts index fd3e7adff..518f90c14 100644 --- a/src/server/routes/info.ts +++ b/src/server/routes/info.ts @@ -45,8 +45,17 @@ export function createInfoRoutes(sessionManager: SessionManager): Hono { }) .get('/agent', c => { const allAgents = getBuiltInAgents() - return c.json( - allAgents.map(a => ({ + const defaultMode = { + name: 'build', + description: 'Default mode for software engineering tasks: writing code, editing files, running commands, and building projects.', + mode: 'primary' as const, + hidden: false, + options: {}, + permission: [], + } + return c.json([ + defaultMode, + ...allAgents.map(a => ({ name: a.agentType, description: a.whenToUse, mode: a.isMainThread ? ('primary' as const) : ('subagent' as const), @@ -54,7 +63,7 @@ export function createInfoRoutes(sessionManager: SessionManager): Hono { options: {}, permission: [], })), - ) + ]) }) .get('/mcp', async c => { for (const handle of sessionManager.getAllSessions()) { diff --git a/src/server/routes/message.ts b/src/server/routes/message.ts index 6c0c072d9..9b319ef0f 100644 --- a/src/server/routes/message.ts +++ b/src/server/routes/message.ts @@ -20,7 +20,7 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono { const { messages, nextCursor } = await readSessionMessages({ sessionId: id, - cwd: handle?.cwd, + cwd: handle?.spawnCwd ?? handle?.cwd, limit, before, includeSystem, @@ -39,7 +39,7 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono { const todos = await readSessionTodos({ sessionId: id, - cwd: handle?.cwd, + cwd: handle?.spawnCwd ?? handle?.cwd, }) return c.json({ todos }) @@ -53,7 +53,7 @@ export function createMessageRoutes(sessionManager: SessionManager): Hono { const { diffs } = await readSessionDiff({ sessionId: id, - cwd: handle?.cwd, + cwd: handle?.spawnCwd ?? handle?.cwd, messageID, }) diff --git a/src/server/routes/session.ts b/src/server/routes/session.ts index 719fa7157..463aca009 100644 --- a/src/server/routes/session.ts +++ b/src/server/routes/session.ts @@ -151,6 +151,9 @@ export function createSessionRoutes( scriptArgs: getScriptArgsForChild(), }) + // 等待子进程初始化完成,确保 ready 后再返回,避免立即发 prompt 时子进程还未就绪 + await handle.waitReady(30000) + const info = handle.getInfo() return c.json( { @@ -187,10 +190,7 @@ export function createSessionRoutes( let historySessions: Awaited> = [] try { historySessions = await listSessionsImpl({ dir, limit: limit + offset }) - process.stderr.write(`[server:session] listSessionsImpl dir=${dir ?? 'all'} found=${historySessions.length}\n`) - } catch (err) { - process.stderr.write(`[server:session] listSessionsImpl error: ${err}\n`) - } + } catch {} // 内存中活跃的 handle,用于覆盖运行时状态 const handleMap = new Map( @@ -245,7 +245,6 @@ export function createSessionRoutes( filtered.sort((a, b) => (b.last_active_at ?? 0) - (a.last_active_at ?? 0)) const sessions = filtered.slice(offset, offset + limit) - process.stderr.write(`[server:session] GET /session -> history=${historySessions.length} active=${handleMap.size} merged=${merged.length} returned=${sessions.length}\n`) return c.json({ sessions }) }) .get('/session/status', async c => { @@ -282,7 +281,7 @@ export function createSessionRoutes( status: st.status, state: st.status, has_pending_permission: st.has_pending_permission, - type: st.status === 'running' ? 'busy' : 'idle', + type: st.prompting ? 'busy' : 'idle', } } diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index 500627029..315b36d2a 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -115,6 +115,8 @@ function loadChildSpawnPrefix(): { execPath: string; scriptArgs: string[]; defin export class SessionHandle { readonly sessionId: string readonly cwd: string + /** 子进程实际的工作目录,dev 模式下与 cwd 不同(csc 根目录),用于定位 transcript 文件 */ + private _spawnCwd: string | undefined private child: ChildProcess | null = null private _status: SessionState = 'starting' private _model?: string @@ -189,6 +191,10 @@ export class SessionHandle { return this.opts.silent ?? false } + get spawnCwd(): string { + return this._spawnCwd ?? this.cwd + } + async waitReady(timeoutMs = 30000): Promise { if (this._status === 'running') return if (this._status === 'stopped') { @@ -199,11 +205,17 @@ export class SessionHandle { reject(new Error(`Session ${this.sessionId} init timed out`)) }, timeoutMs) const origResolve = this.initResolve + const origReject = this.initReject this.initResolve = (data: InitData) => { clearTimeout(timer) origResolve?.(data) resolve() } + this.initReject = (err: unknown) => { + clearTimeout(timer) + origReject?.(err) + reject(err) + } }) } @@ -263,6 +275,7 @@ export class SessionHandle { const featureArgs = saved?.featureArgs ?? [] const spawnArgs = [...defineArgs, ...featureArgs, ...this.opts.scriptArgs, ...printArgs] + this._spawnCwd = this.opts.cwd this.child = spawn(this.opts.execPath, spawnArgs, { cwd: this.opts.cwd, stdio: ['pipe', 'pipe', 'pipe'], @@ -282,8 +295,11 @@ export class SessionHandle { signal: signal ?? null, }) } - if (this.initResolve) { + if (this.initReject) { + const reject = this.initReject this.initResolve = null + this.initReject = null + reject(new Error(`Process exited with code ${code}`)) } if (this.promptReject) { this.promptReject(new Error(`Process exited with code ${code}`)) @@ -293,8 +309,11 @@ export class SessionHandle { this.child.on('error', err => { logError(err) this._status = 'stopped' - if (this.initResolve) { + if (this.initReject) { + const reject = this.initReject this.initResolve = null + this.initReject = null + reject(err) } if (this.promptReject) { this.promptReject(err) @@ -305,13 +324,17 @@ export class SessionHandle { this.sendInitialize() const timeout = setTimeout(() => { - this.initResolve = null - this.initReject = null - this._status = 'stopped' - this.emitEvent('deleted', { - status: 'stopped', - reason: 'init_timeout', - }) + if (this.initReject) { + const reject = this.initReject + this.initResolve = null + this.initReject = null + this._status = 'stopped' + this.emitEvent('deleted', { + status: 'stopped', + reason: 'init_timeout', + }) + reject(new Error(`Session ${this.sessionId} init timed out`)) + } }, 30000) this.initResolve = (data: InitData) => { diff --git a/src/server/sessionManager.ts b/src/server/sessionManager.ts index 7da799c46..bc2d227cb 100644 --- a/src/server/sessionManager.ts +++ b/src/server/sessionManager.ts @@ -128,16 +128,17 @@ export class SessionManager { getSessionStatuses(): Record< string, - { status: SessionState; has_pending_permission: boolean } + { status: SessionState; has_pending_permission: boolean; prompting: boolean } > { const result: Record< string, - { status: SessionState; has_pending_permission: boolean } + { status: SessionState; has_pending_permission: boolean; prompting: boolean } > = {} for (const [id, handle] of this.sessions) { result[id] = { status: handle.status, has_pending_permission: handle.getPendingPermissions().length > 0, + prompting: handle.prompting, } } return result diff --git a/src/server/transcriptReader.ts b/src/server/transcriptReader.ts index 6fc63aa6b..4c48a92bc 100644 --- a/src/server/transcriptReader.ts +++ b/src/server/transcriptReader.ts @@ -63,10 +63,13 @@ async function resolveTranscriptPath( ): Promise { const cacheKey = `${sessionId}:${cwd ?? ''}` const cached = pathCache.get(cacheKey) - if (cached !== undefined) return cached + // 只缓存找到的路径,不缓存 null(文件可能还未写入) + if (cached !== undefined && cached !== null) return cached const result = await resolveTranscriptPathUncached(sessionId, cwd) - pathCache.set(cacheKey, result) + if (result !== null) { + pathCache.set(cacheKey, result) + } return result } diff --git a/tsconfig.json b/tsconfig.json index 62c07df60..f7cac0b34 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "./tsconfig.base.json", "compilerOptions": { + "baseUrl": ".", + "ignoreDeprecations": "6.0", "paths": { "src/*": ["./src/*"], "@claude-code-best/builtin-tools/*": ["./packages/builtin-tools/src/*"],