feat: serve 模式支持 resume 历史会话继续对话
- prompt/prompt_async 接口找不到内存 session 时自动从磁盘历史恢复 - 从 transcript 读取历史 session 的原始 cwd,保证子进程在正确目录启动 - resume 时不传 --session-id 避免与 --resume 冲突(csc 限制) - sessionManager.createSession 支持传入指定 sessionId,resume 时复用历史 id Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
f34325f60a
commit
550dbb5532
|
|
@ -11,6 +11,16 @@ import {
|
||||||
} from '../errors.js'
|
} from '../errors.js'
|
||||||
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
import { listSessionsImpl } from '../../utils/listSessionsImpl.js'
|
||||||
|
|
||||||
|
/** 从磁盘历史记录查找 session 的原始 cwd,用于 resume 时恢复正确的工作目录 */
|
||||||
|
async function getHistoryCwd(sessionId: string): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const list = await listSessionsImpl({ limit: 1000 })
|
||||||
|
return list.find(s => s.sessionId === sessionId)?.cwd
|
||||||
|
} catch {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ssePrompt(
|
function ssePrompt(
|
||||||
handle: import('../sessionHandle.js').SessionHandle,
|
handle: import('../sessionHandle.js').SessionHandle,
|
||||||
id: string,
|
id: string,
|
||||||
|
|
@ -354,8 +364,22 @@ export function createSessionRoutes(
|
||||||
})
|
})
|
||||||
.post('/session/:sessionID/prompt', async c => {
|
.post('/session/:sessionID/prompt', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
const handle = sessionManager.getSession(id)
|
let handle = sessionManager.getSession(id)
|
||||||
if (!handle) throw notFound('session not found')
|
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<{
|
const body = await c.req.json<{
|
||||||
content?: string
|
content?: string
|
||||||
|
|
@ -381,9 +405,23 @@ export function createSessionRoutes(
|
||||||
.post('/session/:sessionID/prompt_async', async c => {
|
.post('/session/:sessionID/prompt_async', async c => {
|
||||||
const id = c.req.param('sessionID')
|
const id = c.req.param('sessionID')
|
||||||
|
|
||||||
const handle = sessionManager.getSession(id)
|
let handle = sessionManager.getSession(id)
|
||||||
|
|
||||||
|
// 历史 session 不在内存中,自动恢复
|
||||||
if (!handle) {
|
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<{
|
const body = await c.req.json<{
|
||||||
|
|
|
||||||
|
|
@ -245,8 +245,8 @@ export class SessionHandle {
|
||||||
'stream-json',
|
'stream-json',
|
||||||
'--output-format',
|
'--output-format',
|
||||||
'stream-json',
|
'stream-json',
|
||||||
'--session-id',
|
// resume 时不传 --session-id,csc 会继承历史 session 的 id
|
||||||
this.sessionId,
|
...(this.opts.resumeSessionId ? [] : ['--session-id', this.sessionId]),
|
||||||
...(this.opts.model ? ['--model', this.opts.model] : []),
|
...(this.opts.model ? ['--model', this.opts.model] : []),
|
||||||
'--permission-mode',
|
'--permission-mode',
|
||||||
this._permissionMode,
|
this._permissionMode,
|
||||||
|
|
@ -299,6 +299,7 @@ export class SessionHandle {
|
||||||
const reject = this.initReject
|
const reject = this.initReject
|
||||||
this.initResolve = null
|
this.initResolve = null
|
||||||
this.initReject = 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}`))
|
reject(new Error(`Process exited with code ${code}`))
|
||||||
}
|
}
|
||||||
if (this.promptReject) {
|
if (this.promptReject) {
|
||||||
|
|
|
||||||
|
|
@ -155,6 +155,7 @@ export class SessionManager {
|
||||||
execPath: string
|
execPath: string
|
||||||
scriptArgs: string[]
|
scriptArgs: string[]
|
||||||
silent?: boolean
|
silent?: boolean
|
||||||
|
sessionId?: string
|
||||||
}): Promise<SessionHandle> {
|
}): Promise<SessionHandle> {
|
||||||
if (this.maxSessions > 0 && this.sessions.size >= this.maxSessions) {
|
if (this.maxSessions > 0 && this.sessions.size >= this.maxSessions) {
|
||||||
throw new Error(
|
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()
|
let cwd = opts.cwd || this.defaultWorkspace || process.cwd()
|
||||||
if (!isAbsolute(cwd)) {
|
if (!isAbsolute(cwd)) {
|
||||||
cwd = resolve(process.cwd(), cwd)
|
cwd = resolve(process.cwd(), cwd)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user