为什么会有 crashctl

做这个工具,起因是自己平时用 crash 实在太麻烦。

第一步就不是分析,而是把命令行拼对。 一个普通的 kdump vmcore 还好,crash vmlinux vmcore 就进去了;但一到厂商 ramdump 就变成一堆手工计算:高通要把 DDRCS*.BIN 按 load.cmm 里的地址拼成 file@addr,...,还要自己从 OCIMEM.BIN 推 KASLR 偏移、判断 vabits_actual 是 39 还是 48;SPRD 得去 dump_report.txt 里翻 memory map、phys_offset、kimage_voffset;MTK 要从 VMCOREINFO 或各种 log 里找 KASLR,再拼一条 --machdep kimage_voffset=...,phys_offset=...,max_physmem_bits=...。参数错一个,crash 要么起不来,要么起来了但符号全是错的,而这两种失败看起来都很像"dump 坏了"。这些经验每次都要重新翻一遍,写成脚本又会因为平台、内核版本、dump 布局不同而失效。

后来我写了 crash-mcp,把 crash 包成 MCP 工具接到智能体上,让它自己去执行 bt、log、struct 这些命令。方向是对的,智能体确实能顺着调用栈往下查;但很快就撞上成本问题:MCP 的工作方式是"命令 → 完整输出 → 塞回模型上下文"。一条 log 几千上万行,一次 bt -a 几十个任务,几十轮下来 token 消耗非常夸张,而这里面绝大部分内容对当前这一步推理是无关的,看过一次就再也不用了。上下文被撑爆之后,还得回头重新读,成本又翻一倍。

所以有了 crashctl。 它把这两件事一起解决:

  • 平台参数交给 intake 去识别和计算,人只需要给 dump 和 vmlinux;

  • 把"一个长驻 crash 进程 + 输出全部落盘"做进 CLI,命令只回一小段预览和一个命令 id,需要细节时再用 output / search 精确取回。

对智能体来说,这等于把上下文从"整段输出"压缩成"结论 + 引用",token 消耗降下来一个量级,同时每条结论都能追溯到 session_id / command_id 的原始输出。它不依赖 MCP,也不依赖某个特定的模型——普通 shell 脚本能调,人和智能体都能用。

下面就是这套接口的完整说明。


它是什么

crashctl 是对 crash(8) 的会话化封装:把"一个 crash 进程 + 一堆命令 + 完整输出"变成一条可以反复调用的命令行接口,适合人手工排查,也适合脚本 / Agent 调用。

下文中的 crashctl 指 skill 包里的 bin/crashctl(自带的独立二进制,不依赖宿主 Python),源码树里也可以用 scripts/crashctl 包装脚本。crash 二进制默认找 bin/crash,也可以用 --crash-bin 指定。

三个设计要点,理解它们就能理解所有命令:

  • 一个 session 对应一个长期存活的 crash 进程。 open 启动它,之后每条 run 只是通过 Unix socket 把命令送进去,不用重新加载 vmlinux / vmcore(大 dump 加载一次可能要几十秒到几分钟)。

  • 每条命令的完整输出都落盘,命令有稳定 id。 run 只回你一小段预览 + 命令 id(c0004),要看全量用 output 翻页、用 search 检索。所以长输出不会灌满终端,也不会丢。

  • 所有命令支持 --json。 成功和失败都是一行 JSON,失败带稳定的 error.code 和 hint。人用可以不加 --json,输出是简短的文本。


通用约定

全局参数

--json 和 --state-dir 可以放在子命令前后任意位置。

crashctl --json list
crashctl list --json --state-dir /data/state

状态目录

会话数据都放在状态目录下:

<state>/sessions/<sid>/
├── session.json      # 元数据:argv、pid、状态、启动参数
├── ctl.sock          # 0600 Unix socket
├── daemon.log        # daemon 自己的日志
├── transcript.log    # crash 进程的原始 PTY 转录
├── commands/cNNNN.txt  # 每条命令的完整输出
└── ledger.jsonl      # 只追加的证据账本

解析顺序:--state-dir > $CRASHCTL_STATE_DIR > $XDG_STATE_HOME/crashctl > ~/.local/state/crashctl。

目录权限是 0700。已存在的目录如果是指向别处的符号链接、不属于当前用户、或对 group/other 开放,crashctl 不会偷偷改权限,而是直接报 STATE_DIR_INSECURE 让你处理。

会话 id

  • 可以自己指定:open ... --session-id mtk-0731

  • 不指定则自动生成:<prefix>-<6 位十六进制>,例如 crashctl-a1b2c3

  • 前缀解析顺序:--session-prefix > $CRASHCTL_SESSION_PREFIX > crashctl

  • 合法字符:字母、数字、-、_,首字符必须是字母或数字,最长 48 字符(作为前缀最多 41 字符)

  • --session-id 和 --session-prefix 同时给时,--session-id 优先(前缀只在自动生成时使用)

crashctl open ... --session-prefix mtk-0731     # -> mtk-0731-9f3c21
CRASHCTL_SESSION_PREFIX=qcom crashctl open ...  # -> qcom-4d8e02

crash 二进制

查找顺序:--crash-bin > $CRASHCTL_CRASH_BIN > crashctl 同目录下的 bin/crash > PATH 里的 crash。

会话状态

starting → ready → closed
             │
             ├─→ dead    (crash 进程自己退了)
             └─→ failed  (启动就失败)

stale = 派生状态:session.json 记着 ready,但 daemon 进程已经没了
  • ready:可以 run

  • closed:不能 run,但历史输出仍可 output / search / export;可以 reopen / rename / remove

  • dead / failed / stale:可以直接 reopen(它会先把残留的 daemon / crash 进程清掉),也可以先 close 再 reopen

退出码

码

含义

0

成功

1

操作失败(会话没准备好、命令超时、参数非法等),或 doctor / intake 判定 ready: false

2

命令行用法错误(argparse 级别)

130

被 Ctrl-C 中断(加了 --json 仍会输出一个 JSON 信封)

被拒绝的 crash 命令

run 只接受单行 crash 命令,以下情况一律报 COMMAND_REJECTED,没有开关可以绕过:

  • 多行命令(含 \r / NUL)

  • 超过 4096 字符的命令

  • quit / exit / q(会结束 crash 进程)

  • 任何 shell 语法:! 开头的 shell 转义、|、>、<、反引号、$(

需要过滤输出时用 search,不要在 crash 里拼管道。


命令速查

命令

一句话

只读?

doctor

检查 crash / python / 状态目录等环境是否就绪

是

intake

校验 dump + vmlinux,打印将要执行的 crash 命令行

是

open

启动一个长驻 crash 会话

否

reopen

重启已关闭/死亡的会话,保留全部历史输出

否

run

在会话里执行一条 crash 命令

否

output

翻页查看某条命令的完整输出

是

search

对会话内所有输出做正则检索

是

list

列出会话

是(带 --prune 会清理)

status

单个会话的详情、实时运行信息、最近命令

基本只读

close

停止会话(--purge 连目录一起删)

否

export

导出证据账本(JSON / Markdown)

是(写文件)

rename

给已关闭的会话改名

否

remove

删除已关闭的会话

否


逐条详解

doctor — 环境自检

crashctl doctor --json
crashctl doctor --crash-bin /opt/crash-arm64/crash --json

检查项:python(3.10+)、platform(Linux)、procfs、pexpect、crash(路径 + 版本 + 内嵌 gdb 版本)、交叉编译的 crash-*、gdb、makedumpfile、state_dir、unix_socket。

每项是 ok / warn / fail,同时给出 message 和可选的 hint。顶层 ready 为 false 时退出码是 1。

{"ok": true, "schema": "crashctl/1", "command": "doctor",
 "data": {"ready": true, "failed": [], "warnings": ["makedumpfile"],
          "checks": [{"name": "crash", "status": "ok",
                      "message": "crash 9.0.2 at /opt/bin/crash",
                      "details": {"path": "/opt/bin/crash", "version": "9.0.2",
                                  "embedded_gdb": "16.2", "first_line": "crash 9.0.2++"}}]}}

先跑 doctor,crash 是 fail 就不要往下走了。


intake — 只做体检,不启动

crashctl intake /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl intake --dump /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl intake /dumps/qcom_dir --vmlinux /dbg/vmlinux --platform qcom --va-bits 39 --json

dump 路径既可以做位置参数,也可以用 --dump,两者同时给会报用法错误。

它会告诉你:dump 是什么格式(ELF vmcore / vendor ramdump 目录)、架构、kernel release、vmlinux 有没有调试信息、两边 release 是否匹配(确定的 mismatch 算错误,无法判断只算 warning)、厂商参数(KASLR、VA bits),以及最终打算怎么拼 crash 命令行。

关键字段:

  • ready:能不能直接 open

  • dump{format,arch,bits,endian,release,size}、vmlinux{format,arch,has_debug_info,has_symtab,release,size}

  • version_check:{dump_release, vmlinux_release, match}

  • plan{crash_args,dump_args,vmlinux,argv_tail}:将要执行的参数

  • warnings[] / errors[] / hints[]

厂商 dump 目录还会多出 detected_platform、indicators(扫到的文件)、dump_mode(single / split)和 vendor.params(KASLR、VA bits 等)。

intake 不创建会话、不需要状态目录,是一个很便宜的预检,适合在批量处理前先筛一遍。

覆盖参数:--platform auto|generic|qcom|sprd|mtk、--kaslr <hex|auto>、--va-bits 39|48。


open — 启动会话

crashctl open /dumps/vmcore --vmlinux /dbg/vmlinux --json
crashctl open --dump /dumps/vmcore --vmlinux /dbg/vmlinux --session-id my-case --json

常用参数:

参数

说明

--session-id S

自己指定 id,同名已存在报 SESSION_EXISTS

--session-prefix P

只影响自动生成的 id

--crash-bin PATH

用哪个 crash

--crash-arg ARG

追加一个原样传给 crash 的参数,可重复;不做 shell 展开

--platform / --kaslr / --va-bits

同 intake

--startup-timeout 300

等 crash 出现提示符的秒数,大 vmlinux 要调大

--idle-timeout 21600

daemon 空闲多久自杀,0 表示永不

--prompt-regex

覆盖提示符识别正则(厂商定制 crash 用)

--max-clients 8

daemon 最多同时服务几个连接(1–64)

--no-init

跳过初始化命令

--head / --tail

启动输出的预览行数,默认各 15

启动时会自动执行 set scroll off 和 gdb set debuginfod enabled off(除非 --no-init),这些记录在账本里 kind 为 init。

返回里最重要的是 session_id。startup 字段是 c0000 的 run view —— 也就是 crash 启动时自己打印的那段头部信息(KERNEL / DUMPFILE / PANIC / PID 等),经常直接就能看出 panic 原因。

启动失败不丢现场:会话目录会保留,状态记成 failed,之后可以用 reopen 重试或用 remove 删除。

  • SESSION_START_FAILED / SESSION_START_TIMEOUT 的 details.startup_output 里带 crash 自己的报错预览,details.startup_command_id 指出对应的启动记录(首次 open 一般是 c0000,reopen 后编号会顺延)

  • daemon 自己的日志在 <state>/sessions/<sid>/daemon.log


reopen — 重开会话,历史不丢

crashctl reopen s1111-33b6f9 --json
crashctl reopen s1111-33b6f9 --crash-bin /opt/crash-arm64/crash --startup-timeout 600 --json

用于 closed / dead / failed / stale 的会话。ready 状态会报 SESSION_NOT_READY(已经在跑了,直接 run 就行)。

它是原地重启:复用 session.json 里记录的 crash argv,会话目录、已存的输出、账本全部保留,命令 id 从原来的最大号继续往后排。注意重开时也会新写一条 kind: "startup" 记录和两条 init 记录,所以例子是:原来最后一条是 c0003,重开后 c0004 是启动记录、c0005/c0006 是 init、你下一条 run 拿到 c0007。返回比 open 多一个 open_count 和 warnings[]。

crashctl close s1111-33b6f9
crashctl reopen s1111-33b6f9
crashctl run s1111-33b6f9 'bt -a'

可覆盖的参数只有运行期的:--crash-bin、--startup-timeout、--idle-timeout、--max-clients、--no-init。dump / vmlinux / --crash-arg 沿用原记录。

重开前会检查记录下来的 vmlinux 是否还在,不在了直接报 PATH_NOT_FOUND 并指明是哪个文件;crash 二进制不在了则退回按默认顺序查找,并在 warnings[] 里说明。dump 文件本身不预检,路径失效会在启动阶段以 SESSION_START_FAILED 暴露,details.startup_output 里能看到 crash 自己的报错。


run — 执行一条 crash 命令

crashctl run s1111-33b6f9 'bt -a' --json
crashctl run s1111-33b6f9 log --timeout 300 --json
crashctl run --json s1111-33b6f9 -- 'foreach bt'     # -- 之后的字面都算命令

-- 之后的部分原样变成 crash 命令,所以 --json 这类选项必须写在 -- 之前,否则会被当成命令的一部分发给 crash。

参数

说明

--timeout 120

单条命令超时秒数,超时后发 Ctrl-C

--wait 30

会话正忙时最多排队等多久

--head / --tail

预览头尾各多少行,默认 15,上限 200

返回的是一个 run view:

{"session_id": "s1111-33b6f9", "command_id": "c0003", "command": "bt", "kind": "user",
 "status": "ok", "duration_s": 0.051, "lines": 7, "bytes": 421, "sha256": "…",
 "artifact": "/…/sessions/s1111-33b6f9/commands/c0003.txt",
 "preview": {"head": ["PID: 1234  TASK: ffff888003a1c000  CPU: 2  COMMAND: \"bash\"", "…"],
             "tail": [], "omitted_lines": 0},
 "parsed": {"command_type": "backtrace",
            "frames": [{"function": "machine_kexec"},
                       {"function": "__crash_kexec"},
                       {"function": "panic"},
                       {"function": "write_sysrq_trigger", "module": "sysrq_mod"}],
            "tasks": [{"pid": 1234, "comm": "bash", "cpu": 2, "task": "ffff888003a1c000"}],
            "warnings": []}}

输出很长时还会多一个 next[],直接给出下一步该敲的 output / search 命令;parsed 的 signatures(log/bt/sys)和 anchors(sys/启动头)也只在该类型命令上出现。

几个要点:

  • status: "error" 时外层仍是 ok: true、退出码 0:这表示 crash 收到了命令但拒绝了(比如 p 参数写错),属于"执行成功但结果报错"。要判断请求本身失败,看 ok / 退出码;要判断 crash 是否接受了命令,看 data.status 和 parsed.semantic_error,其中 suggested_next 通常就是 help <cmd>。

  • status: "timeout" ⇒ 外层是 ok: false、报 COMMAND_TIMEOUT,但部分输出已经存好,在 error.details.result 里(同一份 run view),会话一般还能继续用(details.recovered)。

  • status: "dead" ⇒ crash 进程死了,报 SESSION_DEAD,输出同样在 error.details.result 里保住。

  • 预览里的长行会被截到 400 字符;output 翻页时截到 4000 字符;磁盘上的 artifact 永远是完整的。

  • 同一个会话并发 run 会互相排队,超出 --wait 报 SESSION_BUSY;连接数超过 --max-clients 报 DAEMON_OVERLOADED。


output — 翻页看完整输出

crashctl output s1111-33b6f9 c0004 --json
crashctl output s1111-33b6f9 c0004 --offset 200 --limit 500 --json

参数

说明

--offset 0

从第几行开始(0 基)

--limit 200

取多少行,1–1000

返回 lines[]、offset、returned、total_lines、has_more、next_offset、truncated_long_lines。

next_offset 就是下一页该传的 --offset,has_more 为 false 时说明已经到底。读取是流式的:只读到窗口结束就停,不会把几万行的 log 整个读进内存。


search — 正则检索所有输出

crashctl search s1111-33b6f9 'BUG:|Oops|WARNING:' --json
crashctl search s1111-33b6f9 'page fault' --command c0004 --json
crashctl search s1111-33b6f9 'slab' --ignore-case --context 5 --max-matches 100 --json

参数

说明

--command CMD_ID

只搜这一条命令,可重复;默认搜会话内全部输出

--context 2

命中行前后各带几行,0–20

--max-matches 50

最多返回多少条命中,1–500

-i, --ignore-case

忽略大小写

每条命中带 command_id、line_number(1 基)、line、before[]、after[],所以可以准确引用"哪条命令第几行":

{"pattern": "BUG", "searched_commands": ["c0000", "c0001", "c0002", "c0003", "c0004", "c0005"],
 "total_matches": 1, "returned": 1, "truncated": false, "clipped_lines": 0,
 "matches": [{"command_id": "c0005", "line_number": 1501, "offset": 1500,
              "line": "[ 1500.000000] BUG: unable to handle page fault for address: 0000000000000008",
              "before": ["[ 1498.000000] fake log line 1498", "[ 1499.000000] fake log line 1499"],
              "after": ["[ 1501.000000] fake log line 1501", "[ 1502.000000] fake log line 1502"]}]}

限制(都是有意的):

  • 单行只扫前 64K 字符

  • 单行正则限时 0.25 秒,超时直接报 REGEX_TIMEOUT(提示化简,避免 (a+)+ 这类嵌套量词)

  • 单次调用总预算 20 秒,超了会返回已找到的结果并置 stopped_reason: "time_budget"

truncated: true 表示命中数超过 --max-matches 或扫描被预算截断。


list — 列出会话

crashctl list --json
crashctl list --prune --json

每项包含 session_id、status、vmcore、vmlinux、platform、daemon_pid、crash_pid、command_count 等。

command_count 是用户 run 的次数,不含启动记录 c0000 和初始化命令;账本里的全部记录数在 status 的 command_total 里。

--prune 会把 stale / dead / failed 的会话 close 掉(清理残留进程和 socket),被清理的 id 列在 pruned[]。

不加 --json 时输出三列对齐的简表:

s1111-33b6f9         closed   /dumps/vmcore
s1111-2ebc53         ready    /dumps/vmcore

status — 单个会话详情

crashctl status s1111-33b6f9 --json
crashctl status s1111-33b6f9 --history 50 --json
  • 会话摘要(同 list 的字段)+ 实际执行的 argv

  • 若状态是 ready,多一个 live:busy、current_command、uptime_s、idle_s(daemon 实时返回);连不上时会带 live_error,并把 status 修正成 stale

  • commands[]:最近 N 条命令的 command_id / command / kind / status / duration_s / lines,以及总数 command_total

想不起用过的命令 id,就查这里。


close — 停止会话

crashctl close s1111-33b6f9 --json
crashctl close s1111-33b6f9 --purge --json
crashctl close s1111-33b6f9 --grace 5 --json

先尝试优雅关闭(通过 socket 通知 daemon),不行就发信号,再不行强杀;如果 daemon 已经被 SIGKILL 导致 crash 进程成了孤儿,会按启动时间校验 pid 后一并清理(防止误杀复用了 pid 的进程)。

--purge 连会话目录一起删。

返回里的 method 说明用了哪种方式:

method

含义

graceful

daemon 正常退出

signal

通知失败,发了信号

killed

超时后强杀

stale_cleanup

会话本来就是 stale,清掉了残留 socket / 孤儿 crash

noop

本来就没在跑

close 之后会话是 closed,历史输出仍然可读。


export — 导出证据账本

crashctl export s1111-33b6f9 --format md --json
crashctl export s1111-33b6f9 --format json --out /tmp/evidence.json --json

默认写到 <session_dir>/evidence.<fmt>。Markdown 版是一张表:命令 id、类型(startup/init/user)、状态、耗时、行数、命令、sha256 前 12 位,再加会话基本信息和 vmcore/vmlinux 路径。适合直接贴进分析报告或发给同事复核。

ledger.jsonl 本身是只追加的:每条记录都有 sha256,运行期间绝不会被覆盖或重写。


rename — 给已关闭的会话改名

crashctl close qcom-201426
crashctl rename qcom-201426 mtk-a06x-0731 --json

返回:session_id、previous_session_id、status、session_dir、command_count(账本里的全部记录数)。

规则:

  • 只允许 closed 状态。ready 会报 SESSION_NOT_READY,并提示先 close;如果还有残留进程则报 SESSION_BUSY

  • 新 id 走和创建时同一套校验,非法报 INVALID_ARGUMENT;重名报 SESSION_EXISTS;和原名相同也报 INVALID_ARGUMENT

  • 实现是整个目录 os.rename,输出、账本、argv 一个字节都不动,所以改名后 output / search / reopen 全部照常工作,命令 id 继续往后排

  • 旧名字立即查不到(SESSION_NOT_FOUND);session.json 里留 renamed_from / renamed_at 便于追溯

适合把自动生成的名字改成有意义的名字,比如把工单号、机型、日期写进 id。


remove — 删除已关闭的会话

crashctl remove qcom-201426 --json

只接受 closed 状态,否则报 SESSION_NOT_READY;有残留进程报 SESSION_BUSY。这是带 rmtree 的破坏性操作,没有回收站。

返回 removed、session_dir、command_count、error_count、vmcore。这里两个计数都取自账本:command_count 是全部记录数(含启动 c0000 和 init),error_count 是其中状态非 ok 的条数。

如果只是想释放进程、还想留着证据,用 close(不加 --purge)。


典型工作流

从零到定位一次 panic

# 1. 环境体检
crashctl doctor --json

# 2. 校验输入,确认 release 匹配、vmlinux 有调试信息
crashctl intake /dumps/vmcore --vmlinux /dbg/vmlinux --json

# 3. 开会话(大 vmlinux 把启动超时放宽)
crashctl open /dumps/vmcore --vmlinux /dbg/vmlinux --startup-timeout 600 --json
#    -> session_id = s1111-33b6f9,顺手看返回里的 startup 预览

# 4. 看 panic 上下文
crashctl run s1111-33b6f9 sys --json
crashctl run s1111-33b6f9 'log' --timeout 300 --json      # 预览会给 signatures

# 5. 沿调用栈下钻
crashctl run s1111-33b6f9 'bt -a' --json
crashctl run s1111-33b6f9 'bt -f <pid>' --json

# 6. 需要看具体某段
crashctl search s1111-33b6f9 'BUG:|Oops|WARNING:' --context 3 --json
crashctl output s1111-33b6f9 c0004 --offset 1200 --limit 200 --json

# 7. 导出证据
crashctl export s1111-33b6f9 --format md --json

# 8. 收尾
crashctl close s1111-33b6f9 --json

今天关了,明天想接着看

crashctl list --json
crashctl reopen s1111-33b6f9 --json
crashctl status s1111-33b6f9 --json        # 看历史命令 id
crashctl output s1111-33b6f9 c0004 --json  # 昨天的输出照样能读

清理机器

crashctl list --prune --json                    # 清 stale/dead/failed 的残留
crashctl remove old-case-001 --json             # 删掉确实不要的
crashctl close s1111-33b6f9 --purge --json      # 边关边删

脚本里用

sid=$(crashctl open "$DUMP" --vmlinux "$VMLINUX" --json | jq -r .data.session_id)
crashctl run "$sid" 'bt -a' --json | jq -r '.data.preview.head[]'
crashctl close "$sid" --json >/dev/null

--json 保证 stdout 上恰好一行 JSON,jq 可以直接吃。异常也走 stdout 的 JSON(ok: false),同时退出码非 0,两种判错方式都可用。


错误码速查

码

含义 / 处理

USAGE_ERROR

命令行语法错误;多词 crash 命令记得加引号

INVALID_ARGUMENT

参数值不合法

INVALID_PATH / PATH_NOT_FOUND / PATH_NOT_READABLE

dump / vmlinux / crash 路径问题

INVALID_REGEX

正则编译失败

REGEX_TIMEOUT

单行匹配超 0.25 秒,化简正则或加 --command 缩小范围

STATE_DIR_INSECURE

状态目录是符号链接 / 非本人所有 / 给了 group 或 other 权限;chmod 700 或换 --state-dir

STATE_DIR_NOT_WRITABLE

状态目录建不了或写不进

DEPENDENCY_MISSING

找不到 crash 可执行文件,跑 doctor

INTAKE_UNSUPPORTED / INTAKE_AMBIGUOUS / INTAKE_INCOMPLETE

dump 识别失败,看 details.intake.errors/hints,必要时给 --platform / --kaslr / --va-bits

SESSION_NOT_FOUND / SESSION_EXISTS

id 不存在 / 重名,查 list

SESSION_NOT_READY

状态不对(例如对 ready 的会话 reopen,对 ready 的会话 rename)

SESSION_START_FAILED / SESSION_START_TIMEOUT

crash 启动失败/超时,看 details.startup_output 和 daemon.log

SESSION_BUSY

另一条命令正在跑,重试或加大 --wait

SESSION_DEAD

crash 进程退出,reopen SID 重启(历史输出保留)

SESSION_STALE

daemon 没了,reopen SID 或 close SID 清理

SESSION_CLOSED

会话已关,output / search / export 仍可用

COMMAND_TIMEOUT

命令超时,部分输出已保存,可在 details.result 里看;会话通常还能用

COMMAND_REJECTED

多行、quit、或任何 shell 语法,没有开关可绕过

COMMAND_NOT_FOUND

命令 id 不存在

DAEMON_OVERLOADED

并发连接超过 --max-clients,重试

ARTIFACT_WRITE_FAILED / LEDGER_WRITE_FAILED

磁盘满或权限;已有证据不会被覆盖,失败的命令 id 也不会被复用

DAEMON_UNREACHABLE / PROTOCOL_ERROR / INTERNAL_ERROR

看会话目录下的 daemon.log

INTERRUPTED

Ctrl-C,退出码 130


一句话总结

doctor 确认环境 → intake 验输入 → open 开会话 → run 跑命令拿 id → output / search 按需下钻 → export 出证据 → close 收尾;关掉的会话还能 reopen 接着看、rename 起个有意义的名字、remove 彻底删除。

完整 JSON schema 见 references/cli-contract.md。