From 2e95276db87bdec07b309eaf3e5a54db4a85a6f6 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Wed, 10 Jun 2026 10:05:43 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=94=B6=E6=95=9B=20session=20detail=20?= =?UTF-8?q?=E6=8C=89=20id=20=E7=B2=BE=E7=A1=AE=E6=8B=89=E5=8F=96=20(#137)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Codex --- docs/reference/spec-v01-cli.md | 6 +- scripts/src/cli.ts | 64 ++++++++++++++++++--- src/selftest/cases/15-cli-events-summary.ts | 3 +- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/docs/reference/spec-v01-cli.md b/docs/reference/spec-v01-cli.md index 31e414b..efbbf9e 100644 --- a/docs/reference/spec-v01-cli.md +++ b/docs/reference/spec-v01-cli.md @@ -73,8 +73,8 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 ./scripts/agentrun sessions turn [sessionId] [--json-file |--json-stdin] [--prompt-file |--prompt-stdin|--prompt ] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file |--runner-json-stdin] [--no-runner-job] ./scripts/agentrun sessions steer [--prompt-file |--prompt-stdin|--prompt ] ./scripts/agentrun sessions cancel [--reason ] -./scripts/agentrun sessions trace [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw] -./scripts/agentrun sessions output [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw] +./scripts/agentrun sessions trace [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw] +./scripts/agentrun sessions output [--after-seq ] [--limit ] [--run-id ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw] ./scripts/agentrun sessions read [--reader-id ] ``` @@ -105,7 +105,7 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 - `sessions turn` 是异步 subagent 的受控 CLI 入口:短返回 run、command、runnerJob 和后续 poll/read/steer/cancel 命令,不等待模型完成。`--profile M3` 是 `minimax-m3` 的 CLI alias;profile 仍写入 canonical `backendProfile`,不得 fallback。 - `sessions steer` 对当前 active run 创建 `type=steer` command;`sessions cancel` 通过 Session control 取消 active command 或 run;`sessions read` 写入 reader cursor,使 terminal session 从默认 ps 中消失。 - `sessions output` 与 `sessions trace` 是输出和 trace 的唯一 CLI 查询入口;不得新增 `queue output` 或 `queue trace` 兼容命令。 -- `sessions output` 与 `sessions trace` 默认必须按渐进披露输出低噪声 JSON:只展示 `assistant_message` 与 `tool_call`/`error` 摘要,`command_output`、`backend_status`、raw event、runnerTrace 和大 stdout/stderr 只进入 `suppressedEvents` 计数与 bytes,不得默认展开正文。需要查看工具输出、backend_status 或原始 event 时,必须通过默认摘要中的 `detailCommands`,或显式使用 `--seq `、`--event-id `、`--item-id `、`--include-output`、`--full`/`--raw` 做定点展开;这样保证默认不爆上下文,同时按 id/seq 可完整追溯。 +- `sessions output` 与 `sessions trace` 默认必须按渐进披露输出低噪声 JSON:只展示 `assistant_message` 与 `tool_call`/`error` 摘要,`command_output`、`backend_status`、raw event、runnerTrace 和大 stdout/stderr 只进入 `suppressedEvents` 计数与 bytes,不得默认展开正文。需要查看工具输出、backend_status 或原始 event 时,必须通过默认摘要中的 `detailCommands`,或显式使用 `--seq `、`--event-id `、`--item-id `、`--include-output`、`--full`/`--raw` 做定点展开;默认摘要生成的 `detailCommands` 必须带上能定位该 event 的最小 `--after-seq`/`--limit` hint,避免按 id 拉详情时重新扫描长 trace。这样保证默认不爆上下文,同时按 id/seq 可完整追溯。 ## 配置与 Secret 边界 diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 243260b..772775f 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -355,14 +355,16 @@ function withSessionDetailCommands(items: JsonRecord[], kind: "trace" | "output" const seq = numberValue(item.seq); const itemId = stringValue(item.itemId); const eventId = stringValue(item.eventId); - const detail = seq === null ? null : `./scripts/agentrun sessions ${kind} ${sessionId} --seq ${seq}${runId ? ` --run-id ${runId}` : ""} --full`; + const pageHint = seq === null ? "" : ` --after-seq ${Math.max(0, seq - 1)} --limit 1`; + const runFlag = runId ? ` --run-id ${runId}` : ""; + const detail = seq === null ? null : `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --seq ${seq}${runFlag} --full`; return { ...item, ...(detail || itemId || eventId ? { detailCommands: { ...(detail ? { seq: detail } : {}), - ...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId} --item-id ${itemId}${runId ? ` --run-id ${runId}` : ""} --full` } : {}), - ...(eventId ? { event: `./scripts/agentrun sessions ${kind} ${sessionId} --event-id ${eventId}${runId ? ` --run-id ${runId}` : ""} --full` } : {}), + ...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --item-id ${itemId}${runFlag} --full` } : {}), + ...(eventId ? { event: `./scripts/agentrun sessions ${kind} ${sessionId}${pageHint} --event-id ${eventId}${runFlag} --full` } : {}), }, } : {}), }; @@ -390,6 +392,13 @@ function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "o }, }); } + return sessionEventDetailResultFromMatches(page, matches, options); +} + +function sessionEventDetailResultFromMatches(page: JsonValue, matches: JsonRecord[], options: { kind: "trace" | "output"; sessionId: string; runId: string | null; summaryChars: number; filter: SessionEventDetailFilter; pagesScanned?: number; eventsScanned?: number }): JsonRecord { + const record = jsonRecordValue(page); + if (!record) throw new AgentRunError("schema-invalid", "sessions event response must be an object", { httpStatus: 2 }); + const events = eventPageItems(page); const runId = stringValue(record.runId) ?? options.runId; return { action: `session-${options.kind}-event-detail`, @@ -398,6 +407,8 @@ function sessionEventDetailResult(page: JsonValue, options: { kind: "trace" | "o filter: options.filter as unknown as JsonRecord, sourceCount: events.length, count: matches.length, + ...(options.pagesScanned === undefined ? {} : { pagesScanned: options.pagesScanned }), + ...(options.eventsScanned === undefined ? {} : { eventsScanned: options.eventsScanned }), valuesPrinted: false, items: matches.map((event) => ({ summary: summarizeRunEvent(event, options.summaryChars), @@ -829,15 +840,19 @@ async function listSessions(args: ParsedArgs): Promise { async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise { const params = new URLSearchParams(); const requestedSeq = optionalIntegerFlag(args, "seq", { min: 0 }); - const afterSeq = requestedSeq !== null && optionalFlag(args, "after-seq") === null ? Math.max(0, requestedSeq - 1) : integerFlag(args, "after-seq", 0, { min: 0 }); + const hasExplicitAfterSeq = optionalFlag(args, "after-seq") !== null; + const afterSeq = requestedSeq !== null && !hasExplicitAfterSeq ? Math.max(0, requestedSeq - 1) : integerFlag(args, "after-seq", 0, { min: 0 }); const limit = requestedSeq !== null && optionalFlag(args, "limit") === null ? 1 : integerFlag(args, "limit", 100, { min: 1, max: 500 }); const runId = optionalFlag(args, "run-id"); + const detailFilter = sessionEventDetailFilter(args, requestedSeq); + if (detailFilter && requestedSeq === null && !hasExplicitAfterSeq && (detailFilter.eventId || detailFilter.itemId)) { + return scanSessionEventDetail(args, { kind, sessionId, runId, afterSeq, limit, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter }); + } params.set("afterSeq", String(afterSeq)); params.set("limit", String(limit)); if (runId) params.set("runId", runId); const query = params.toString(); const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/${kind}${query ? `?${query}` : ""}`); - const detailFilter = sessionEventDetailFilter(args, requestedSeq); if (detailFilter) return sessionEventDetailResult(page, { kind, sessionId, runId, summaryChars: integerFlag(args, "summary-chars", 1_200, { min: 1, max: 8_000 }), filter: detailFilter }); if (wantsExpandedOutput(args)) return page; return summarizeSessionEventPage(page, { @@ -852,6 +867,41 @@ async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" }); } +async function scanSessionEventDetail(args: ParsedArgs, options: { kind: "trace" | "output"; sessionId: string; runId: string | null; afterSeq: number; limit: number; summaryChars: number; filter: SessionEventDetailFilter }): Promise { + const maxPages = integerFlag(args, "detail-scan-pages", 20, { min: 1, max: 100 }); + let afterSeq = options.afterSeq; + let pagesScanned = 0; + let eventsScanned = 0; + while (pagesScanned < maxPages) { + const params = new URLSearchParams(); + params.set("afterSeq", String(afterSeq)); + params.set("limit", String(options.limit)); + if (options.runId) params.set("runId", options.runId); + const query = params.toString(); + const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(options.sessionId)}/${options.kind}${query ? `?${query}` : ""}`); + const events = eventPageItems(page); + pagesScanned += 1; + eventsScanned += events.length; + const matches = events.filter((event) => matchesSessionEventFilter(event, options.filter)); + if (matches.length > 0) return sessionEventDetailResultFromMatches(page, matches, { kind: options.kind, sessionId: options.sessionId, runId: options.runId, summaryChars: options.summaryChars, filter: options.filter, pagesScanned, eventsScanned }); + const lastSeq = events.length > 0 ? numberValue(events[events.length - 1]?.seq) : null; + const cursorSeq = numberValue(jsonRecordValue(page)?.cursor); + const nextSeq = cursorSeq ?? lastSeq; + if (nextSeq === null || nextSeq <= afterSeq) break; + afterSeq = nextSeq; + } + throw new AgentRunError("schema-invalid", "no session event matched --event-id/--item-id in scanned pages", { + httpStatus: 2, + details: { + filter: options.filter as unknown as JsonRecord, + pagesScanned, + eventsScanned, + nextAfterSeq: afterSeq, + hint: "use the detailCommands from the summary, pass --seq, or add --after-seq/--limit near the event if the trace is longer than the scan window", + }, + }); +} + async function sessionCreate(args: ParsedArgs, positionalSessionId: string | null): Promise { const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId(); const profile = normalizeProfile(optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? "codex"); @@ -1581,8 +1631,8 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "sessions turn [sessionId] [--json-file |--json-stdin] [--prompt-file |--prompt-stdin|--prompt ] [--profile codex|deepseek|minimax-m3|dsflash-go||M3] [--runner-json-file |--runner-json-stdin]", "sessions steer [--prompt-file |--prompt-stdin|--prompt ]", "sessions cancel [--reason ] [--full|--raw]", - "sessions trace [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw]", - "sessions output [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--full|--raw]", + "sessions trace [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw]", + "sessions output [--after-seq ] [--limit ] [--run-id ] [--summary-chars ] [--include-output] [--seq |--event-id |--item-id ] [--detail-scan-pages ] [--full|--raw]", "sessions read [--reader-id ] [--full|--raw]", "commands create --type turn|steer|interrupt --json-file |--json-stdin", "commands show --run-id ", diff --git a/src/selftest/cases/15-cli-events-summary.ts b/src/selftest/cases/15-cli-events-summary.ts index 7355848..ef57cda 100644 --- a/src/selftest/cases/15-cli-events-summary.ts +++ b/src/selftest/cases/15-cli-events-summary.ts @@ -62,6 +62,7 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).command_output, 1); assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).backend_status, 1); assert.deepEqual(sessionItems.map((item) => item.type), ["tool_call"]); + assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).item as string).includes("--after-seq 264 --limit 1"), true); assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).item as string).includes("--item-id tool_noise"), true); assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true); assert.ok(Number((sessionSummary.suppressedEvents as JsonRecord).outputBytes) > 0); @@ -77,7 +78,7 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { count: 1, cursor: "266", }, { kind: "output", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: "run_noise", tail: null, summaryChars: 80, includeOutput: true }); - assert.equal(((sessionOutputIncluded.items as JsonRecord[])[0]?.detailCommands as JsonRecord).seq, "./scripts/agentrun sessions output sess_noise --seq 266 --run-id run_noise --full"); + assert.equal(((sessionOutputIncluded.items as JsonRecord[])[0]?.detailCommands as JsonRecord).seq, "./scripts/agentrun sessions output sess_noise --after-seq 265 --limit 1 --seq 266 --run-id run_noise --full"); assert.equal(JSON.stringify(sessionOutputIncluded).includes("runner trace line"), false); const queueSummary = summarizeQueueDispatchResult({