fix: 收敛 session detail 按 id 精确拉取 (#137)

Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
Lyon
2026-06-10 10:05:43 +08:00
committed by GitHub
parent ecbe1368ba
commit 2e95276db8
3 changed files with 62 additions and 11 deletions
+3 -3
View File
@@ -73,8 +73,8 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交
./scripts/agentrun sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>|--runner-json-stdin] [--no-runner-job] ./scripts/agentrun sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file <job.json>|--runner-json-stdin] [--no-runner-job]
./scripts/agentrun sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>] ./scripts/agentrun sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]
./scripts/agentrun sessions cancel <sessionId> [--reason <text>] ./scripts/agentrun sessions cancel <sessionId> [--reason <text>]
./scripts/agentrun sessions trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw] ./scripts/agentrun sessions trace <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]
./scripts/agentrun sessions output <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw] ./scripts/agentrun sessions output <sessionId> [--after-seq <n>] [--limit <limit>] [--run-id <runId>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]
./scripts/agentrun sessions read <sessionId> [--reader-id <reader>] ./scripts/agentrun sessions read <sessionId> [--reader-id <reader>]
``` ```
@@ -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 aliasprofile 仍写入 canonical `backendProfile`,不得 fallback。 - `sessions turn` 是异步 subagent 的受控 CLI 入口:短返回 run、command、runnerJob 和后续 poll/read/steer/cancel 命令,不等待模型完成。`--profile M3``minimax-m3` 的 CLI aliasprofile 仍写入 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 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` 是输出和 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 <n>``--event-id <id>``--item-id <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 <n>``--event-id <id>``--item-id <id>``--include-output``--full`/`--raw` 做定点展开;默认摘要生成的 `detailCommands` 必须带上能定位该 event 的最小 `--after-seq`/`--limit` hint,避免按 id 拉详情时重新扫描长 trace。这样保证默认不爆上下文,同时按 id/seq 可完整追溯。
## 配置与 Secret 边界 ## 配置与 Secret 边界
+57 -7
View File
@@ -355,14 +355,16 @@ function withSessionDetailCommands(items: JsonRecord[], kind: "trace" | "output"
const seq = numberValue(item.seq); const seq = numberValue(item.seq);
const itemId = stringValue(item.itemId); const itemId = stringValue(item.itemId);
const eventId = stringValue(item.eventId); 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 { return {
...item, ...item,
...(detail || itemId || eventId ? { ...(detail || itemId || eventId ? {
detailCommands: { detailCommands: {
...(detail ? { seq: detail } : {}), ...(detail ? { seq: detail } : {}),
...(itemId ? { item: `./scripts/agentrun sessions ${kind} ${sessionId} --item-id ${itemId}${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} --event-id ${eventId}${runId ? ` --run-id ${runId}` : ""} --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; const runId = stringValue(record.runId) ?? options.runId;
return { return {
action: `session-${options.kind}-event-detail`, 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, filter: options.filter as unknown as JsonRecord,
sourceCount: events.length, sourceCount: events.length,
count: matches.length, count: matches.length,
...(options.pagesScanned === undefined ? {} : { pagesScanned: options.pagesScanned }),
...(options.eventsScanned === undefined ? {} : { eventsScanned: options.eventsScanned }),
valuesPrinted: false, valuesPrinted: false,
items: matches.map((event) => ({ items: matches.map((event) => ({
summary: summarizeRunEvent(event, options.summaryChars), summary: summarizeRunEvent(event, options.summaryChars),
@@ -829,15 +840,19 @@ async function listSessions(args: ParsedArgs): Promise<JsonValue> {
async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise<JsonValue> { async function sessionEvents(args: ParsedArgs, sessionId: string, kind: "trace" | "output"): Promise<JsonValue> {
const params = new URLSearchParams(); const params = new URLSearchParams();
const requestedSeq = optionalIntegerFlag(args, "seq", { min: 0 }); 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 limit = requestedSeq !== null && optionalFlag(args, "limit") === null ? 1 : integerFlag(args, "limit", 100, { min: 1, max: 500 });
const runId = optionalFlag(args, "run-id"); 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("afterSeq", String(afterSeq));
params.set("limit", String(limit)); params.set("limit", String(limit));
if (runId) params.set("runId", runId); if (runId) params.set("runId", runId);
const query = params.toString(); const query = params.toString();
const page = await client(args).get(`/api/v1/sessions/${encodeURIComponent(sessionId)}/${kind}${query ? `?${query}` : ""}`); 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 (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; if (wantsExpandedOutput(args)) return page;
return summarizeSessionEventPage(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<JsonRecord> {
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<JsonRecord> { async function sessionCreate(args: ParsedArgs, positionalSessionId: string | null): Promise<JsonRecord> {
const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId(); const sessionId = positionalSessionId ?? optionalFlag(args, "session-id") ?? newSessionId();
const profile = normalizeProfile(optionalFlag(args, "profile") ?? optionalFlag(args, "backend-profile") ?? "codex"); 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 <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>|--runner-json-stdin]", "sessions turn [sessionId] [--json-file <run-base.json>|--json-stdin] [--prompt-file <file>|--prompt-stdin|--prompt <text>] [--profile codex|deepseek|minimax-m3|dsflash-go|<dynamic-profile>|M3] [--runner-json-file <job.json>|--runner-json-stdin]",
"sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]", "sessions steer <sessionId> [--prompt-file <file>|--prompt-stdin|--prompt <text>]",
"sessions cancel <sessionId> [--reason <text>] [--full|--raw]", "sessions cancel <sessionId> [--reason <text>] [--full|--raw]",
"sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]", "sessions trace <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]",
"sessions output <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--full|--raw]", "sessions output <sessionId> [--after-seq <n>] [--limit <n>] [--run-id <runId>] [--summary-chars <n>] [--include-output] [--seq <n>|--event-id <id>|--item-id <id>] [--detail-scan-pages <n>] [--full|--raw]",
"sessions read <sessionId> [--reader-id <reader>] [--full|--raw]", "sessions read <sessionId> [--reader-id <reader>] [--full|--raw]",
"commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin", "commands create <runId> --type turn|steer|interrupt --json-file <payload.json>|--json-stdin",
"commands show <commandId> --run-id <runId>", "commands show <commandId> --run-id <runId>",
+2 -1
View File
@@ -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).command_output, 1);
assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).backend_status, 1); assert.equal(((sessionSummary.suppressedEvents as JsonRecord).byType as JsonRecord).backend_status, 1);
assert.deepEqual(sessionItems.map((item) => item.type), ["tool_call"]); 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).item as string).includes("--item-id tool_noise"), true);
assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true); assert.equal(((sessionItems[0]?.detailCommands as JsonRecord).seq as string).includes("--seq 265"), true);
assert.ok(Number((sessionSummary.suppressedEvents as JsonRecord).outputBytes) > 0); assert.ok(Number((sessionSummary.suppressedEvents as JsonRecord).outputBytes) > 0);
@@ -77,7 +78,7 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult {
count: 1, count: 1,
cursor: "266", cursor: "266",
}, { kind: "output", sessionId: "sess_noise", afterSeq: 250, limit: 100, runId: "run_noise", tail: null, summaryChars: 80, includeOutput: true }); }, { 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); assert.equal(JSON.stringify(sessionOutputIncluded).includes("runner trace line"), false);
const queueSummary = summarizeQueueDispatchResult({ const queueSummary = summarizeQueueDispatchResult({