mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
feat: 实现 ACP session/delete + message-id 两个 UNSTABLE RFD
session/delete(rfds/session-delete.mdx):
- sessionCapabilities.delete: {} 能力广告(类型增强写入,SDK 0.19.0 早于该 RFD)
- extMethod 钩子路由 session/delete → unstable_deleteSession
- 硬删除 .jsonl 文件,ENOENT 视为成功(幂等)
- 未知方法抛 RequestError.methodNotFound(JSON-RPC -32601)
message-id(rfds/message-id.mdx):
- agent_message_chunk / user_message_chunk / agent_thought_chunk 携带 messageId
- forwardSessionUpdates 维护 currentAgentMessageId,lazy 生成 UUID
- streaming text/thinking chunks 与最终 assistant message 共享同一 ID
- replayHistoryMessages per-message 生成 UUID
- PromptRequest.messageId → PromptResponse.userMessageId 回显
- tool_call / plan / subagent 不带 messageId(spec 仅规定 chunk 类型)
测试:ACP service 从 176 → 191 (+15)
- bridge.test.ts: +9 个 message-id 测试
- agent.test.ts: +6 个 session/delete + userMessageId 测试
- 总测试 5851 → 5866,全通过
审计文档:新增附录 A.2 记录两个 UNSTABLE RFD 实现状态
Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
@@ -14,39 +14,42 @@
|
||||
* `./index.js` imports those side-effect modules so the prototype is fully
|
||||
* populated before any AcpAgent instance is constructed.
|
||||
*/
|
||||
import type {
|
||||
Agent,
|
||||
AgentSideConnection,
|
||||
InitializeRequest,
|
||||
InitializeResponse,
|
||||
AuthenticateRequest,
|
||||
AuthenticateResponse,
|
||||
NewSessionRequest,
|
||||
NewSessionResponse,
|
||||
PromptRequest,
|
||||
PromptResponse,
|
||||
CancelNotification,
|
||||
LoadSessionRequest,
|
||||
LoadSessionResponse,
|
||||
ListSessionsRequest,
|
||||
ListSessionsResponse,
|
||||
ResumeSessionRequest,
|
||||
ResumeSessionResponse,
|
||||
ForkSessionRequest,
|
||||
ForkSessionResponse,
|
||||
CloseSessionRequest,
|
||||
CloseSessionResponse,
|
||||
SetSessionModeRequest,
|
||||
SetSessionModeResponse,
|
||||
SetSessionModelRequest,
|
||||
SetSessionModelResponse,
|
||||
SetSessionConfigOptionRequest,
|
||||
SetSessionConfigOptionResponse,
|
||||
ClientCapabilities,
|
||||
import {
|
||||
RequestError,
|
||||
type Agent,
|
||||
type AgentSideConnection,
|
||||
type InitializeRequest,
|
||||
type InitializeResponse,
|
||||
type AuthenticateRequest,
|
||||
type AuthenticateResponse,
|
||||
type NewSessionRequest,
|
||||
type NewSessionResponse,
|
||||
type PromptRequest,
|
||||
type PromptResponse,
|
||||
type CancelNotification,
|
||||
type LoadSessionRequest,
|
||||
type LoadSessionResponse,
|
||||
type ListSessionsRequest,
|
||||
type ListSessionsResponse,
|
||||
type ResumeSessionRequest,
|
||||
type ResumeSessionResponse,
|
||||
type ForkSessionRequest,
|
||||
type ForkSessionResponse,
|
||||
type CloseSessionRequest,
|
||||
type CloseSessionResponse,
|
||||
type SetSessionModeRequest,
|
||||
type SetSessionModeResponse,
|
||||
type SetSessionModelRequest,
|
||||
type SetSessionModelResponse,
|
||||
type SetSessionConfigOptionRequest,
|
||||
type SetSessionConfigOptionResponse,
|
||||
type ClientCapabilities,
|
||||
} from '@agentclientprotocol/sdk'
|
||||
import { unlink } from 'node:fs/promises'
|
||||
import type { Message } from '../../../types/message.js'
|
||||
import { sanitizeTitle } from '../utils.js'
|
||||
import { listSessionsImpl } from '../../../utils/listSessionsImpl.js'
|
||||
import { resolveSessionFilePath } from '../../../utils/sessionStoragePortable.js'
|
||||
import type { AcpSession } from './sessionTypes.js'
|
||||
|
||||
// ── Agent class ───────────────────────────────────────────────────
|
||||
@@ -123,6 +126,11 @@ export class AcpAgent implements Agent {
|
||||
list: {},
|
||||
resume: {},
|
||||
close: {},
|
||||
// UNSTABLE per session-delete.mdx: capability-gated session/delete.
|
||||
// SDK 0.19.0's SessionCapabilities type predates this field — clients
|
||||
// implementing the RFD read `sessionCapabilities.delete`, so we
|
||||
// advertise it at the standard path via type augmentation.
|
||||
...({ delete: {} } as { delete: Record<string, never> }),
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -236,6 +244,51 @@ export class AcpAgent implements Agent {
|
||||
return {}
|
||||
}
|
||||
|
||||
// ── deleteSession (UNSTABLE, routed via extMethod) ──────────────
|
||||
|
||||
async unstable_deleteSession(params: {
|
||||
sessionId: string
|
||||
}): Promise<Record<string, never>> {
|
||||
// Per session-delete.mdx §Semantics: idempotent — deleting a session
|
||||
// that doesn't exist (or was already deleted) MUST succeed silently.
|
||||
const resolved = await resolveSessionFilePath(params.sessionId)
|
||||
if (resolved) {
|
||||
try {
|
||||
await unlink(resolved.filePath)
|
||||
} catch (err) {
|
||||
// ENOENT is fine — file was concurrently removed. Any other error
|
||||
// (EACCES, EISDIR, ...) we propagate.
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err
|
||||
}
|
||||
}
|
||||
// Tear down in-memory session if present (e.g., session was active in
|
||||
// another connection). teardownSession is a no-op if not loaded.
|
||||
if (this.sessions.has(params.sessionId)) {
|
||||
await this.teardownSession(params.sessionId)
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
// ── extMethod (UNSTABLE method dispatch) ────────────────────────
|
||||
|
||||
async extMethod(
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
// SDK 0.19.0 routes unknown methods here (acp.js:139 default branch).
|
||||
// We surface UNSTABLE capabilities that the SDK hasn't typed yet.
|
||||
if (method === 'session/delete') {
|
||||
const sessionId = params.sessionId
|
||||
if (typeof sessionId !== 'string' || sessionId.length === 0) {
|
||||
throw new Error('session/delete requires a non-empty sessionId')
|
||||
}
|
||||
return this.unstable_deleteSession({ sessionId })
|
||||
}
|
||||
// Unknown method — surface as JSON-RPC methodNotFound so clients see a
|
||||
// standard error code (-32601) rather than a generic internal error.
|
||||
throw RequestError.methodNotFound(method)
|
||||
}
|
||||
|
||||
// ── cancel ────────────────────────────────────────────────────
|
||||
|
||||
async cancel(params: CancelNotification): Promise<void> {
|
||||
|
||||
@@ -43,6 +43,12 @@ async function prompt(
|
||||
throw new Error(`Session ${params.sessionId} not found`)
|
||||
}
|
||||
|
||||
// Per message-id.mdx RFD: if the client supplied a `messageId` on the
|
||||
// PromptRequest, echo it back as `userMessageId` to confirm receipt.
|
||||
// We do not self-generate when omitted — the spec makes that optional and
|
||||
// staying quiet avoids surfacing IDs the client didn't ask to track.
|
||||
const userMessageId = params.messageId ?? undefined
|
||||
|
||||
// Extract text/image content from the prompt
|
||||
const promptInput = promptToQueryInput(params.prompt)
|
||||
|
||||
@@ -134,6 +140,7 @@ async function prompt(
|
||||
return {
|
||||
stopReason,
|
||||
usage: usagePayload,
|
||||
...(userMessageId ? { userMessageId } : {}),
|
||||
_meta: {
|
||||
claudeCode: {
|
||||
usage: usagePayload,
|
||||
@@ -141,7 +148,10 @@ async function prompt(
|
||||
},
|
||||
}
|
||||
}
|
||||
return { stopReason }
|
||||
return {
|
||||
stopReason,
|
||||
...(userMessageId ? { userMessageId } : {}),
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
// Treat AbortError / cancellation-shaped errors as a turn cancellation
|
||||
// regardless of the session.cancelled flag, to close the race window
|
||||
|
||||
Reference in New Issue
Block a user