一个 Python 脚本,从 Codex 日志提取失败命令和行号
Agent 输出几百行日志,想知道哪些 shell 命令没正常结束,可以先按退出码和状态筛一遍。下面这个脚本只读本地文件,输出命令、行号和 item ID,方便回到原日志定位。
适用范围:codex exec --json 的 JSONL 事件输出,Python 3.9 及以上,不装第三方库。先跑样例不需要 Codex、账号或 API。
外层 item.completed 表示这条事件结束;命令可能成功、失败,也可能被拒绝。还要读内层 item.type、status 和 exit_code。脚本把失败、拒绝、无法确认的命令分开列出。
1. 保存为 failed_commands.py
import argparse
import json
def summarize(lines):
report = {"commands": 0, "failed": [], "declined": [], "unknown": []}
for lineno, raw in enumerate(lines, 1):
if not raw.strip():
continue
try:
event = json.loads(raw)
except json.JSONDecodeError as exc:
raise ValueError(f"line {lineno}: invalid JSON") from exc
if not isinstance(event, dict):
raise ValueError(f"line {lineno}: expected an object")
if event.get("type") != "item.completed":
continue
item = event.get("item")
if not isinstance(item, dict):
raise ValueError(f"line {lineno}: missing item object")
if item.get("type") != "command_execution":
continue
report["commands"] += 1
code, status = item.get("exit_code"), item.get("status")
if code is not None and type(code) is not int:
raise ValueError(f"line {lineno}: invalid exit_code")
if status == "declined":
bucket = "declined"
elif status == "failed" or (code is not None and code != 0):
bucket = "failed"
elif status == "completed" and code == 0:
continue
else:
bucket = "unknown"
report[bucket].append({"line": lineno, "id": item.get("id"),
"command": item.get("command"),
"exit_code": code, "status": status})
return report
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("log")
args = parser.parse_args()
try:
with open(args.log, encoding="utf-8") as stream:
report = summarize(stream)
except (OSError, ValueError) as exc:
parser.exit(2, f"{exc}\n")
print(json.dumps(report, ensure_ascii=False, indent=2)) 2. 用这三行最小样例验证
保存为 demo.jsonl。这是手工构造的事件样例,用于验证解析逻辑,不是一次真实 Codex 任务的日志。
{"type":"item.started","item":{"id":"a","type":"command_execution","command":"python -c 'raise SystemExit(2)'","aggregated_output":"","exit_code":null,"status":"in_progress"}}
{"type":"item.completed","item":{"id":"a","type":"command_execution","command":"python -c 'raise SystemExit(2)'","aggregated_output":"","exit_code":2,"status":"failed"}}
{"type":"item.completed","item":{"id":"b","type":"command_execution","command":"python -c 'print(1)'","aggregated_output":"1\n","exit_code":0,"status":"completed"}} 执行:
python3 failed_commands.py demo.jsonl 输出应该是:
{
"commands": 2,
"failed": [
{
"line": 2,
"id": "a",
"command": "python -c 'raise SystemExit(2)'",
"exit_code": 2,
"status": "failed"
}
],
"declined": [],
"unknown": []
} commands 是已结束的命令事件数,两条命令里只有第二行记录的那条需要排查;第一行的 item.started 不会再计一次。
3. 换成你的日志
已有 Codex CLI 并完成认证时,可以在自己的项目中采集一次 JSONL。下面的重定向写法用于 Bash,只把 stdout 写入文件,不要再加 2>&1 把文本错误混进去:
codex exec --json "只概述当前仓库结构,不修改文件" > run.jsonl
python3 failed_commands.py run.jsonl 也可以直接把第二条命令里的文件名换成已有日志。脚本不会执行日志里的 command 内容,也不会调用模型。
怎么处理输出
failed:回到给出的行号,查看该事件的aggregated_output。非零退出是排查线索;例如rg的退出码 1 也可能只是没找到匹配,不应一概当作程序故障。declined:命令没有按预期执行,先查对应的拒绝信息,不把它当成通过。unknown:退出码为空或状态无法确认,保留人工检查,不能把null当 0。- 遇到损坏的 JSON,程序退出码为 2,stderr 会指出行号,例如
line 2: invalid JSON。先检查日志是否截断、是否混入了非 JSON 文本。
三组列表都是空,也只说明“已结束的命令里没筛出这些情况”。它不检查命令一直没结束、重复拼接的日志、turn.failed、顶层 error,也不证明任务目标或测试已完成;这些要另看原日志和实际验收结果。只适用于上面这种事件结构,不要拿内部 session 文件或其他 Agent 日志直接套用。
样例脚本已在本地运行验证:正常退出、非零退出、failed 且退出码为空、declined、退出码无法确认、开始/更新事件、非命令事件及坏输入均已检查;没有调用真实 Codex 模型。分享原日志前记得检查命令和输出中是否有凭证或私有内容。
参考:OpenAI Codex 非交互模式文档与官方 exec 事件定义,2026 年 10 月 21 日核对。
脚本和样例下载
不想逐段复制的话,可以直接下载:
包里有脚本、三行样例、验证程序和中文说明。解压后进入目录,直接运行:
python3 failed_commands.py demo.jsonl 样例运行结果
图中是附件内固定样例的实际输出排版展示,没有调用模型。原日志第 2 行的命令退出码为 2,因此进入 failed 列表。
分类逻辑示意