feat: 修复 serve 模式新建会话及消息读取问题
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
265ff7d864
commit
f34325f60a
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ReturnType<typeof listSessionsImpl>> = []
|
||||
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',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -63,10 +63,13 @@ async function resolveTranscriptPath(
|
|||
): Promise<string | null> {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/*"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user