Bug 描述
当 session/load 失败(如上游 API 代理返回 500 或连接中断)时,后端回退到 session/new 并无条件覆盖 conversation 的 external_id 为新的 session ID。这会导致旧的 session 文件被孤立——该文件中存储的所有对话轮次永久不可访问。
根因分析
两个代码位置共同导致此问题:
1. connection.rs:1133-1158 — session/new 回退逻辑
session/load 失败后,代码 emit 一个 error 事件,然后创建全新的 session:
if !err_str.contains("Method not found") {
emit_with_state(&state, &emitter_clone, AcpEvent::Error {
message: format!("Failed to load session, starting new: {e}"),
// ...
}).await;
}
let new_resp = cx.send_request_to(Agent, build_new_session_request(agent_type, &cwd))
.block_task().await?;
let fallback_sid = new_resp.session_id.0.to_string();
// 用 fallback_sid 发出 SessionStarted
2. lifecycle.rs:93-103 — 无条件覆盖 external_id
SessionStarted 处理器总是覆盖 external_id,没有对已有值做任何保护:
AcpEvent::SessionStarted { session_id } => {
if let Some(cid) = conversation_id {
conversation_service::update_external_id(db_conn, cid, session_id.clone()).await?;
}
}
影响
- 数据丢失:实际案例中,4 小时以上的会话日志(18:41–23:00)永久丢失。旧的 session JSONL 文件仍存在于磁盘,但不再被任何 conversation 行引用。
- 静默失败:用户看到 "Failed to load session, starting new" 错误标记,但不知道历史记录已丢失。后续 reload 显示空会话。
- 触发条件:任何瞬态上游故障(API 代理连接中断、网络超时、agent 崩溃)都可能触发。
复现步骤
- 与 AI agent 进行对话,积累多个轮次
- 使
session/load 失败(如上游 API 代理返回 500)
- 后端回退到
session/new,覆盖 external_id
- 重新加载会话——所有历史轮次消失
修复建议
方案 A(最小改动):在 lifecycle.rs 中,当 conversation 已有非空 external_id 时跳过覆盖:
AcpEvent::SessionStarted { session_id } => {
if let Some(cid) = conversation_id {
// 仅在尚未关联时设置 external_id
if conversation_service::get_external_id(db_conn, cid).await?.is_none() {
conversation_service::update_external_id(db_conn, cid, session_id.clone()).await?;
}
}
}
方案 B(更严格):回退路径不发出 SessionStarted 事件,改为发出 SessionLoadFailed 事件,由前端处理而不创建新 session。
环境
- Agent: Claude Code (claude-cli/2.1.119)
- 触发错误: 上游 API 代理返回 HTTP 500 + unexpected EOF
Bug 描述
当
session/load失败(如上游 API 代理返回 500 或连接中断)时,后端回退到session/new并无条件覆盖 conversation 的external_id为新的 session ID。这会导致旧的 session 文件被孤立——该文件中存储的所有对话轮次永久不可访问。根因分析
两个代码位置共同导致此问题:
1.
connection.rs:1133-1158— session/new 回退逻辑session/load失败后,代码 emit 一个 error 事件,然后创建全新的 session:2.
lifecycle.rs:93-103— 无条件覆盖 external_idSessionStarted处理器总是覆盖external_id,没有对已有值做任何保护:影响
复现步骤
session/load失败(如上游 API 代理返回 500)session/new,覆盖external_id修复建议
方案 A(最小改动):在
lifecycle.rs中,当 conversation 已有非空external_id时跳过覆盖:方案 B(更严格):回退路径不发出
SessionStarted事件,改为发出SessionLoadFailed事件,由前端处理而不创建新 session。环境