Preserve interrupted provider output, keep checkpoint projections deterministic, and enforce proto snapshot synchronization.
17 KiB
name, description
| name | description |
|---|---|
| coding-guidance | 本地模式实现指南 |
当用户在处理本地模式的时候,使用此指南
先遵守这个约束:
- 不要修改已安装的 Cursor 客户端代码、bundle 或 app 副本。
- 允许且推荐读取、搜索、比对和分析客户端 bundle、日志、协议与仓库代码。
- 如果用户提到“临时 patch 客户端做 e2e”,也要改成只读排查:核对实际运行副本、采集证据、对照仓库实现,然后把修复落在本仓库代码或输出明确结论。
如果问题已经涉及以下任一事项,请同时读取 ../cursor-client-e2e-debugging/SKILL.md:
- 需要只读核对已安装的 Cursor 客户端 bundle、日志或运行副本
- 需要确认当前到底是哪一个 app 副本在运行
- 需要同时排查客户端 bundle 与本仓库 forwarder 的协同问题
- 需要对照已安装客户端行为与本仓库实现差异
本地模式协议需要优先核对这些文件:
- proto/agent_v1.proto
- proto/aiserver_v1.proto 客户端是:/Users/leokun/Library/Application\ Support/Cursor 客户端 bundle 是:/Applications/Cursor.app/Contents/Resources/app/extensions/cursor-always-local/dist/main.js
可选抓包调试工具
仓库提供了独立的 Cursor 协议抓包调试器。开发者在手动排查协议问题时,可以运行:
go run ./cmd/cursor-proxy-debugger
默认代理地址是 http://127.0.0.1:9090,调试界面是 http://127.0.0.1:9091。该工具可以辅助查看:
agent.v1.AgentService/RunSSEaiserver.v1.BidiService/BidiAppend- Connect 帧、gzip 压缩内容、Protobuf 解码结果和原始二进制数据
- 同一
request_id对应的上下行消息
开发者启动工具后,需要自行完成以下配置:
- 在 Cursor 的代理设置中,将代理修改为工具启动时显示的代理地址,默认是
http://127.0.0.1:9090。 - 在 Cursor 的 Network 设置中开启 HTTP/1.1。
- 从
http://127.0.0.1:9091/api/ca.crt下载代理 CA 证书,并确保 Cursor 信任该证书。
这只是供开发者手动使用的辅助工具,不属于自动化 Debug 流程。不要因为加载此指南就自动启动代理、修改 Cursor 或系统设置、安装证书,或操作 Cursor 发起请求。只有开发者明确表示已经启用抓包时,才把调试界面中的数据作为当前运行证据。调试结束后,提醒开发者恢复原来的 Cursor 代理和 Network 设置。
Cursor 客户端格式化快照
- 如果用户要求提取、格式化、刷新或规范化 Cursor.app 快照流程,使用
cursor-app-formattedskill。 - 如果本仓库存在
.cursor-app-formatted/,排查客户端 bundle 时优先读取这里的格式化副本。 .cursor-app-formatted/是从/Applications/Cursor.app/Contents/Resources/app只读提取后格式化生成的本地快照;它不应写回、替换或影响已安装的 Cursor.app。- 常用格式化路径:
.cursor-app-formatted/extensions/cursor-always-local/dist/main.js.cursor-app-formatted/extensions/cursor-agent-exec/dist/main.js.cursor-app-formatted/extensions/cursor-agent-worker/dist/main.js.cursor-app-formatted/out/vs/workbench/workbench.desktop.main.js.cursor-app-formatted/out/vs/workbench/api/node/extensionHostProcess.js
- 如果
.cursor-app-formatted/不存在、明显过期,或需要核对真实安装包 hash,再只读读取/Applications/Cursor.app原始 bundle。
本仓库已固定的会话承接规则
- 下一轮真实请求给 LLM 的历史承接,以
history/<conversationId>/state.json+history/<conversationId>/context.json为持久化事实源。 state.json保存会话元数据和当前状态,例如next_turn_seq、next_entry_seq、context_version、current_todos、current_plans、latest_request_prefix、last_provider_call。context.json.items保存 append-only 的语义历史 entries;provider messages 不是主存储事实,而是由ProjectPromptReplay()从 entries 投影出来。- 模型渠道唯一性不再由
modelID决定;当前规范化渠道 ID 是baseURL + modelID + apiKey + displayName + openAIEndpoint的短SHA-256hash,resolver 仍兼容 legacybaseURL + modelID + apiKey + displayName。 - 可 replay 的历史应以 entry 顺序稳定追加,不能把已发送给模型且仍需保留的历史移动到新位置。
- 最新态、易变态,例如 active todo、current plan、最新编辑保护和动态 reminder,应优先作为
state.json状态或本轮 latest-only suffix;不要无意持久化成会在后续轮次无限 replay 的历史。 - 新一轮
run_request到来时,服务端应通过LoadConversation()读取state.json + context.json,再由 projector 投影 prompt replay;客户端带回来的 checkpoint/replay 不参与历史承接真相判定。 summary.json、replay.json、runtime.json、request.json、conversation.json、entries.jsonl、turns/和数字 turn 目录都属于旧持久化产物,会被 history maintenance 当 legacy artifact 清理。- 如果发现请求历史与本地状态不一致,优先检查
context.json.items是否缺失、重复、顺序异常,以及state.json的next_entry_seq、next_turn_seq、context_version、当前状态字段是否与 entries 派生结果一致;不要再按旧summary.json路径排查。
已确认结论
1. AgentServerMessage 不是统一都要“回复完成”
要按 oneof message 分类看:
exec_server_message- 这是服务端发给客户端的“执行请求”。
- 客户端需要显式回
ExecClientMessage。 - 流式/异常场景下还会回
ExecClientControlMessage,常见是:stream_closethrowheartbeat
interaction_query- 这是服务端发给客户端的“交互请求”。
- 客户端需要显式回
InteractionResponse。
interaction_update- 这是展示/状态更新消息,通常不需要客户端回包。
conversation_checkpoint_update- 这是 checkpoint 同步消息,通常不需要客户端回包。
kv_server_message- 这是 KV 同步消息,通常不需要客户端回包。
exec_server_control_message- 这是服务端对执行桥的控制消息(例如 abort),客户端要按控制语义处理,但不是通用“完成 ack”。
2. Cursor 客户端没有“收到任意 ServerMessage 自动回 ack”的通用层
在 cursor-always-local/dist/main.js 里,BidiTransport.startYieldingInputsToTheServer 只会把“客户端主动产出的消息”送到 BidiAppend:
- 它对输入 iterable 做
p.value.toBinary()后 hex 编码,再发BidiAppendRequest.data - 说明只有客户端业务逻辑主动产出的
AgentClientMessage才会上行 - 没有发现“收到一个
AgentServerMessage就自动回 completed/ack”的统一机制
因此客户端是否回包,取决于上层业务逻辑有没有因为某个下行消息而主动构造新的 AgentClientMessage。
2.1 更具体的客户端侧结论
从 cursor-always-local/dist/main.js 里能直接确认:
AgentServerMessage的下行类型里有:interaction_updateexec_server_messageexec_server_control_messageconversation_checkpoint_updateinteraction_query
AgentClientMessage的上行类型里有:run_requestexec_client_messageexec_client_control_messageinteraction_response
这意味着本地模式不是“server message -> 通用 ack”模型,而是:
exec_server_message-> 客户端执行本地工具 -> 产出exec_client_message-> 以及可选exec_client_control_messageinteraction_query-> 客户端展示或处理交互 -> 产出interaction_response- 其他下行消息 -> 一般只更新 UI / checkpoint /流状态 -> 不会自然地产生一个“完成 ack”
2.2 exec_server_message 常见的客户端回包形态
客户端协议模型里已确认这些回包类型:
ExecClientMessage- 正常结果面
- 包括
read_result/write_result/grep_result/ls_result/diagnostics_result/mcp_result/shell_stream等
ExecClientControlMessage- 控制面
- 包括:
stream_closethrowheartbeat
所以调查本地模式 exec 问题时,不要只盯 ExecClientMessage:
- 有些工具只回一次结果面消息
- shell 之类的流式工具会混合回:
- 多次
shell_stream - 以及控制消息(例如
stream_close/heartbeat)
- 多次
2.3 exec_server_message 的完整回包形态
事实依据:
proto/agent_v1.protoExecServerMessage.oneof messageExecClientMessage.oneof messageExecClientControlMessage.oneof message
cursor-always-local/dist/main.js- bundle 内含同名 proto 模型
BidiTransport.startYieldingInputsToTheServer说明客户端上行消息来自业务逻辑主动构造,不存在通用自动 ack
结果面回包:ExecServerMessage -> ExecClientMessage
ExecServerMessage 的 message 分支与 ExecClientMessage 的 message 分支是一一对应的:
shell_args->shell_resultwrite_args->write_resultdelete_args->delete_resultgrep_args->grep_resultread_args->read_resultls_args->ls_resultdiagnostics_args->diagnostics_resultrequest_context_args->request_context_resultmcp_args->mcp_resultshell_stream_args->shell_streambackground_shell_spawn_args->background_shell_spawn_resultlist_mcp_resources_exec_args->list_mcp_resources_exec_resultread_mcp_resource_exec_args->read_mcp_resource_exec_resultfetch_args->fetch_resultrecord_screen_args->record_screen_resultcomputer_use_args->computer_use_resultwrite_shell_stdin_args->write_shell_stdin_resultexecute_hook_args->execute_hook_resultsubagent_args->subagent_result
所有这些结果面回包都带:
idexec_id
服务端匹配时通常优先用:
exec_idid
控制面回包:ExecServerMessage -> ExecClientControlMessage
除了结果面回包外,客户端还可能回控制面消息:
stream_close- 表示当前 exec 流已关闭
- 只有
id
throw- 表示执行异常
- 只有
id+error+ 可选stack_trace
heartbeat- 表示执行过程中的心跳
- 只有
id
关键理解
ExecClientControlMessage不是某个单独ExecServerMessage分支的“专属结果类型”- 它是跨 exec 通用的控制面回包
- 因此调查时必须同时看两类上行:
ExecClientMessageExecClientControlMessage
调查规则
对于任意 exec_server_message,至少要确认以下之一是否发生:
- 收到对应的
ExecClientMessage - 或收到
ExecClientControlMessage.throw - 对流式 exec,还要看:
- 是否有多次增量
ExecClientMessage - 是否最终有
stream_close
- 是否有多次增量
如果只看到 started / pending,没有任何结果面或控制面回包,服务端 pending 大概率不会收口。
排查时优先搜索这些关键字:
3. forwarder 状态机实现规则
在本仓库修本地模式 forwarder 时,默认遵守下面这些稳定约束,避免再次引入“工具晚到污染当前轮”或“同一 request 在 [DONE] 后又续跑一轮”的问题。
3.1 resume 必须按 provider pass 隔离
request_id不是 provider 调用代次;同一个 request 可以合法包含多次 provider pass。scheduleProviderResume不能只依赖 request 级布尔态(例如单个ResumePending)。- resume 请求必须带来源 pass,至少要能区分:
- 当前 pass 的结果触发的合法续跑
- 上一轮工具终态晚到造成的陈旧 resume
driveProvider开始与结束时都要显式清理上一轮的 resume 状态,不能让旧状态跨 pass 残留。
3.2 工具晚到是常态,只能影响所属 pass
ExecClientMessage/ExecClientControlMessage晚于 provider[DONE]到达是正常现象。- 晚到结果只能驱动其所属 pass 的 checkpoint / history / resume 判定,不能影响后续 pass。
- 非流式 exec 的
stream_closesynthetic recovery 也必须沿用原工具的来源 pass,不能按 request 级全局状态续跑。
3.3 pending exec 必须严格按 id 匹配
selectPendingExec/selectPendingExecByControl只允许按:exec_idmessage_id进行匹配。
- 不允许再用“当前只有一个 pending,就直接返回它”的兜底逻辑。
- 迟到的 result /
stream_close/throw如果 pending 已不存在:- 优先看
RecentCompletedExecs做幂等忽略 - 不要把它重新落到当前轮的 pending 上
- 优先看
3.4 看到这些现象时,优先怀疑 stale resume / stale exec
如果出现下面任一现象,先查 forwarder 状态机,不要先怪客户端:
- 同一个
request_id在[DONE]后又出现新的model_call_id turns/<n+1>/request.json与turns/<n>/request.jsonmessages 几乎完全相同- 上一轮工具
grepResult/readResult/...晚于上一轮[DONE] - 晚到的
stream_close恰好跨到下一轮 provider 已经启动之后
优先核对:
-
ProviderPassCount -
resume 请求的来源 pass
-
PendingExec.ProviderPass -
selectPendingExec是否存在跨轮误匹配 -
startYieldingInputsToTheServer -
bidiAppend({requestId:A,appendSeqno -
ExecServerMessage -
ExecClientMessage -
ExecClientControlMessage -
InteractionQuery -
InteractionResponse
3. 对本地模式最重要的协议理解
exec_server_message/interaction_query属于“请求型下行消息”- 如果客户端不回对应结果,服务端 pending 不会收口
- 后续重连后可能出现 “No tool output found for function call ...” 这类 provider 400
interaction_update/conversation_checkpoint_update属于“通知型下行消息”- 它们用于 UI 展示、同一 backend 进程内的 live checkpoint 同步、状态同步
- 一般不要求客户端再回一个“完成”消息
4. 调查本地模式时的优先顺序
- 先确认收到的
AgentServerMessage是哪一类 - 如果是
exec_server_message- 查客户端是否回了
ExecClientMessage - 查是否只回了
stream_close但没有真正结果 - 查
exec_id/id是否匹配
- 查客户端是否回了
- 如果是
interaction_query- 查客户端是否回了
InteractionResponse
- 查客户端是否回了
- 如果是
conversation_checkpoint_update- 重点查里面的
pending_tool_calls/root_prompt_messages_json/turns - 不要误以为它本身需要回 ack
- 重点查里面的
5. 对服务端实现的直接要求
- 服务端必须区分“请求型下行”和“通知型下行”
- 服务端不能把
ServerMessage统一建模成“发出去就等一个完成 ack” - 对请求型消息,必须在本地状态机里维护 pending:
PendingExecPendingInteraction
- 同一 backend 进程内的
RunSSE重连,要优先看 checkpoint /pending_tool_calls里的 live pending - backend 重启后,不要把 checkpoint 当持久恢复点;跨轮承接与持久恢复只看
history/<conversationId>/state.json+history/<conversationId>/context.json
5.1 checkpoint 投影必须幂等且只有一个事实源
- 把 checkpoint 当作
state.json + context.json的纯投影,不要把它写成第二套语义历史。 - 不要创建或维护
checkpoint.json、checkpoint history、独立 checkpoint entry 序列等持久化事实源。 - 允许在当前 stream 内存中保留 latest checkpoint 供 retry/resume 使用;进程重启后必须能从唯一事实源重新投影。
- 对同一份 semantic history 重复投影时,要求 state、turn 顺序、blob ID 和 blob 内容在语义上完全一致;投影函数不得修改输入 history。
- 把重复发送视为同一快照的幂等覆盖,不要追加一条新的会话历史;内容寻址 blob 的重复写入必须可安全忽略。
- 将
turns投影为 UI 可恢复的完整结构,保留所有需要展示的ThinkingMessage、ToolCall和工具结果;不要为了模型 prompt 过滤而删除 UI step。 - 将
root_prompt_messages_json单独投影为模型 replay;只在这条投影上应用 provider/context 过滤,不能反向改变turns。 - 将工具完成结果合并回同一
ToolCall,保留开始态的args、调用 ID 和开始时间,再补齐result与完成时间;不要制造协议不存在的独立ToolResultstep。 - 用 TDD 覆盖至少这些性质:重复投影相等、投影不修改 history、开始态字段在结果合并后仍存在、UI turns 保留思考/工具内容而模型 replay 仍遵守独立过滤规则。