当用户发送消息后未收到回复就关闭聊天窗口(终端/Tab),Claude Code 的处理流程。
1. 信号触发
| 平台 | 信号 | 检测方式 |
|---|---|---|
| Linux / WSL | SIGHUP |
终端关闭时内核发送 |
| macOS | TTY 吊销 | 无 SIGHUP,靠 30s 轮询检查 process.stdout.writable |
| Windows | SIGTERM |
窗口关闭事件 |
| 用户主动 Ctrl+C | SIGINT |
键盘中断 |
处理入口:gracefulShutdown.ts 的 setupGracefulShutdown()(注册于 entrypoints/init.ts:87)。
2. 取消正在进行的 API 请求
scss
SIGHUP/SIGTERM/SIGINT
→ gracefulShutdown()
→ cleanupTerminalModes() ← 恢复终端模式
→ printResumeHint() ← 打印 "Resume with: claude --resume <id>"
→ 此时 AbortController 已被触发:
queryLoop 中的 signal.aborted == true
→ anthropic.beta.messages.create({...params, stream: true}, { signal })
中的 signal 触发 → SDK 抛出 APIUserAbortError
→ stream 被取消,不再接收后续数据
关键路径:用户发送消息时 onQueryImpl 创建一个 AbortController(REPL.tsx:4010),其 signal 传入 queryModel() → 传给 Anthropic SDK 的 messages.create() 作为 abort signal。shutdown 时该 controller 被 abort,SDK 中断 HTTP 流。
3. 刷新会话持久化
typescript
// gracefulShutdown.ts:445
await runCleanupFunctions()
→ Project.flush() ← 将内存中 100ms 队列的待写入消息刷到 JSONL
→ Project.reAppendSessionMetadata() ← 重新写入 session 元数据到文件尾部
已发送的用户消息 和 已收到的部分助手回复(如果有)会被写入 JSONL。如果一条消息都没收到,则只记录用户消息。
4. 执行 SessionEnd hooks
typescript
// gracefulShutdown.ts:473
await executeSessionEndHooks(reason, { signal, timeoutMs })
允许用户配置的 SessionEnd hooks 执行(默认 1.5s 超时)。
5. failsafe 兜底
typescript
// gracefulShutdown.ts:417
failsafeTimer = setTimeout(
() => { cleanupTerminalModes(); printResumeHint(); forceExit(code); },
Math.max(5000, sessionEndTimeoutMs + 3500),
)
若上述异步清理挂起超过 5 秒(或 hook timeout + 3.5s),强制退出进程。
6. 退出后
- 打印
Resume this session with: claude --resume <sessionId>提示 - 进程 exit code = 129 (SIGHUP) / 143 (SIGTERM) / 0 (Ctrl+C)
- JSONL 文件保留在磁盘上供后续 resume
- 消息已部分写入 JSONL --- resume 后会看到已发送的消息,但回复可能不完整或不存在(取决于当时流到了哪里)
关键要点
| 问题 | 答案 |
|---|---|
| 已发送的消息会丢吗? | 不会 --- runCleanupFunctions() 会将内存队列 Flush 到 JSONL |
| 已收到的部分回复会保存吗? | 会 --- 已 yield 的 assistant message 片段已写入 |
| API 会继续处理吗? | 不会 --- abort signal 取消后,API 收到 TCP 断开继续处理但结果被丢弃 |
| 能恢复会话继续吗? | 可以 --- claude --resume <sessionId> 重新加载 JSONL 恢复上下文继续对话 |