From 01416d2d39d66dedc0dbb176876d0918ad698448 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 16 Jun 2026 03:49:56 +0000 Subject: [PATCH] feat: improve codex trace filtering --- .agents/skills/unidesk-code-queue/SKILL.md | 7 +- scripts/src/codex-trace.ts | 656 +++++++++++++++++++-- scripts/src/help.ts | 9 +- 3 files changed, 619 insertions(+), 53 deletions(-) diff --git a/.agents/skills/unidesk-code-queue/SKILL.md b/.agents/skills/unidesk-code-queue/SKILL.md index 114c6ff0..1ff4a95c 100644 --- a/.agents/skills/unidesk-code-queue/SKILL.md +++ b/.agents/skills/unidesk-code-queue/SKILL.md @@ -94,12 +94,15 @@ HWLAB Code Agent / CaseRun follow-up 的日常派单也归入 AgentRun 资源原 ```bash bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30] bun scripts/cli.ts codex trace active [--root ~/.codex] +bun scripts/cli.ts codex trace grep --session --pattern 'playwright|auth-login-failed' [--since ISO] +bun scripts/cli.ts codex trace grep --session --messages --pattern 'Playwright|web-probe' +bun scripts/cli.ts codex trace grep --session --failed-only [--tool exec_command] bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO] bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/] [--limit 30] -bun scripts/cli.ts codex trace show --file sessions/2026/06/16/.jsonl [--root ~/.codex] [--tail-bytes 12000] +bun scripts/cli.ts codex trace show --session [--root ~/.codex] [--tail-bytes 12000] ``` -默认只收集 `sessions/*.jsonl`、`history.jsonl`、`shell_snapshots`、`*.log` 和 trace 命名文本;`auth.json`、`config.toml`、sqlite、cache、`.tmp`、generated images、plugins 和 skills 默认跳过。`active` 从 `/proc` 找正在被 Codex 进程打开的 session JSONL,不依赖外部 `lsof`。`grep` 按 JSONL 结构解析并输出有界摘要/signals,不打印整行,优先用于排查 `playwright`、`auth-login-failed`、`UNIDESK_*HINT` 等高噪声 trace。默认输出是类似 k8s 的简洁 text/table;脚本消费时显式加 `-o json|yaml|name|wide`。需要本地审阅敏感/数据库文件时必须显式加 `--include-sensitive` 或 `--include-sqlite`;`collect` 只复制有界文件并写 manifest,不压缩、不上传、不删除源文件。 +默认只收集 `sessions/*.jsonl`、`history.jsonl`、`shell_snapshots`、`*.log` 和 trace 命名文本;`auth.json`、`config.toml`、sqlite、cache、`.tmp`、generated images、plugins 和 skills 默认跳过。`active` 从 `/proc` 找正在被 Codex 进程打开的 session JSONL,不依赖外部 `lsof`,并输出可复制的 `SESSION-ID`。`grep/show` 优先用 `--session ` 定位,避免复制长 JSONL 路径;session id 支持完整 UUID、短前缀和文件名片段,歧义时会提示候选。`grep` 默认只扫 active/recent session,并用 `rg`/raw-line prefilter 先定位候选行再解析 JSONL;需要全量文件域时显式加 `--all-files`,需要跳过 raw 预筛、逐行深度解析 decoded 字段时显式加 `--deep`。排查对话脉络优先 `--messages --pattern ...`,排查工具链优先 `--tools --tool `,只看失败调用用 `--failed-only [--tool ]` 且可以不带 pattern。默认输出优先 message 和 tool-call input;tool output 默认折叠,只给失败状态、错误摘要和对应输入,不打印整段 transcript;需要查看命中输出片段时显式加 `--include-output` 或 `-o wide`。它优先用于排查 `playwright`、`auth-login-failed`、`UNIDESK_*HINT` 等高噪声 trace。默认输出是类似 k8s 的简洁 text/table;脚本消费时显式加 `-o json|yaml|name|wide`。需要本地审阅敏感/数据库文件时必须显式加 `--include-sensitive` 或 `--include-sqlite`;`collect` 只复制有界文件并写 manifest,不压缩、不上传、不删除源文件。 --- diff --git a/scripts/src/codex-trace.ts b/scripts/src/codex-trace.ts index 25820f03..1a0ca156 100644 --- a/scripts/src/codex-trace.ts +++ b/scripts/src/codex-trace.ts @@ -2,6 +2,7 @@ import { closeSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdir import { homedir } from "node:os"; import path from "node:path"; import { createInterface } from "node:readline"; +import { spawnSync } from "node:child_process"; import { repoRoot } from "./config"; import type { RenderedCliResult } from "./output"; @@ -14,6 +15,7 @@ interface CodexTraceOptions { root: string; outputDir: string | null; file: string | null; + session: string | null; limit: number; maxDepth: number; maxFileBytes: number; @@ -26,6 +28,13 @@ interface CodexTraceOptions { allowOutsideRoot: boolean; pattern: string | null; fixed: boolean; + deep: boolean; + allFiles: boolean; + messagesOnly: boolean; + toolsOnly: boolean; + failedOnly: boolean; + includeOutput: boolean; + tool: string | null; includeSystem: boolean; contextChars: number; since: string | null; @@ -43,6 +52,13 @@ interface CodexTraceCandidate { skippedReason: string | null; } +interface ToolCallInfo { + name: string | null; + input: string | null; + line: number; + timestamp: string | null; +} + const defaultLimit = 30; const maxLimit = 500; const defaultMaxDepth = 8; @@ -65,9 +81,12 @@ export function codexTraceHelp(): Record { usage: [ "bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]", "bun scripts/cli.ts codex trace active [--root ~/.codex]", + "bun scripts/cli.ts codex trace grep --session --pattern 'playwright|auth-login-failed' [--since ISO]", + "bun scripts/cli.ts codex trace grep --session --messages --pattern 'Playwright|web-probe'", + "bun scripts/cli.ts codex trace grep --session --failed-only [--tool exec_command]", "bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]", "bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/] [--limit 30]", - "bun scripts/cli.ts codex trace show --file sessions/2026/06/16/.jsonl [--root ~/.codex] [--tail-bytes 12000]", + "bun scripts/cli.ts codex trace show --session [--root ~/.codex] [--tail-bytes 12000]", ], safety: [ "Default scan includes sessions/*.jsonl, history.jsonl, shell_snapshots, *.log, and trace-named text files.", @@ -87,8 +106,16 @@ export function codexTraceHelp(): Record { "--include-cache": "Traverse cache, .tmp, generated_images, plugins, and skills directories. Off by default.", "--dry-run": "For collect, report the plan without copying files.", "--file ": "For show, file path relative to --root or an absolute path under --root.", + "--session ": "For show/grep, resolve a Codex session by full or short session id instead of copying the JSONL path.", "--pattern ": "For grep, pattern to match against structured event text.", "--fixed": "For grep, treat --pattern as a literal string.", + "--deep": "For grep, parse every JSONL line. Default uses a raw-line prefilter for speed.", + "--all-files": "For grep without --file/--session, scan all included trace files instead of active/recent sessions only.", + "--messages": "For grep, only return user/assistant message events.", + "--tools": "For grep, only return tool calls/outputs.", + "--failed-only": "For grep, only return failed tool outputs with folded error summaries.", + "--tool ": "For grep, restrict tool calls/outputs by tool name such as exec_command or imagegen.", + "--include-output": "For grep, allow matched tool output snippets. Default folds tool output and shows input/error summary.", "--since ": "For grep, skip JSONL events before the timestamp string.", "--context-chars ": `For grep, max summary characters per match, default ${defaultContextChars}, max ${maxContextChars}.`, "--file-limit ": `For grep without --file, max target files, default ${defaultFileLimit}, max ${maxFileLimit}.`, @@ -119,8 +146,8 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions { : (() => { throw new Error(`codex trace action must be one of: list, active, grep, collect, show, help; got ${rawAction}`); })(); const optionArgs = action === "help" && rawAction === undefined ? args : args.slice(1); assertKnownOptions(optionArgs, { - flags: ["--dry-run", "--full", "--include-sqlite", "--include-sensitive", "--include-cache", "--allow-outside-root", "--include-system", "--fixed", "--wide", "--help", "-h"], - values: ["--root", "--output", "--file", "--limit", "--max-depth", "--max-file-bytes", "--tail-bytes", "--pattern", "--since", "--context-chars", "--file-limit", "-o", "--format"], + flags: ["--dry-run", "--full", "--include-sqlite", "--include-sensitive", "--include-cache", "--allow-outside-root", "--include-system", "--fixed", "--deep", "--all-files", "--messages", "--tools", "--failed-only", "--include-output", "--wide", "--help", "-h"], + values: ["--root", "--output", "--file", "--session", "--limit", "--max-depth", "--max-file-bytes", "--tail-bytes", "--pattern", "--tool", "--since", "--context-chars", "--file-limit", "-o", "--format"], }); if (optionArgs.includes("--help") || optionArgs.includes("-h")) { return { @@ -128,6 +155,7 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions { root: defaultCodexRoot(), outputDir: null, file: null, + session: null, limit: defaultLimit, maxDepth: defaultMaxDepth, maxFileBytes: defaultMaxFileBytes, @@ -140,6 +168,13 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions { allowOutsideRoot: false, pattern: null, fixed: false, + deep: false, + allFiles: false, + messagesOnly: false, + toolsOnly: false, + failedOnly: false, + includeOutput: false, + tool: null, includeSystem: false, contextChars: defaultContextChars, since: null, @@ -150,11 +185,18 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions { const root = resolveUserPath(optionValue(optionArgs, "--root") ?? defaultCodexRoot()); const positionalPattern = action === "grep" && optionArgs[0] !== undefined && !optionArgs[0].startsWith("-") ? optionArgs[0] : null; const output = parseOutputMode(optionValue(optionArgs, "-o") ?? optionValue(optionArgs, "--format"), hasFlag(optionArgs, "--wide")); + const file = optionValue(optionArgs, "--file") ?? null; + const session = optionValue(optionArgs, "--session") ?? null; + if (file !== null && session !== null) throw new Error("codex trace accepts either --file or --session, not both"); + const messagesOnly = hasFlag(optionArgs, "--messages"); + const toolsOnly = hasFlag(optionArgs, "--tools") || hasFlag(optionArgs, "--failed-only") || optionValue(optionArgs, "--tool") !== undefined; + if (messagesOnly && toolsOnly) throw new Error("codex trace grep accepts either --messages or tool filters, not both"); return { action, root, outputDir: optionValue(optionArgs, "--output") === undefined ? null : resolveUserPath(optionValue(optionArgs, "--output") ?? ""), - file: optionValue(optionArgs, "--file") ?? null, + file, + session, limit: positiveIntegerOption(optionArgs, "--limit", defaultLimit, maxLimit), maxDepth: positiveIntegerOption(optionArgs, "--max-depth", defaultMaxDepth, maxDepthLimit), maxFileBytes: bytesOption(optionArgs, "--max-file-bytes", defaultMaxFileBytes, maxFileBytesLimit), @@ -167,6 +209,13 @@ function parseCodexTraceOptions(args: string[]): CodexTraceOptions { allowOutsideRoot: hasFlag(optionArgs, "--allow-outside-root"), pattern: optionValue(optionArgs, "--pattern") ?? positionalPattern, fixed: hasFlag(optionArgs, "--fixed"), + deep: hasFlag(optionArgs, "--deep"), + allFiles: hasFlag(optionArgs, "--all-files"), + messagesOnly, + toolsOnly, + failedOnly: hasFlag(optionArgs, "--failed-only"), + includeOutput: hasFlag(optionArgs, "--include-output") || output === "wide", + tool: optionValue(optionArgs, "--tool") ?? null, includeSystem: hasFlag(optionArgs, "--include-system"), contextChars: positiveIntegerOption(optionArgs, "--context-chars", defaultContextChars, maxContextChars), since: optionValue(optionArgs, "--since") ?? null, @@ -246,12 +295,14 @@ function codexTraceCollect(options: CodexTraceOptions, candidates: CodexTraceCan } function codexTraceShow(options: CodexTraceOptions): Record | RenderedCliResult { - if (options.file === null) throw new Error("codex trace show requires --file "); - const filePath = resolveTraceFile(options.root, options.file); const rootReal = safeRealpath(options.root); - const fileReal = safeRealpath(filePath); if (rootReal === null) throw new Error(`codex trace root does not exist: ${options.root}`); - if (fileReal === null) throw new Error(`codex trace file does not exist: ${filePath}`); + if (options.file === null && options.session === null) throw new Error("codex trace show requires --file or --session "); + const target = options.session === null + ? candidateFromFile(rootReal, options.root, options.file ?? "") + : resolveSessionCandidate(rootReal, options.root, options.session); + const fileReal = safeRealpath(target.path); + if (fileReal === null) throw new Error(`codex trace file does not exist: ${target.path}`); if (!options.allowOutsideRoot && !isWithin(rootReal, fileReal)) { throw new Error(`codex trace show refuses file outside --root; pass --root accordingly or --allow-outside-root for reviewed local diagnostics`); } @@ -272,6 +323,7 @@ function codexTraceShow(options: CodexTraceOptions): Record | R root: options.root, file: fileReal, relativePath, + sessionId: extractSessionId(relativePath), kind, bytes: stats.size, mtime: stats.mtime.toISOString(), @@ -315,7 +367,7 @@ function codexTraceActive(options: CodexTraceOptions): Record | count: sessions.length, sessions, next: { - grep: "bun scripts/cli.ts codex trace grep --pattern --file ", + grep: "bun scripts/cli.ts codex trace grep --session --pattern ", collect: "bun scripts/cli.ts codex trace collect --limit 30", }, valuesPrinted: false, @@ -324,8 +376,10 @@ function codexTraceActive(options: CodexTraceOptions): Record | } async function codexTraceGrep(options: CodexTraceOptions): Promise | RenderedCliResult> { - if (options.pattern === null || options.pattern.length === 0) throw new Error("codex trace grep requires --pattern or a positional pattern"); - const matcher = makeMatcher(options.pattern, options.fixed); + if ((options.pattern === null || options.pattern.length === 0) && !options.failedOnly) { + throw new Error("codex trace grep requires --pattern , a positional pattern, or --failed-only"); + } + const matcher = options.pattern === null || options.pattern.length === 0 ? null : makeMatcher(options.pattern, options.fixed); const rootReal = safeRealpath(options.root); if (rootReal === null) throw new Error(`codex trace root does not exist: ${options.root}`); const files = grepTargetFiles(options, rootReal); @@ -350,6 +404,11 @@ async function codexTraceGrep(options: CodexTraceOptions): Promise, options: CodexTrac String(session.fd ?? "-"), ageFromIso(stringValue(session.mtime)), formatBytes(numberValue(session.bytes)), + String(session.sessionId ?? "-"), truncateMiddle(String(session.path ?? "-"), options.output === "wide" ? 140 : 82), ]); return [ - renderTable(["PID", "FD", "AGE", "SIZE", "SESSION"], rows), + renderTable(["PID", "FD", "AGE", "SIZE", "SESSION-ID", "FILE"], rows), "", "Next:", - " bun scripts/cli.ts codex trace grep --file --pattern ", + " bun scripts/cli.ts codex trace grep --session --pattern ", ].join("\n"); } @@ -427,7 +487,7 @@ function renderCodexTraceGrep(raw: Record, options: CodexTraceO const rows = results.map((result) => [ shortIso(stringValue(result.timestamp)), `${truncateMiddle(String(result.file ?? "-"), options.output === "wide" ? 96 : 42)}:${String(result.line ?? "-")}`, - truncateMiddle(String(result.item ?? result.type ?? "-"), 24), + truncateMiddle(traceResultItemLabel(result), 28), signalLabel(asRecord(result.signals)), truncateOneLine(String(result.summary ?? ""), options.output === "wide" ? 220 : 96), ]); @@ -441,6 +501,7 @@ function renderCodexTraceList(raw: Record, options: CodexTraceO const files = recordArray(raw.files); if (files.length === 0) return `No trace files found under ${String(raw.root ?? "-")}.`; const rows = files.map((file) => [ + String(file.sessionId ?? "-"), truncateMiddle(String(file.path ?? "-"), options.output === "wide" ? 140 : 82), String(file.kind ?? "-"), formatBytes(numberValue(file.bytes)), @@ -450,7 +511,7 @@ function renderCodexTraceList(raw: Record, options: CodexTraceO const skippedText = Object.entries(skipped).map(([key, value]) => `${key}=${String(value)}`).join(" "); return [ `Root: ${String(raw.root ?? "-")}`, - renderTable(["PATH", "KIND", "SIZE", "AGE"], rows), + renderTable(["SESSION-ID", "PATH", "KIND", "SIZE", "AGE"], rows), skippedText.length > 0 ? `Skipped: ${skippedText}` : "", Number(raw.omitted ?? 0) > 0 ? `Omitted: ${String(raw.omitted)}` : "", ].filter((line) => line.length > 0).join("\n"); @@ -477,6 +538,7 @@ function renderCodexTraceShow(raw: Record): string { const content = stringValue(raw.content); const lines = [ `Name: ${String(raw.relativePath ?? raw.file ?? "-")}`, + `Session: ${String(raw.sessionId ?? "-")}`, `Kind: ${String(raw.kind ?? "-")}`, `Size: ${formatBytes(numberValue(raw.bytes))}`, `Mode: ${String(policy.mode ?? "-")} bytes=${String(policy.bytesReturned ?? 0)} truncated=${String(policy.truncated ?? false)}`, @@ -488,14 +550,34 @@ function renderCodexTraceShow(raw: Record): string { function renderCodexTraceNames(raw: Record): string { const sessions = recordArray(raw.sessions); - if (sessions.length > 0) return sessions.map((session) => `session/${String(session.path ?? "")}`).join("\n"); + if (sessions.length > 0) { + return sessions + .map((session) => String(session.sessionId ?? session.path ?? "")) + .filter((value) => value.length > 0) + .map((value) => `session/${value}`) + .join("\n"); + } const files = recordArray(raw.files); - if (files.length > 0) return files.map((file) => String(file.path ?? file.source ?? file.relativePath ?? "")).filter((line) => line.length > 0).join("\n"); + if (files.length > 0) { + return files.map((file) => { + const sessionId = stringValue(file.sessionId); + return sessionId === null ? String(file.path ?? file.source ?? file.relativePath ?? "") : `session/${sessionId}`; + }).filter((line) => line.length > 0).join("\n"); + } const results = recordArray(raw.results); if (results.length > 0) return results.map((result) => `${String(result.file ?? "")}:${String(result.line ?? "")}`).join("\n"); return ""; } +function traceResultItemLabel(result: Record): string { + const eventClass = stringValue(result.class); + const tool = stringValue(result.tool); + if (eventClass === "tool-call" || eventClass === "tool-output") { + return tool === null ? eventClass : `${eventClass}/${tool}`; + } + return eventClass ?? String(result.item ?? result.type ?? "-"); +} + function scanCodexTraceRoot(options: CodexTraceOptions): CodexTraceCandidate[] { if (!existsSync(options.root)) return []; const root = realpathSync(options.root); @@ -583,16 +665,22 @@ function activeCodexSessionFiles(rootReal: string): Record[] { continue; } const relativePath = path.relative(rootReal, realTarget); + const sessionId = extractSessionId(relativePath); const key = `${pid}:${realTarget}`; rows.set(key, { pid, fd: fdEntry.name, path: relativePath, + sessionId, bytes: stats.size, mtime: stats.mtime.toISOString(), command: truncateOneLine(cmdline, 180), - grepCommand: `bun scripts/cli.ts codex trace grep --file ${shellWord(relativePath)} --pattern `, - showCommand: `bun scripts/cli.ts codex trace show --file ${shellWord(relativePath)}`, + grepCommand: sessionId === null + ? `bun scripts/cli.ts codex trace grep --file ${shellWord(relativePath)} --pattern ` + : `bun scripts/cli.ts codex trace grep --session ${shellWord(sessionId)} --pattern `, + showCommand: sessionId === null + ? `bun scripts/cli.ts codex trace show --file ${shellWord(relativePath)}` + : `bun scripts/cli.ts codex trace show --session ${shellWord(sessionId)}`, }); } } @@ -600,22 +688,9 @@ function activeCodexSessionFiles(rootReal: string): Record[] { } function grepTargetFiles(options: CodexTraceOptions, rootReal: string): CodexTraceCandidate[] { + if (options.session !== null) return [resolveSessionCandidate(rootReal, options.root, options.session)]; if (options.file !== null) { - const filePath = resolveTraceFile(options.root, options.file); - const fileReal = safeRealpath(filePath); - if (fileReal === null) throw new Error(`codex trace grep file does not exist: ${filePath}`); - if (!options.allowOutsideRoot && !isWithin(rootReal, fileReal)) throw new Error("codex trace grep refuses file outside --root"); - const stats = statSync(fileReal); - const relativePath = path.relative(rootReal, fileReal); - return [{ - path: fileReal, - relativePath, - kind: classifyTraceKind(relativePath, path.basename(fileReal)), - bytes: stats.size, - mtimeMs: stats.mtimeMs, - included: true, - skippedReason: null, - }]; + return [candidateFromFile(rootReal, options.root, options.file, options.allowOutsideRoot)]; } const active = activeCodexSessionFiles(rootReal) .map((row) => String(row.path)) @@ -633,7 +708,9 @@ function grepTargetFiles(options: CodexTraceOptions, rootReal: string): CodexTra skippedReason: null, } satisfies CodexTraceCandidate; }); - const scanned = scanCodexTraceRoot(options).filter((candidate) => candidate.included && isTextReadableTrace(candidate.kind, candidate.path)); + const scanned = options.allFiles + ? scanCodexTraceRoot(options).filter((candidate) => candidate.included && isTextReadableTrace(candidate.kind, candidate.path)) + : recentSessionCandidates(rootReal, options, options.fileLimit); const byPath = new Map(); for (const candidate of [...active, ...scanned]) { if (!isTextReadableTrace(candidate.kind, candidate.path)) continue; @@ -642,26 +719,162 @@ function grepTargetFiles(options: CodexTraceOptions, rootReal: string): CodexTra return [...byPath.values()].sort((left, right) => right.mtimeMs - left.mtimeMs).slice(0, options.fileLimit); } +function candidateFromFile(rootReal: string, root: string, file: string, allowOutsideRoot = false): CodexTraceCandidate { + const filePath = resolveTraceFile(root, file); + const fileReal = safeRealpath(filePath); + if (fileReal === null) throw new Error(`codex trace file does not exist: ${filePath}`); + if (!allowOutsideRoot && !isWithin(rootReal, fileReal)) throw new Error("codex trace refuses file outside --root"); + const stats = statSync(fileReal); + const relativePath = path.relative(rootReal, fileReal); + return { + path: fileReal, + relativePath, + kind: classifyTraceKind(relativePath, path.basename(fileReal)), + bytes: stats.size, + mtimeMs: stats.mtimeMs, + included: true, + skippedReason: null, + }; +} + +function resolveSessionCandidate(rootReal: string, root: string, rawSession: string): CodexTraceCandidate { + const session = rawSession.replace(/^session\//u, "").trim(); + if (session.length === 0) throw new Error("codex trace --session requires a non-empty session id"); + if (session.includes("/") || session.endsWith(".jsonl")) { + return candidateFromFile(rootReal, root, session); + } + const scanned = (sessionJsonlCandidatesByToken(rootReal, session) ?? sessionJsonlCandidates(rootReal)) + .filter((candidate) => sessionCandidateMatches(candidate.relativePath, session)); + const byPath = new Map(); + for (const candidate of scanned) byPath.set(candidate.path, candidate); + const matches = [...byPath.values()].sort((left, right) => right.mtimeMs - left.mtimeMs); + if (matches.length === 0) throw new Error(`codex trace session not found under ${root}: ${session}`); + const exact = matches.filter((candidate) => sessionCandidateExact(candidate.relativePath, session)); + if (exact.length === 1) return exact[0] as CodexTraceCandidate; + const prefix = matches.filter((candidate) => { + const id = extractSessionId(candidate.relativePath); + return id !== null && id.startsWith(session.toLowerCase()); + }); + if (prefix.length === 1) return prefix[0] as CodexTraceCandidate; + if (matches.length === 1) return matches[0] as CodexTraceCandidate; + const choices = matches.slice(0, 8).map((candidate) => ` ${extractSessionId(candidate.relativePath) ?? "-"} ${candidate.relativePath}`).join("\n"); + throw new Error(`codex trace session id is ambiguous: ${session}\n${choices}`); +} + +function recentSessionCandidates(rootReal: string, options: CodexTraceOptions, limit: number): CodexTraceCandidate[] { + return sessionJsonlCandidates(rootReal) + .filter((candidate) => candidate.bytes <= options.maxFileBytes || candidate.mtimeMs > Date.now() - 7 * 24 * 60 * 60 * 1000) + .sort((left, right) => right.mtimeMs - left.mtimeMs) + .slice(0, limit); +} + +function sessionJsonlCandidates(rootReal: string): CodexTraceCandidate[] { + const sessionsRoot = path.join(rootReal, "sessions"); + if (!existsSync(sessionsRoot)) return []; + const fast = sessionJsonlCandidatesFast(rootReal, sessionsRoot); + if (fast !== null) return fast; + const candidates: CodexTraceCandidate[] = []; + const walk = (dir: string, depth: number) => { + if (depth > 8) return; + let entries; + try { + entries = readdirSync(dir, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isSymbolicLink()) continue; + if (entry.isDirectory()) { + walk(fullPath, depth + 1); + continue; + } + if (!entry.isFile() || !entry.name.endsWith(".jsonl")) continue; + let stats; + try { + stats = lstatSync(fullPath); + } catch { + continue; + } + const relativePath = path.relative(rootReal, fullPath); + candidates.push({ + path: fullPath, + relativePath, + kind: "session-jsonl", + bytes: stats.size, + mtimeMs: stats.mtimeMs, + included: true, + skippedReason: null, + }); + } + }; + walk(sessionsRoot, 0); + return candidates; +} + +function sessionJsonlCandidatesByToken(rootReal: string, token: string): CodexTraceCandidate[] | null { + if (!/^[A-Za-z0-9._-]+$/u.test(token)) return null; + const sessionsRoot = path.join(rootReal, "sessions"); + if (!existsSync(sessionsRoot)) return []; + return sessionJsonlCandidatesFromRg(rootReal, sessionsRoot, `*${token}*.jsonl`); +} + +function sessionJsonlCandidatesFast(rootReal: string, sessionsRoot: string): CodexTraceCandidate[] | null { + return sessionJsonlCandidatesFromRg(rootReal, sessionsRoot, "*.jsonl"); +} + +function sessionJsonlCandidatesFromRg(rootReal: string, sessionsRoot: string, glob: string): CodexTraceCandidate[] | null { + const result = spawnSync("rg", ["--files", "--glob", glob, sessionsRoot], { encoding: "utf8", maxBuffer: 8 * 1024 * 1024 }); + if (result.error !== undefined || result.status !== 0) return null; + const candidates: CodexTraceCandidate[] = []; + for (const line of (result.stdout ?? "").split("\n")) { + if (line.length === 0) continue; + let stats; + try { + stats = lstatSync(line); + } catch { + continue; + } + const relativePath = path.relative(rootReal, line); + candidates.push({ + path: line, + relativePath, + kind: "session-jsonl", + bytes: stats.size, + mtimeMs: stats.mtimeMs, + included: true, + skippedReason: null, + }); + } + return candidates; +} + async function grepJsonlFile( candidate: CodexTraceCandidate, rootReal: string, - matcher: RegExp, + matcher: RegExp | null, options: CodexTraceOptions, results: Record[], limit: number, ): Promise<{ linesScanned: number; matchedTotal: number }> { + const fast = grepJsonlFileFast(candidate, rootReal, matcher, options, results, limit); + if (fast !== null) return fast; let linesScanned = 0; let matchedTotal = 0; + const toolCalls = new Map(); + const toolMatcher = options.tool === null ? null : makeMatcher(options.tool, false); const input = createReadStream(candidate.path, { encoding: "utf8" }); const reader = createInterface({ input, crlfDelay: Infinity }); try { for await (const line of reader) { linesScanned += 1; + const rawLineMatch = matcher === null ? false : matcher.test(line); + if (matcher !== null) matcher.lastIndex = 0; + if (!rawLineMatch && !options.deep && !shouldInspectTraceLine(line, options)) continue; const event = parseJsonlEvent(line); if (event === null) { - const rawMatch = matcher.test(line); - matcher.lastIndex = 0; - if (!rawMatch) continue; + if (matcher === null) continue; + if (!rawLineMatch) continue; matchedTotal += 1; if (results.length < limit) { results.push({ @@ -676,29 +889,54 @@ async function grepJsonlFile( } continue; } + const eventClass = traceEventClass(event); + if (eventClass === "tool-call") { + const callId = traceCallId(event); + if (callId !== null) { + toolCalls.set(callId, { + name: traceToolName(event, null), + input: summarizeToolInput(event, Math.max(options.contextChars, 1000)), + line: linesScanned, + timestamp: typeof event.timestamp === "string" ? event.timestamp : null, + }); + } + } if (!options.includeSystem && event.type === "session_meta") continue; if (!options.includeSystem && isBootstrapTraceEvent(event)) continue; if (options.since !== null && typeof event.timestamp === "string" && event.timestamp < options.since) continue; + const callInfo = traceCallInfo(event, toolCalls); + const toolName = traceToolName(event, callInfo); + const failed = eventClass === "tool-output" && isFailedToolOutput(event); + if (!traceEventPassesKindFilters(eventClass, toolName, failed, toolMatcher, options)) continue; const fields = traceEventFields(event); const matchedFields: string[] = []; const matchedValues: string[] = []; - for (const field of fields) { - if (matcher.test(field.value)) { - matchedFields.push(field.name); - matchedValues.push(field.value); + if (matcher === null) { + matchedFields.push(failed ? "failed-output" : eventClass); + matchedValues.push(toolOutputText(event) ?? summarizeTraceEvent(event, Math.max(options.contextChars, 1000))); + } else { + for (const field of fields) { + const match = matcher.exec(field.value); + if (match !== null) { + matchedFields.push(field.name); + matchedValues.push(snippetAroundMatch(field.value, match.index, Math.max(options.contextChars, 1000))); + } + matcher.lastIndex = 0; } - matcher.lastIndex = 0; } if (matchedFields.length === 0) continue; matchedTotal += 1; if (results.length >= limit) continue; - const summary = summarizeMatchedTraceEvent(event, fields, matcher, options.contextChars); + const summary = summarizeMatchedTraceEvent(event, fields, matcher, options, callInfo, failed); results.push({ file: path.relative(rootReal, candidate.path), line: linesScanned, timestamp: typeof event.timestamp === "string" ? event.timestamp : null, type: event.type, + class: eventClass, item: traceEventItem(event), + tool: toolName, + failed, fields: matchedFields, summary, signals: traceSignals(matchedValues.join(" ")), @@ -711,10 +949,202 @@ async function grepJsonlFile( return { linesScanned, matchedTotal }; } +function grepJsonlFileFast( + candidate: CodexTraceCandidate, + rootReal: string, + matcher: RegExp | null, + options: CodexTraceOptions, + results: Record[], + limit: number, +): { linesScanned: number; matchedTotal: number } | null { + if (options.deep || !isTextReadableTrace(candidate.kind, candidate.path)) return null; + const rgPattern = fastGrepPattern(options); + if (rgPattern === null) return null; + const rgMaxCount = options.since === null ? Math.max(limit * 50, 200) : Math.max(limit * 200, 2000); + const rg = runRipgrepLines(candidate.path, rgPattern.pattern, rgPattern.fixed, rgMaxCount); + if (rg === null) return null; + let matchedTotal = 0; + const toolCalls = preloadToolCallsForMatches(candidate.path, rg, options); + const callCache = new Map(); + const toolMatcher = options.tool === null ? null : makeMatcher(options.tool, false); + for (const match of rg) { + const event = parseJsonlEvent(match.text); + if (event === null) continue; + const eventClass = traceEventClass(event); + if (eventClass === "tool-call") { + const callId = traceCallId(event); + if (callId !== null) { + toolCalls.set(callId, { + name: traceToolName(event, null), + input: summarizeToolInput(event, Math.max(options.contextChars, 1000)), + line: match.line, + timestamp: typeof event.timestamp === "string" ? event.timestamp : null, + }); + } + } + if (!options.includeSystem && event.type === "session_meta") continue; + if (!options.includeSystem && isBootstrapTraceEvent(event)) continue; + if (options.since !== null && typeof event.timestamp === "string" && event.timestamp < options.since) continue; + let callInfo = traceCallInfo(event, toolCalls); + const callId = traceCallId(event); + if (callInfo === null && eventClass === "tool-output" && callId !== null) { + if (!callCache.has(callId)) callCache.set(callId, findToolCallInfoByCallId(candidate.path, callId, match.line, options)); + callInfo = callCache.get(callId) ?? null; + } + const toolName = traceToolName(event, callInfo); + const failed = eventClass === "tool-output" && isFailedToolOutput(event); + if (!traceEventPassesKindFilters(eventClass, toolName, failed, toolMatcher, options)) continue; + const fields = traceEventFields(event); + const matchedFields: string[] = []; + const matchedValues: string[] = []; + if (matcher === null) { + matchedFields.push(failed ? "failed-output" : eventClass); + matchedValues.push(toolOutputText(event) ?? summarizeTraceEvent(event, Math.max(options.contextChars, 1000))); + } else { + for (const field of fields) { + const fieldMatch = matcher.exec(field.value); + if (fieldMatch !== null) { + matchedFields.push(field.name); + matchedValues.push(snippetAroundMatch(field.value, fieldMatch.index, Math.max(options.contextChars, 1000))); + } + matcher.lastIndex = 0; + } + } + if (matchedFields.length === 0) continue; + matchedTotal += 1; + if (results.length >= limit) continue; + results.push({ + file: path.relative(rootReal, candidate.path), + line: match.line, + timestamp: typeof event.timestamp === "string" ? event.timestamp : null, + type: event.type, + class: eventClass, + item: traceEventItem(event), + tool: toolName, + failed, + fields: matchedFields, + summary: summarizeMatchedTraceEvent(event, fields, matcher, options, callInfo, failed), + signals: traceSignals(matchedValues.join(" ")), + }); + } + return { linesScanned: rg.length, matchedTotal }; +} + +function fastGrepPattern(options: CodexTraceOptions): { pattern: string; fixed: boolean } | null { + if (options.pattern !== null && options.pattern.length > 0) return { pattern: options.pattern, fixed: options.fixed }; + if (options.failedOnly) { + return { + pattern: "Process exited with code [1-9][0-9]*|ok=false|\"ok\"\\s*:\\s*false|status=(blocked|failed|error)|\"status\"\\s*:\\s*\"(blocked|failed|error)\"|probe\\.error=auth-login-failed|auth-login-failed|Internal Server Error|Service Unavailable|Traceback|Unhandled exception|Exception:|Error:", + fixed: false, + }; + } + return null; +} + +function runRipgrepLines(filePath: string, pattern: string, fixed: boolean, maxCount: number): Array<{ line: number; text: string }> | null { + return runRipgrepLinesMany(filePath, [pattern], fixed, maxCount); +} + +function runRipgrepLinesMany(filePath: string, patterns: string[], fixed: boolean, maxCount: number): Array<{ line: number; text: string }> | null { + if (patterns.length === 0) return []; + const args = ["--line-number", "--color", "never", "--ignore-case", "--max-count", String(maxCount)]; + if (fixed) args.push("--fixed-strings"); + for (const pattern of patterns) args.push("-e", pattern); + args.push("--", filePath); + const result = spawnSync("rg", args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); + if (result.error !== undefined) return null; + if (result.status !== 0 && result.status !== 1) return null; + const stdout = result.stdout ?? ""; + if (stdout.length === 0) return []; + const rows: Array<{ line: number; text: string }> = []; + for (const rawLine of stdout.split("\n")) { + if (rawLine.length === 0) continue; + const separator = rawLine.indexOf(":"); + if (separator <= 0) continue; + const line = Number(rawLine.slice(0, separator)); + if (!Number.isInteger(line) || line <= 0) continue; + rows.push({ line, text: rawLine.slice(separator + 1) }); + } + return rows; +} + +function findToolCallInfoByCallId(filePath: string, callId: string, outputLine: number, options: CodexTraceOptions): ToolCallInfo | null { + const matches = runRipgrepLines(filePath, callId, true, 20); + if (matches === null) return null; + for (const match of matches) { + if (match.line >= outputLine) continue; + const event = parseJsonlEvent(match.text); + if (event === null || traceEventClass(event) !== "tool-call") continue; + return { + name: traceToolName(event, null), + input: summarizeToolInput(event, Math.max(options.contextChars, 1000)), + line: match.line, + timestamp: typeof event.timestamp === "string" ? event.timestamp : null, + }; + } + return null; +} + +function preloadToolCallsForMatches(filePath: string, matches: Array<{ line: number; text: string }>, options: CodexTraceOptions): Map { + const callIds = new Set(); + for (const match of matches) { + const event = parseJsonlEvent(match.text); + if (event === null || traceEventClass(event) !== "tool-output") continue; + const callId = traceCallId(event); + if (callId !== null) callIds.add(callId); + } + if (callIds.size === 0) return new Map(); + return findToolCallInfosByCallIds(filePath, [...callIds], options); +} + +function findToolCallInfosByCallIds(filePath: string, callIds: string[], options: CodexTraceOptions): Map { + const calls = new Map(); + const matches = runRipgrepLinesMany(filePath, callIds, true, Math.max(callIds.length * 4, 20)); + if (matches === null) return calls; + for (const match of matches) { + const event = parseJsonlEvent(match.text); + if (event === null || traceEventClass(event) !== "tool-call") continue; + const callId = traceCallId(event); + if (callId === null) continue; + calls.set(callId, { + name: traceToolName(event, null), + input: summarizeToolInput(event, Math.max(options.contextChars, 1000)), + line: match.line, + timestamp: typeof event.timestamp === "string" ? event.timestamp : null, + }); + } + return calls; +} + function selectedCandidates(options: CodexTraceOptions, candidates: CodexTraceCandidate[]): CodexTraceCandidate[] { return candidates.filter((item) => item.included).slice(0, options.limit); } +function extractSessionId(relativePath: string): string | null { + const basename = path.basename(relativePath).replace(/\.jsonl$/iu, ""); + const matches = basename.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/giu); + return matches === null || matches.length === 0 ? null : (matches[matches.length - 1] ?? null)?.toLowerCase() ?? null; +} + +function sessionCandidateMatches(relativePath: string, rawSession: string): boolean { + const session = rawSession.toLowerCase(); + const basename = path.basename(relativePath).replace(/\.jsonl$/iu, "").toLowerCase(); + const normalized = normalizePath(relativePath).toLowerCase(); + const id = extractSessionId(relativePath); + return sessionCandidateExact(relativePath, session) + || (id !== null && (id.startsWith(session) || id.includes(session))) + || basename.includes(session) + || normalized.includes(session); +} + +function sessionCandidateExact(relativePath: string, rawSession: string): boolean { + const session = rawSession.toLowerCase(); + const basename = path.basename(relativePath).replace(/\.jsonl$/iu, "").toLowerCase(); + const normalized = normalizePath(relativePath).toLowerCase(); + const id = extractSessionId(relativePath); + return id === session || basename === session || normalized === session || normalized === `sessions/${session}`; +} + function classifyTraceKind(relativePath: string, basename: string): CodexTraceKind { const normalized = normalizePath(relativePath); if (/^sessions\/.+\.jsonl$/u.test(normalized)) return "session-jsonl"; @@ -754,12 +1184,16 @@ function isTextReadableTrace(kind: CodexTraceKind, filePath: string): boolean { } function candidateRow(candidate: CodexTraceCandidate): Record { + const sessionId = extractSessionId(candidate.relativePath); return { path: candidate.relativePath, + sessionId, kind: candidate.kind, bytes: candidate.bytes, mtime: new Date(candidate.mtimeMs).toISOString(), - showCommand: `bun scripts/cli.ts codex trace show --file ${shellWord(candidate.relativePath)}`, + showCommand: sessionId === null + ? `bun scripts/cli.ts codex trace show --file ${shellWord(candidate.relativePath)}` + : `bun scripts/cli.ts codex trace show --session ${shellWord(sessionId)}`, }; } @@ -793,6 +1227,8 @@ function policySummary(options: CodexTraceOptions): Record { includeSqlite: options.includeSqlite, includeSensitive: options.includeSensitive, includeCache: options.includeCache, + grepDefaultScope: options.allFiles ? "all-included-files" : "active-and-recent-sessions", + grepParseMode: options.deep ? "deep-json-parse" : "raw-prefilter", skippedByDefault: ["auth/config", "sqlite", "cache/.tmp/generated_images/plugins/skills"], }; } @@ -869,13 +1305,39 @@ function summarizeTraceEvent(event: Record, maxChars: number): return truncateOneLine(redactTraceText(JSON.stringify(event)), maxChars); } -function summarizeMatchedTraceEvent(event: Record, fields: Array<{ name: string; value: string }>, matcher: RegExp, maxChars: number): string { +function summarizeMatchedTraceEvent( + event: Record, + fields: Array<{ name: string; value: string }>, + matcher: RegExp | null, + options: CodexTraceOptions, + callInfo: ToolCallInfo | null, + failed: boolean, +): string { + const maxChars = options.contextChars; + const eventClass = traceEventClass(event); + if (eventClass === "tool-call") { + const toolName = traceToolName(event, callInfo) ?? "tool"; + return truncateOneLine(`tool-call ${toolName} input: ${summarizeToolInput(event, maxChars)}`, maxChars + 120); + } + if (eventClass === "tool-output") { + const toolName = traceToolName(event, callInfo) ?? "tool"; + const input = callInfo?.input ?? ""; + const output = toolOutputText(event) ?? ""; + if (failed) { + const error = failureExcerpt(output, maxChars); + return truncateOneLine(`tool-output ${toolName} failed error: ${error} input: ${truncateOneLine(input, 220)}`, maxChars + 180); + } + if (!options.includeOutput) { + return truncateOneLine(`tool-output ${toolName} folded input: ${input}`, maxChars + 120); + } + } const item = traceEventItem(event); for (const field of fields) { + if (matcher === null) break; const match = matcher.exec(field.value); matcher.lastIndex = 0; if (match === null || match.index < 0) continue; - const snippet = snippetAroundMatch(redactTraceText(field.value), match.index, maxChars); + const snippet = redactTraceText(snippetAroundMatch(field.value, match.index, maxChars)); return truncateOneLine(`${item === null ? String(event.type ?? "event") : item} ${field.name}: ${snippet}`, maxChars + 120); } return summarizeTraceEvent(event, maxChars); @@ -895,6 +1357,104 @@ function traceEventItem(event: Record): string | null { return stringValue(payload?.name) ?? stringValue(payload?.type) ?? stringValue(payload?.role); } +function shouldInspectTraceLine(line: string, options: CodexTraceOptions): boolean { + if (line.includes('"function_call"')) return true; + if (options.failedOnly || options.toolsOnly || options.tool !== null) return line.includes('"function_call"') || line.includes('"function_call_output"'); + if (options.messagesOnly) return line.includes('"message"'); + return false; +} + +function traceEventClass(event: Record): "message" | "tool-call" | "tool-output" | "event" { + const payload = asRecord(event.payload); + if (event.type === "response_item" && payload?.type === "message") return "message"; + if (event.type === "response_item" && payload?.type === "function_call") return "tool-call"; + if (event.type === "response_item" && payload?.type === "function_call_output") return "tool-output"; + return "event"; +} + +function traceCallId(event: Record): string | null { + const payload = asRecord(event.payload); + return stringValue(payload?.call_id) ?? stringValue(payload?.id); +} + +function traceCallInfo(event: Record, toolCalls: Map): ToolCallInfo | null { + const callId = traceCallId(event); + return callId === null ? null : toolCalls.get(callId) ?? null; +} + +function traceToolName(event: Record, callInfo: ToolCallInfo | null): string | null { + const payload = asRecord(event.payload); + return stringValue(payload?.name) ?? callInfo?.name ?? null; +} + +function summarizeToolInput(event: Record, maxChars: number): string { + const payload = asRecord(event.payload); + const argsText = stringValue(payload?.arguments) ?? ""; + const args = parseJsonObject(argsText); + const cmd = stringValue(args?.cmd); + const prompt = stringValue(args?.prompt); + const rendered = cmd ?? prompt ?? argsText; + return truncateOneLine(redactTraceText(rendered.length === 0 ? "" : rendered), maxChars); +} + +function toolOutputText(event: Record): string | null { + const payload = asRecord(event.payload); + return stringValue(payload?.output); +} + +function isFailedToolOutput(event: Record): boolean { + const output = toolOutputText(event) ?? ""; + return /Process exited with code (?!0\b)\d+/iu.test(output) + || /(?:^|\n)\s*ok=false\s*(?:\n|$)/iu.test(output) + || /(?:^|\n)\s*status=(?:blocked|failed|error)\s*(?:\n|$)/iu.test(output) + || /(?:^|\n)\s*probe\.error=auth-login-failed\s*(?:\n|$)/iu.test(output) + || /(?:^|\n)\s*probe\.auth\.status(?:Text)?=(?:5\d\d|Internal Server Error|Service Unavailable)\s*(?:\n|$)/iu.test(output) + || /Output:\s*\n\s*\{\s*\n\s*"ok"\s*:\s*false/iu.test(output) + || /"error"\s*:\s*"auth-login-failed"/iu.test(output) + || /"status"\s*:\s*"(?:blocked|failed|error)"/iu.test(output) + || /"statusText"\s*:\s*"(?:Internal Server Error|Service Unavailable)"/iu.test(output) + || /(?:^|\n)(?:Traceback|Unhandled exception|Exception:|Error:)[^\n]*/iu.test(output); +} + +function failureExcerpt(output: string, maxChars: number): string { + const patterns = [ + /Process exited with code (?!0\b)\d+[^\n]*/iu, + /(?:^|\n)\s*probe\.error=[^\n]+/iu, + /(?:^|\n)\s*probe\.auth\.statusText=[^\n]+/iu, + /"error"\s*:\s*"[^"]+"/iu, + /"name"\s*:\s*"(?:TimeoutError|[^"]*Error)"/iu, + /"message"\s*:\s*"[^"]*(?:Timeout|Error|failed|blocked)[^"]*"/iu, + /"statusText"\s*:\s*"[^"]+"/iu, + /auth-login-failed[^\n,}]*/iu, + /Internal Server Error|Service Unavailable|Traceback[^\n]*/iu, + /Error:[^\n]*/iu, + ]; + for (const pattern of patterns) { + const match = pattern.exec(output); + if (match !== null) return truncateOneLine(redactTraceText(match[0] ?? ""), maxChars); + } + return truncateOneLine(redactTraceText(output), maxChars); +} + +function traceEventPassesKindFilters( + eventClass: "message" | "tool-call" | "tool-output" | "event", + toolName: string | null, + failed: boolean, + toolMatcher: RegExp | null, + options: CodexTraceOptions, +): boolean { + if (options.messagesOnly && eventClass !== "message") return false; + if (options.toolsOnly && eventClass !== "tool-call" && eventClass !== "tool-output") return false; + if (options.failedOnly && (eventClass !== "tool-output" || !failed)) return false; + if (toolMatcher !== null) { + const matched = toolName !== null && toolMatcher.test(toolName); + toolMatcher.lastIndex = 0; + if (!matched) return false; + } + if (eventClass === "tool-output" && !failed && !options.includeOutput && !options.failedOnly) return false; + return true; +} + function traceSignals(text: string): Record { return { authLoginFailed: /auth-login-failed/iu.test(text), diff --git a/scripts/src/help.ts b/scripts/src/help.ts index 5b8ea47f..d8a94068 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -390,9 +390,12 @@ function codexHelp(): unknown { usage: [ "bun scripts/cli.ts codex trace list [--root ~/.codex] [--limit 30]", "bun scripts/cli.ts codex trace active [--root ~/.codex]", + "bun scripts/cli.ts codex trace grep --session --pattern 'playwright|auth-login-failed' [--since ISO]", + "bun scripts/cli.ts codex trace grep --session --messages --pattern 'Playwright|web-probe'", + "bun scripts/cli.ts codex trace grep --session --failed-only [--tool exec_command]", "bun scripts/cli.ts codex trace grep --pattern 'playwright|auth-login-failed' [--file sessions/...jsonl] [--since ISO]", "bun scripts/cli.ts codex trace collect [--root ~/.codex] [--output .state/codex-trace/] [--limit 30]", - "bun scripts/cli.ts codex trace show --file sessions/2026/06/16/.jsonl [--root ~/.codex] [--tail-bytes 12000]", + "bun scripts/cli.ts codex trace show --session [--root ~/.codex] [--tail-bytes 12000]", "bun scripts/cli.ts codex deploy # disabled legacy deployment entry", "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", "bun scripts/cli.ts agentrun describe aipodspec/Artificer", @@ -444,8 +447,8 @@ function codexHelp(): unknown { defaultRoot: "~/.codex", defaultIncludes: ["sessions/*.jsonl", "history.jsonl", "shell_snapshots", "*.log", "trace-named text files"], defaultExcludes: ["auth/config", "sqlite", "cache/.tmp/generated_images/plugins/skills"], - active: "codex trace active finds open Codex session JSONL files from /proc without requiring lsof.", - grep: "codex trace grep parses JSONL records and returns bounded summaries/signals instead of printing whole matching lines.", + active: "codex trace active finds open Codex session JSONL files from /proc without requiring lsof and prints copyable session ids.", + grep: "codex trace grep supports --session , --messages, --tools, --tool , and --failed-only. It defaults to active/recent sessions, uses rg/raw prefiltering for speed, prioritizes messages/tool inputs, and folds tool outputs unless --include-output or -o wide is explicit.", output: "Default output is concise text/table; use -o json|yaml|name|wide for machine or wider output.", collectOutput: ".state/codex-trace//manifest.json", note: "Use --root to scan another local Codex trace directory; collect copies bounded files locally and never uploads or deletes source files.",