<!-- title: 会话续跑：造可重启自主代理 -->

## 概述

本教程解决一个真实痛点：用 `claude -p` 跑一个长任务（重构整个模块、批量改文档、生成测试套件），跑到一半限流了、CI 超时了、或者你手滑 Ctrl+C 了——上下文全丢，只能从头再来。我们将用**会话续跑（`--resume`/`--continue`）+ 流式输出解析（`stream-json`）**搭一个"可重启、可观测"的自主代理运行器：每次运行自动捕获 `session_id`，失败后重跑命令即可从断点续上，全程实时打印进度、工具调用与限流重试事件。适合做夜间批处理、CI 长任务、大型重构的工程师。

## 前置条件

- **Claude Code v2.1.x**：流式事件、`--json-schema`、`--bare`、`--fork-session` 均需较新版本（下文会标注具体小版本）。
- **`jq`**：解析 JSON 输出。`sudo apt install jq` 或 `brew install jq`。
- **Python 3.8+**：用于流式事件解析器（纯标准库，无需装包）。
- **Bash**：运行器脚本。
- **`ANTHROPIC_API_KEY`**：本教程默认用 `--bare` 模式，它跳过 OAuth 与钥匙串读取，鉴权必须来自环境变量。

```bash
export ANTHROPIC_API_KEY=sk-ant-...
claude --version   # 确认版本
```

---

## 第一部分：先搞懂"会话"是什么

Claude Code 把每一次对话存成一条**会话（session）**，绑定到当前项目目录，随着你工作**持续写入本地 transcript 文件**。会话有两层价值，本教程吃的是第二层：

1. **交互层**：`/clear` 后还能 `/resume` 找回来；
2. **脚本层**：`claude -p` 跑的非交互任务也有 `session_id`，可以被同一条命令再次 `--resume`。

关键事实（官方文档原文）：

> Sessions created with `claude -p` or the Agent SDK do not appear in the session picker, but you can still resume one by passing its session ID to `claude --resume <session-id>`.

也就是说，`-p` 模式的会话**不会出现在选择器里**，但只要你有 `session_id`，照样能续。这正是构建运行器的基础。

transcript 落盘位置（了解即可，**不建议直接解析**，格式随版本变）：

```
~/.claude/projects/<工作目录路径-非字母数字替换为->/<session-id>.jsonl
```

默认保留 30 天，可用 `settings.json` 里的 `cleanupPeriodDays` 改。

---

## 第二部分：三个续跑入口

| 命令 | 作用 | 适用场景 |
| :--- | :--- | :--- |
| `claude --continue` | 续跑**当前目录最近一条**会话 | 交互式"接着干" |
| `claude --resume` | 打开会话选择器 | 人工挑历史会话 |
| `claude --resume <id或名字>` | 按 ID/名字**直接续** | **脚本自动化（本教程核心）** |
| `claude --from-pr <url>` | 续跑绑定到某个 PR 的会话 | PR 机器人 |

⚠️ **作用域陷阱**：`--resume <id>` 的查找**只限当前项目目录及其 git worktree**。在 A 目录创建的会话，到 B 目录 `--resume` 会报 `No conversation found with session ID`。所以运行器脚本必须**每次在同一目录执行**——把状态文件放在项目根，配合固定的工作目录即可避免。

### 步骤 1：给会话起名字

长任务并行时，命名是找回会话的最快方式：

```bash
# 启动时命名
claude -n auth-refactor

# 会话内改名
/rename auth-refactor

# 选择器里高亮后按 Ctrl+R
```

命名后，`claude --resume auth-refactor` 比 `--resume abc123...` 可读得多。

**预期结果**：`--resume <name>` 命中唯一会话则直接进入；名字冲突时打开选择器并预填搜索词。

**常见问题**：名字在多 worktree 间重复会变"歧义"，选择器会要求你二选一。

---

## 第三部分：从 JSON 输出里"捞"出 session_id

续跑的前提是先拿到 ID。`--output-format json` 会把结果包成结构化对象，含 `result`、`session_id`、`total_cost_usd`、`usage` 等：

```bash
claude -p "审查这个代码库的性能问题" --output-format json > run.json
jq '{session_id, cost: .total_cost_usd, result: .result[0:80]}' run.json
```

**预期输出**：

```json
{
  "session_id": "a1b2c3d4-...",
  "cost": 0.0123,
  "result": "发现 3 个 N+1 查询，集中在 user_service.py..."
}
```

一行捕获，供后续命令复用：

```bash
session_id=$(claude -p "开始一次审查" --output-format json | jq -r '.session_id')
echo "拿到会话: $session_id"
```

**常见问题**：忘记加 `--output-format json` 时，`session_id` 字段不存在，`jq` 返回 `null`，下一步 `--resume null` 直接报错。养成"先 `--output-format json` 兜底拿 ID"的习惯。

### 步骤 2：续跑同一条会话

拿到 `session_id` 后，发后续 prompt 时带上它，Claude 就**带着完整上下文继续**：

```bash
# 第一条
session_id=$(claude -p "审查这个代码库的性能问题" --output-format json | jq -r '.session_id')

# 续问 1
claude -p "现在重点看数据库查询"           --resume "$session_id"
# 续问 2
claude -p "汇总所有发现的问题"            --resume "$session_id"
```

注意：**续跑的是"会话状态"，不是"工作区文件"**——文件改动在磁盘上，本来就在；`--resume` 恢复的是对话记忆。所以"跑了一半挂掉 → 重跑续上"这条链路是成立的。

---

## 第四部分：用 stream-json 做"可观测性"

`json` 模式只在**结尾**给你一个对象。长任务跑 10 分钟，中间发生什么一无所知。`stream-json` 是**逐行换行分隔的 JSON 事件流**，每个事件一行，能实时看到模型在想什么、在调什么工具、是否在限流重试。

```bash
claude -p "解释递归" \
  --output-format stream-json \
  --verbose \
  --include-partial-messages
```

`--include-partial-messages` 让 token **逐字**吐出（不加它只有整段消息）。三类高价值事件：

| 事件类型 | 关键字段 | 用途 |
| :--- | :--- | :--- |
| `system` / `init` | `session_id`, `model`, `tools`, `mcp_servers`, `plugins` | **一开头就拿到 session_id**，不用等结尾 |
| `system` / `api_retry` | `attempt`, `max_retries`, `retry_delay_ms`, `error_status`, `error` | 限流/过载时**主动告警 + 自定义退避** |
| `result` | `session_id`, `total_cost_usd`, `is_error` | 权威的最终状态与 `session_id` |

`api_retry` 的 `error` 字段会是这些类别之一：`rate_limit`、`overloaded`、`authentication_failed`、`billing_error`、`invalid_request`、`model_not_found`、`server_error`、`max_output_tokens`、`unknown`。

用 `jq` 只看流式文本（`-r` 去引号、`-j` 不加换行，让 token 连续）：

```bash
claude -p "写一首关于调试的诗" \
  --output-format stream-json --verbose --include-partial-messages | \
  jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'
```

**版本注意**：`--input-format stream-json`（反向把事件喂回 Claude、做双向程序化控制）目前官方文档说明不全（见 GitHub issue #24594 / #44911）。CLI 单向消费用 `--output-format stream-json` 已足够；需要双向回调请用 Agent SDK（Python/TS）。

---

## 第五部分：拿结构化结果（--json-schema）

除了自然语言，还能强制 Claude 按 JSON Schema 吐结果，落在 `structured_output` 字段：

```bash
claude -p "提取 auth.py 里的主函数名" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'
```

**预期输出**：

```json
{"functions": ["login", "verify_token", "refresh_session"]}
```

运行器里常用这招：让代理**自己产出"任务是否完成 / 还差什么"的结构化结论**，脚本据此决定要不要续跑下一轮。

---

## 第六部分：分支会话做 A/B 方案对比

想在不污染主线的前提下试另一种思路？**分支（fork）**复制一份对话副本，原会话原封不动：

```bash
# 命令行：续跑 + 分支
claude --resume "$session_id" --fork-session

# 会话内
/branch try-streaming-approach
```

`/branch` 会打印两个 ID：你正进入的新分支、和留在原地的原始会话。注意：**"仅本次会话允许"的权限不会带进新分支**；不 fork 而在两个终端 `--resume` 同一会话，消息会**交错进同一条 transcript**，避免这么做。

---

## 实战案例：可重启自主代理运行器

把上面拼成一套**真正能跑**的工程。目标：跑一个长任务，崩了重跑能续上，全程可观测，还能记录成本。

目录结构：

```
my-project/
├─ run.sh            # 运行器
├─ stream_parse.py   # 流式解析器
└─ .agent_state.json # 上次会话状态（自动生成）
```

### 文件 1：`stream_parse.py`（流式事件解析器）

逐行读 `stream-json`，打印实时进度，把最终 `session_id`/成本/是否出错写到状态文件。

```python
#!/usr/bin/env python3
# 读 claude 的 stream-json，打印实时进度，并把最终状态写盘
import sys, json

state_path = sys.argv[1] if len(sys.argv) > 1 else None
session_id, cost, is_error = None, None, False

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    try:
        evt = json.loads(line)
    except json.JSONDecodeError:
        continue

    t = evt.get("type")

    # 1) 开场即拿到 session_id、模型、加载的工具/MCP/插件
    if t == "system" and evt.get("subtype") == "init":
        session_id = evt.get("session_id")
        print(f"[init] session={session_id} model={evt.get('model')} "
              f"tools={len(evt.get('tools', []))}", flush=True)

    # 2) 限流/过载重试：可据此发告警或自定义退避
    elif t == "system" and evt.get("subtype") == "api_retry":
        print(f"[retry] 第{evt.get('attempt')}/{evt.get('max_retries')}次 "
              f"{evt.get('error')} (http {evt.get('error_status')}) "
              f"等待 {evt.get('retry_delay_ms')}ms", flush=True)

    # 3) 助手消息：只打印工具调用（文本已由下面的 delta 实时打印）
    elif t == "assistant":
        for c in evt.get("message", {}).get("content", []):
            if c.get("type") == "tool_use":
                print(f"\n[tool] {c.get('name')} -> " +
                      json.dumps(c.get("input", {}), ensure_ascii=False)[:120],
                      flush=True)

    # 4) 逐 token 文本流（需 --include-partial-messages）
    elif t == "stream_event":
        d = evt.get("event", {}).get("delta", {})
        if d.get("type") == "text_delta":
            sys.stdout.write(d.get("text", ""))
            sys.stdout.flush()

    # 5) 权威收尾：最终 session_id / 成本 / 是否出错
    elif t == "result":
        session_id = evt.get("session_id")
        cost = evt.get("total_cost_usd")
        is_error = evt.get("is_error", False)

print(f"\n[done] session={session_id} cost=${cost} error={is_error}", flush=True)

if state_path:
    with open(state_path, "w") as f:
        json.dump({"session_id": session_id, "cost": cost,
                   "is_error": is_error}, f)

sys.exit(1 if is_error else 0)   # 退出码驱动运行器
```

**设计要点**：`session_id` 既在 `init` 又在 `result` 里取（后者更权威），保证即使中途崩了 `init` 也兜住。

### 文件 2：`run.sh`（运行器）

```bash
#!/usr/bin/env bash
set -uo pipefail

PROMPT="${1:?用法: ./run.sh \"<任务>\" [额外 claude 参数]}"
STATE=".agent_state.json"
RESUME=""

# 有上次会话就续跑，没有就新开
if [[ -f "$STATE" ]] && SID=$(jq -r '.session_id // empty' "$STATE") && [[ -n "$SID" ]]; then
  echo "↩  续跑会话 $SID"
  RESUME="--resume $SID"
else
  echo "✦  新建会话"
fi

# --bare：跳过 hooks/skills/plugins/MCP/CLAUDE.md，保证多机一致（官方推荐脚本默认）
# 注意 bare 跳过 OAuth，鉴权须来自 ANTHROPIC_API_KEY
# shellcheck disable=SC2086
claude --bare -p "$PROMPT" $RESUME \
  --output-format stream-json \
  --verbose \
  --include-partial-messages \
  --permission-mode acceptEdits \
  ${2:-} \
  | python3 stream_parse.py "$STATE"

code=$?
if (( code != 0 )); then
  echo "✗ 失败(退出码 $code)。状态已存于 $STATE，重跑本命令即可从断点续上。"
  exit 1
fi
echo "✓ 完成。会话 $(jq -r '.session_id' "$STATE") 已记录，累计成本 \$$(jq -r '.cost' "$STATE")。"
```

### 跑起来

```bash
chmod +x run.sh stream_parse.py

# 第一次：新开会话
./run.sh "把 src/ 下所有 console.log 换成项目统一的 logger，并跑一遍测试"

# 中途被限流/超时/你按了 Ctrl+C —— 状态已落盘，直接重跑：
./run.sh "继续刚才的任务"
```

第二次执行时，`run.sh` 读到 `.agent_state.json` 里的 `session_id`，自动加 `--resume`，Claude 带着**完整上下文**从断点继续。`stream_parse.py` 在终端实时打印 `[tool] Edit -> ...`、`[retry] 第2/10次 rate_limit ...`，你看得见代理在干什么、卡在哪。

### 关键权衡与陷阱

- **`--bare` vs 非 bare**：bare 是脚本/SDK 的官方推荐模式，快且确定（队友 `~/.claude` 里的 hook、项目 `.mcp.json` 都不会偷偷跑）。代价是不自动加载 CLAUDE.md/技能。需要项目上下文时，要么在 prompt 里显式说明，要么用 `--settings`/`--append-system-prompt` 注入，要么去掉 `--bare`。
- **`acceptEdits` 的边界**：它放行文件写入和 `mkdir/touch/mv/cp` 等，但**其它 shell 命令和网络请求仍需 `--allowedTools` 或 `permissions.allow`**，否则一执行就中止。需要 `npm test` 这类，补 `--allowedTools "Bash(npm *)"`.
- **后台任务会被回收**：`claude -p` 里若起了 dev server，Claude 返回后约 **5 秒**宽限期就杀掉该 shell。v2.1.182 起对卡住的子代理等待上限默认 **10 分钟**，用 `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 调。运行器若依赖后台进程，别指望它跨次续跑存活。
- **stdin 上限 10MB**（v2.1.128+）：超大输入别用管道，写文件后在 prompt 里给路径。
- **会话不是免费的**：续跑会重放上下文，超长任务注意 `total_cost_usd` 累计，必要时 `/compact` 后再续。

---

## 工作流程图

```mermaid
flowchart TD
    A["运行 ./run.sh '任务'"] --> B{".agent_state.json 存在?"}
    B -- 是 --> C["读取上次 session_id"]
    C --> D["claude --resume SID 续跑"]
    B -- 否 --> E["claude 新开会话"]
    D --> F["stream-json 实时事件流"]
    E --> F
    F --> G["stream_parse.py 解析"]
    G --> H["打印 init/tool/retry/文本"]
    H --> I["捕获 result 事件"]
    I --> J["写 session_id 到状态文件"]
    J --> K{"is_error?"}
    K -- 是 --> L["退出非 0，等待重跑"]
    K -- 否 --> M["✓ 完成"]
    L --> B
```

整条链路的精髓：**状态文件 `.agent_state.json` 是断点续跑的"存档点"**，失败不丢上下文，重跑即可续，闭环。

---

## 延伸阅读

- **Agent SDK**：当 CLI 单向流不够、需要工具审批回调与原生消息对象时，换 Python/TS SDK 做双向程序化驱动。
- **Checkpointing**：会话内的代码+对话快照回退，比 `--resume` 粒度更细。
- **Worktrees**：在独立分支上跑隔离的并行会话，和本教程的续跑正交组合。
- **GitHub Actions / GitLab CI**：把运行器塞进流水线，PR 触发即续跑绑定的会话（`--from-pr`）。

---

## 训练营适用性评估

| 维度 | 评估 |
| :--- | :--- |
| 适合训练营 | DevOps / SRE 自动化营、AI 工程实战营、CI/CD 进阶营 |
| 学员基础 | 已会用 `claude -p` 跑过单次任务，懂 Bash 与 JSON |
| 教学时长 | 2.5–3 小时（含动手搭建运行器 + 触发一次限流重跑续跑） |
| 难点 | stream-json 事件类型识别、bare 模式鉴权、permission-mode 边界 |
| 验收标准 | 学员能演示"任务跑到一半被中断 → 重跑命令 → 代理从断点带上下文续上"，并能从日志指出至少一次 retry 事件 |

---

## 参考来源

1. [Manage sessions — Claude Code Docs](https://code.claude.com/docs/en/sessions) — 官方会话管理，覆盖 `--continue`/`--resume`/`--fork-session`/命名/transcript 落盘。
2. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方 Headless 文档，覆盖 `-p`/`--bare`/`--output-format`/`--json-schema`/续跑与权限模式。
3. [Claude Code --continue and --resume: Never Lose Your Context Again](https://pasqualepillitteri.it/en/news/366/claude-code-continue-resume-guide) — 续跑机制的完整实战指南。
4. [Claude Code stream-json: the output format that changes everything](https://backgroundclaude.com/blog/stream-json) — 深入解析 `stream-json` 事件流与程序化消费。
5. [Mastering Claude Code Sessions: -continue & -resume](https://aiopsschool.com/blog/mastering-claude-code-sessions-continue-resume/) — 含 4 个捕获 session ID 传给 `--resume` 的 CI/CD 脚本套路。
6. [GitHub Issue #24594: `--input-format stream-json` undocumented](https://github.com/anthropics/claude-code/issues/24594) — 双向程序化驱动的文档缺口说明，提示何时该转用 Agent SDK。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
