From 265ff7d8648d472f59d1b518059d6984eda13c96 Mon Sep 17 00:00:00 2001 From: DoSun Date: Sat, 9 May 2026 16:37:03 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=B8=AD=E6=96=AD=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=97=B6=E6=B7=BB=E5=8A=A0=20is=5Finterrupted=20=E6=A0=87?= =?UTF-8?q?=E8=AE=B0=E5=B9=B6=E4=BC=98=E5=8C=96=20abort=20=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E9=80=9F=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QueryEngine: result 消息新增 is_interrupted 字段,标识用户中断 - sessionHandle: abort() 不再等待子进程退出,改为等待 result 事件(2s 超时兜底) --- src/QueryEngine.ts | 6 +----- src/server/sessionHandle.ts | 17 +++++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/QueryEngine.ts b/src/QueryEngine.ts index 6704dac68..030d93f0c 100644 --- a/src/QueryEngine.ts +++ b/src/QueryEngine.ts @@ -1116,6 +1116,7 @@ export class QueryEngine { duration_ms: Date.now() - startTime, duration_api_ms: getTotalAPIDuration(), is_error: true, + is_interrupted: this.abortController.signal.aborted, num_turns: turnCount, stop_reason: lastStopReason, session_id: getSessionId(), @@ -1128,11 +1129,6 @@ export class QueryEngine { initialAppState.fastMode, ), uuid: randomUUID(), - // Diagnostic prefix: these are what isResultSuccessful() checks — if - // the result type isn't assistant-with-text/thinking or user-with- - // tool_result, and stop_reason isn't end_turn, that's why this fired. - // errors[] is turn-scoped via the watermark; previously it dumped the - // entire process's logError buffer (ripgrep timeouts, ENOENT, etc). errors: (() => { const all = getInMemoryErrors() const start = errorLogWatermark diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index 9482894b6..500627029 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -684,18 +684,19 @@ export class SessionHandle { }) this.writeStdin(interrupt) + if (!this.promptResolve) return + await new Promise(resolve => { const timeout = setTimeout(() => { this.kill() resolve() - }, 5000) - const check = setInterval(() => { - if (!this.child || this.child.killed) { - clearTimeout(timeout) - clearInterval(check) - resolve() - } - }, 200) + }, 2000) + const originalResolve = this.promptResolve + this.promptResolve = (value) => { + clearTimeout(timeout) + originalResolve?.(value) + resolve() + } }) } From f34325f60afb1823d0656bbd1f40e68ec2439632 Mon Sep 17 00:00:00 2001 From: Askhz <1361267452@qq.com> Date: Sat, 9 May 2026 17:35:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=20serve=20?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=96=B0=E5=BB=BA=E4=BC=9A=E8=AF=9D=E5=8F=8A?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=AF=BB=E5=8F=96=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tsconfig.json 加 baseUrl 修复 dev 模式子进程 src/* 路径别名解析 - waitReady/initReject 修复:子进程退出时正确 reject,避免永久 pending - POST /session 等待子进程 ready 后再返回,避免立即发 prompt 崩溃 - transcriptReader pathCache 不缓存 null,文件未写入时下次可重新查找 - session/status 用 prompting 判断 busy/idle,prompt 结束后正确变为 idle - /agent 接口补充默认 build 模式 - message.ts 使用 spawnCwd 定位 transcript 文件 Co-Authored-By: Claude Opus 4.7 --- src/server/routes/info.ts | 15 ++++++++++--- src/server/routes/message.ts | 6 ++--- src/server/routes/session.ts | 11 +++++---- src/server/sessionHandle.ts | 41 ++++++++++++++++++++++++++-------- src/server/sessionManager.ts | 5 +++-- src/server/transcriptReader.ts | 7 ++++-- tsconfig.json | 2 ++ 7 files changed, 62 insertions(+), 25 deletions(-) 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/*"], From 550dbb55328cb25df30b547fd0b62d54dcdcd738 Mon Sep 17 00:00:00 2001 From: Askhz <1361267452@qq.com> Date: Sat, 9 May 2026 17:54:25 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20serve=20=E6=A8=A1=E5=BC=8F=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20resume=20=E5=8E=86=E5=8F=B2=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prompt/prompt_async 接口找不到内存 session 时自动从磁盘历史恢复 - 从 transcript 读取历史 session 的原始 cwd,保证子进程在正确目录启动 - resume 时不传 --session-id 避免与 --resume 冲突(csc 限制) - sessionManager.createSession 支持传入指定 sessionId,resume 时复用历史 id Co-Authored-By: Claude Opus 4.7 --- src/server/routes/session.ts | 46 ++++++++++++++++++++++++++++++++---- src/server/sessionHandle.ts | 5 ++-- src/server/sessionManager.ts | 3 ++- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/src/server/routes/session.ts b/src/server/routes/session.ts index 463aca009..d76e328a4 100644 --- a/src/server/routes/session.ts +++ b/src/server/routes/session.ts @@ -11,6 +11,16 @@ import { } from '../errors.js' import { listSessionsImpl } from '../../utils/listSessionsImpl.js' +/** 从磁盘历史记录查找 session 的原始 cwd,用于 resume 时恢复正确的工作目录 */ +async function getHistoryCwd(sessionId: string): Promise { + try { + const list = await listSessionsImpl({ limit: 1000 }) + return list.find(s => s.sessionId === sessionId)?.cwd + } catch { + return undefined + } +} + function ssePrompt( handle: import('../sessionHandle.js').SessionHandle, id: string, @@ -354,8 +364,22 @@ export function createSessionRoutes( }) .post('/session/:sessionID/prompt', async c => { const id = c.req.param('sessionID') - const handle = sessionManager.getSession(id) - if (!handle) throw notFound('session not found') + let handle = sessionManager.getSession(id) + if (!handle) { + const cwd = await getHistoryCwd(id) + try { + handle = await sessionManager.createSession({ + cwd, + sessionId: id, + resumeSessionId: id, + execPath: process.execPath, + scriptArgs: getScriptArgsForChild(), + }) + await handle.waitReady(30000) + } catch (err) { + throw sessionError(err instanceof Error ? err.message : 'Failed to resume session') + } + } const body = await c.req.json<{ content?: string @@ -381,9 +405,23 @@ export function createSessionRoutes( .post('/session/:sessionID/prompt_async', async c => { const id = c.req.param('sessionID') - const handle = sessionManager.getSession(id) + let handle = sessionManager.getSession(id) + + // 历史 session 不在内存中,自动恢复 if (!handle) { - throw notFound('session not found') + const cwd = await getHistoryCwd(id) + try { + handle = await sessionManager.createSession({ + cwd, + sessionId: id, + resumeSessionId: id, + execPath: process.execPath, + scriptArgs: getScriptArgsForChild(), + }) + await handle.waitReady(30000) + } catch (err) { + throw sessionError(err instanceof Error ? err.message : 'Failed to resume session') + } } const body = await c.req.json<{ diff --git a/src/server/sessionHandle.ts b/src/server/sessionHandle.ts index 315b36d2a..96c24d1cf 100644 --- a/src/server/sessionHandle.ts +++ b/src/server/sessionHandle.ts @@ -245,8 +245,8 @@ export class SessionHandle { 'stream-json', '--output-format', 'stream-json', - '--session-id', - this.sessionId, + // resume 时不传 --session-id,csc 会继承历史 session 的 id + ...(this.opts.resumeSessionId ? [] : ['--session-id', this.sessionId]), ...(this.opts.model ? ['--model', this.opts.model] : []), '--permission-mode', this._permissionMode, @@ -299,6 +299,7 @@ export class SessionHandle { const reject = this.initReject this.initResolve = null this.initReject = null + process.stderr.write(`[serve:${this.sessionId}] exit_code=${code} cwd=${this.cwd} stderr:\n${this.lastStderr.slice(-5).join('\n')}\n`) reject(new Error(`Process exited with code ${code}`)) } if (this.promptReject) { diff --git a/src/server/sessionManager.ts b/src/server/sessionManager.ts index bc2d227cb..76618f464 100644 --- a/src/server/sessionManager.ts +++ b/src/server/sessionManager.ts @@ -155,6 +155,7 @@ export class SessionManager { execPath: string scriptArgs: string[] silent?: boolean + sessionId?: string }): Promise { if (this.maxSessions > 0 && this.sessions.size >= this.maxSessions) { throw new Error( @@ -162,7 +163,7 @@ export class SessionManager { ) } - const sessionId = crypto.randomUUID() + const sessionId = opts.sessionId ?? crypto.randomUUID() let cwd = opts.cwd || this.defaultWorkspace || process.cwd() if (!isAbsolute(cwd)) { cwd = resolve(process.cwd(), cwd)