From e0f1a142f42f145f1d9036a74471c9a4b90322a7 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 10 Jun 2026 08:29:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20queue=20dry-run=20?= =?UTF-8?q?=E4=B8=8E=E9=BB=98=E8=AE=A4=E6=91=98=E8=A6=81=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/reference/spec-v01-cli.md | 24 +- docs/reference/spec-v01-queue.md | 21 +- scripts/src/cli.ts | 247 ++++++++++++++++++-- src/selftest/cases/15-cli-events-summary.ts | 50 +++- 4 files changed, 302 insertions(+), 40 deletions(-) diff --git a/docs/reference/spec-v01-cli.md b/docs/reference/spec-v01-cli.md index d3ff71b..662d34b 100644 --- a/docs/reference/spec-v01-cli.md +++ b/docs/reference/spec-v01-cli.md @@ -59,15 +59,15 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 ./scripts/agentrun server status [--port ] ./scripts/agentrun server logs [--port ] [--tail-bytes ] [--log-file ] ./scripts/agentrun server stop [--port ] -./scripts/agentrun queue submit --json-file -./scripts/agentrun queue list [--queue ] [--state ] [--cursor ] [--limit ] -./scripts/agentrun queue show +./scripts/agentrun queue submit --json-file [--dry-run] +./scripts/agentrun queue list [--queue ] [--state ] [--cursor ] [--limit ] [--full|--raw] +./scripts/agentrun queue show [--full|--raw] ./scripts/agentrun queue stats [--queue ] -./scripts/agentrun queue commander [--queue ] -./scripts/agentrun queue read -./scripts/agentrun queue cancel [--reason ] -./scripts/agentrun queue dispatch [--json-file ] -./scripts/agentrun queue refresh +./scripts/agentrun queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw] +./scripts/agentrun queue read [--reader-id ] [--dry-run] [--full|--raw] +./scripts/agentrun queue cancel [--reason ] [--dry-run] [--full|--raw] +./scripts/agentrun queue dispatch [--json-file ] [--dry-run] [--full|--raw] +./scripts/agentrun queue refresh [--dry-run] [--full|--raw] ./scripts/agentrun sessions ps [--state default|running|unread|terminal|idle|all] [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--reader-id ] ./scripts/agentrun sessions show [--reader-id ] ./scripts/agentrun sessions turn [sessionId] --json-file --prompt-file [--profile codex|deepseek|minimax-m3|dsflash-go|M3] [--runner-json-file ] [--no-runner-job] @@ -95,9 +95,11 @@ CLI 官方 TypeScript 入口固定为 `scripts/agentrun-cli.ts`。在 G14 非交 - `secrets codex render --dry-run` 返回 Codex stdio profile Secret 创建计划、输入文件 bytes/hash、SecretRef、manifest 摘要和 apply 命令形状;`--profile codex` 默认 Secret name 为 `agentrun-v01-provider-codex`,`--profile deepseek` 默认 Secret name 为 `agentrun-v01-provider-deepseek`,`--profile minimax-m3` 默认 Secret name 为 `agentrun-v01-provider-minimax-m3`,`--profile dsflash-go` 默认 Secret name 为 `agentrun-v01-provider-dsflash-go` 并包含 `model-catalog.json`;它不得输出 Secret value 或执行 Kubernetes 写操作。 - `provider-profiles` 命令族调用 manager REST 管理 API,覆盖 profile status、删除、API Key 写入和 canary 验证。`set-key --key-stdin` 从 stdin 读取 API Key,响应只显示 SecretRef、resourceVersion、hash 后缀和 failureKind;不得输出 key、Codex auth/config 或 Secret data。 - `backends list` 必须显示 `codex`、`deepseek`、`minimax-m3` 与 `dsflash-go` profile 的 backendKind、protocol、transport、command、requiredSecretKeys 和状态;`dsflash-go` 的 `requiredSecretKeys` 必须包含 `model-catalog.json`;已配置的动态 provider profile(例如 `hy`)必须同样可见,并带动态 discovery 状态;不得因为某个 provider Secret 尚未配置就隐藏 capability。 -- `queue dispatch` 是 Q2 的受控手动调度入口,只对单个 task 显式创建 attempt 和 Core run/command/runner job;不得伪装成自动 scheduler。 -- `queue refresh` 只根据 Queue task 中保存的 Core run/command 引用回写 Queue attempt 状态,不读取 Core trace 反推 commander 或统计。 -- `queue show` 必须返回 task/attempt summary、state、read cursor、stats 相关字段和 `sessionPath`;不得返回或代理完整 output/trace。 +- `queue submit/read/cancel/dispatch/refresh --dry-run` 必须只返回 non-mutating plan,固定 `dryRun=true`、`mutation=false`,不得创建 task、mark read、cancel、dispatch、refresh 或启动 runner job。 +- `queue dispatch` 是 Q2 的受控手动调度入口,只对单个 task 显式创建 attempt 和 Core run/command/runner job;不得伪装成自动 scheduler;带 `--dry-run` 时只读取 task 并展示将要 POST 的路径和有界 request 摘要。 +- `queue refresh` 只根据 Queue task 中保存的 Core run/command 引用回写 Queue attempt 状态,不读取 Core trace 反推 commander 或统计;带 `--dry-run` 时不得写回状态。 +- `queue list/show/commander` 默认返回低噪声 summary,只显示 task/attempt/session ids、state、read cursor、stats 相关字段和 drill-down 命令;需要完整 task payload、resource bundle 或 metadata 时显式使用 `--full|--raw`。 +- `queue show` 不得返回或代理完整 output/trace;输出和 trace 只能通过返回的 `sessionPath` 对应 `sessions ...` 命令查询。 - `sessions ps` 默认只显示 running 和 unread session;`--state all` 才显示历史 read session,避免旧 session 噪声淹没当前进度。 - `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 中消失。 diff --git a/docs/reference/spec-v01-queue.md b/docs/reference/spec-v01-queue.md index e5a7949..c6eac24 100644 --- a/docs/reference/spec-v01-queue.md +++ b/docs/reference/spec-v01-queue.md @@ -77,15 +77,15 @@ Queue task 详情必须返回 session 引用,而不是代理输出或 trace: AgentRun CLI 必须提供 Queue 和 Session 两组命令。Queue 命令只操作队列资源: ```bash -./scripts/agentrun queue submit --json-file -./scripts/agentrun queue list [--queue ] [--state ] [--cursor ] [--limit ] -./scripts/agentrun queue show +./scripts/agentrun queue submit --json-file [--dry-run] +./scripts/agentrun queue list [--queue ] [--state ] [--cursor ] [--limit ] [--full|--raw] +./scripts/agentrun queue show [--full|--raw] ./scripts/agentrun queue stats [--queue ] -./scripts/agentrun queue commander [--queue ] -./scripts/agentrun queue read -./scripts/agentrun queue cancel [--reason ] -./scripts/agentrun queue dispatch [--json-file ] -./scripts/agentrun queue refresh +./scripts/agentrun queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw] +./scripts/agentrun queue read [--reader-id ] [--dry-run] [--full|--raw] +./scripts/agentrun queue cancel [--reason ] [--dry-run] [--full|--raw] +./scripts/agentrun queue dispatch [--json-file ] [--dry-run] [--full|--raw] +./scripts/agentrun queue refresh [--dry-run] [--full|--raw] ``` Session 命令负责输出、trace 和会话控制: @@ -101,7 +101,7 @@ Session 命令负责输出、trace 和会话控制: ./scripts/agentrun sessions read [--reader-id ] ``` -不得新增 `queue output`、`queue trace` 或 `queue session/*` 这类子路径代理。`queue show` 最多打印 `sessionPath` 和下一步 `sessions ...` 命令。 +不得新增 `queue output`、`queue trace` 或 `queue session/*` 这类子路径代理。`queue list/show/commander` 默认输出低噪声 summary,最多打印 task/attempt/session ids、状态、统计、`sessionPath` 和下一步 `sessions ...` 命令;完整 payload/resource bundle/metadata 只能通过显式 `--full|--raw` 展开。Queue mutation 命令带 `--dry-run` 时必须只返回 `mutation=false` 的计划,不得写 Queue、Core run/command 或 runner job。 ## 数据模型方向 @@ -160,7 +160,8 @@ Queue Q2 的真实手动验收必须覆盖以下稳定边界: - `queue submit` 只创建 Queue task,不触发自动 scheduler。 - `queue dispatch` 是受控手动调度入口,必须创建 Core run、command 和 runner job,并把 attempt 引用写回 Queue task。 - `queue refresh` 只读取 Queue task 保存的 run/command 引用,将 Core 终态回写到 Queue task 和 latestAttempt;不得读取 Core trace、Session trace 或 events 来反推 Queue stats/commander。 -- `queue show/list/stats/commander` 的统计口径必须来自 Queue 模型;输出、trace 和会话控制继续由 Session API/CLI 承接。 +- `queue submit/read/cancel/dispatch/refresh --dry-run` 必须只返回计划,不得改变 task state、read cursor、attempt、run、command 或 runner job。 +- `queue show/list/stats/commander` 的统计口径必须来自 Queue 模型;默认输出必须有界,输出、trace 和会话控制继续由 Session API/CLI 承接。 - 综合联调里的 `workspaceRef` 必须是 runner 实际可访问且能启动 backend command 的工作区。`opaque` workspace 可用于不依赖 Git checkout 的最小 Queue dispatch 验收;`host-path` 只有在该 path 在 runner 容器/进程内可访问且不破坏 Codex command resolution 时才能作为通过证据。 - 因 workspace 形态导致的 backend command spawn 失败,应归类为 runtime workspace/command 一致性问题,不能误判为 Queue dispatch 或 Queue refresh 失败。 diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 0fa4750..b77fcef 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -80,13 +80,13 @@ async function dispatch(args: ParsedArgs): Promise { if (sessionStorageCmd && id) return sessionStorageDelete(args, id); if (group === "queue" && command === "submit") return submitQueueTask(args); if (group === "queue" && command === "list") return listQueueTasks(args); - if (group === "queue" && command === "show" && id) return client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(id)}`); + if (group === "queue" && command === "show" && id) return showQueueTask(args, id); if (group === "queue" && command === "stats") return client(args).get(`/api/v1/queue/stats${queueQuery(args)}`); - if (group === "queue" && command === "commander") return client(args).get(`/api/v1/queue/commander${queueQuery(args, { readerId: true })}`); - if (group === "queue" && command === "read" && id) return client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(id)}/read`, { readerId: optionalFlag(args, "reader-id") ?? "cli" }); - if (group === "queue" && command === "cancel" && id) return client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(id)}/cancel`, cancelBody(args)); + if (group === "queue" && command === "commander") return queueCommander(args); + if (group === "queue" && command === "read" && id) return readQueueTask(args, id); + if (group === "queue" && command === "cancel" && id) return cancelQueueTask(args, id); if (group === "queue" && command === "dispatch" && id) return dispatchQueueTask(args, id); - if (group === "queue" && command === "refresh" && id) return client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(id)}/refresh`, {}); + if (group === "queue" && command === "refresh" && id) return refreshQueueTask(args, id); if (group === "runs" && command === "create") return client(args).post("/api/v1/runs", await jsonFile(args)); if (group === "runs" && command === "show" && id) return client(args).get(`/api/v1/runs/${encodeURIComponent(id)}`); if (group === "runs" && command === "events" && id) return runEvents(args, id); @@ -420,6 +420,169 @@ function summarizeSessionMutationResult(action: "session-cancel" | "session-read }; } +interface QueueSummaryOptions { + limit: number; +} + +export function summarizeQueueTaskListResult(result: JsonValue, options: QueueSummaryOptions): JsonRecord { + const record = jsonRecordValue(result); + if (!record) throw new AgentRunError("schema-invalid", "queue list response must be an object", { httpStatus: 2 }); + const items = queueItems(record.items); + const selected = items.slice(0, options.limit); + return { + action: "queue-list-summary", + queue: stringValue(record.queue), + state: stringValue(record.state), + cursor: stringValue(record.cursor), + nextCursor: stringValue(record.nextCursor), + sourceCount: items.length, + displayedCount: selected.length, + limit: options.limit, + items: selected.map((item) => summarizeQueueTaskWithAttempt(item, stringValue(item.id) ?? "unknown")), + fullResponseBytes: jsonByteLength(result), + valuesPrinted: false, + drillDownCommands: { + full: "./scripts/agentrun queue list --full", + raw: "./scripts/agentrun queue list --raw", + next: stringValue(record.nextCursor) ? `./scripts/agentrun queue list --cursor ${String(record.nextCursor)} --limit ${options.limit}` : null, + }, + }; +} + +export function summarizeQueueTaskShowResult(result: JsonValue, taskId: string): JsonRecord { + const record = jsonRecordValue(result); + if (!record) throw new AgentRunError("schema-invalid", "queue show response must be an object", { httpStatus: 2 }); + const sessionId = stringValue(jsonRecordValue(record.sessionRef)?.sessionId) ?? stringValue(jsonRecordValue(record.latestAttempt)?.sessionId); + return { + action: "queue-show-summary", + task: summarizeQueueTaskWithAttempt(record, taskId), + payloadBytes: jsonByteLength(record.payload), + resourceBundleBytes: jsonByteLength(record.resourceBundleRef), + referencesCount: Array.isArray(record.references) ? record.references.length : null, + metadataKeys: Object.keys(jsonRecordValue(record.metadata) ?? {}).sort().slice(0, 24), + fullResponseBytes: jsonByteLength(result), + valuesPrinted: false, + pollCommands: { + full: `./scripts/agentrun queue show ${taskId} --full`, + ...(sessionId ? { trace: `./scripts/agentrun sessions trace ${sessionId} --after-seq 0 --limit 100`, output: `./scripts/agentrun sessions output ${sessionId} --after-seq 0 --limit 100` } : {}), + }, + }; +} + +export function summarizeQueueCommanderSnapshot(result: JsonValue, options: QueueSummaryOptions): JsonRecord { + const record = jsonRecordValue(result); + if (!record) throw new AgentRunError("schema-invalid", "queue commander response must be an object", { httpStatus: 2 }); + const items = queueItems(record.items); + const selected = items.slice(0, options.limit); + return { + action: "queue-commander-summary", + queue: stringValue(record.queue), + readerId: stringValue(record.readerId), + stats: summarizeQueueStats(jsonRecordValue(record.stats)), + sourceCount: items.length, + displayedCount: selected.length, + limit: options.limit, + unreadCount: items.filter((item) => item.unread === true).length, + activeCount: items.filter((item) => item.active === true || stringValue(item.attentionState) === "active").length, + items: selected.map((item) => summarizeQueueTaskWithAttempt(item, stringValue(item.id) ?? "unknown")), + generatedAt: stringValue(record.generatedAt), + fullResponseBytes: jsonByteLength(result), + valuesPrinted: false, + drillDownCommands: { + full: "./scripts/agentrun queue commander --reader-id cli --full", + raw: "./scripts/agentrun queue commander --reader-id cli --raw", + item: "./scripts/agentrun queue show ", + trace: "./scripts/agentrun sessions trace --after-seq 0 --limit 100", + output: "./scripts/agentrun sessions output --after-seq 0 --limit 100", + }, + }; +} + +function summarizeQueueTaskMutationResult(action: "queue-read" | "queue-cancel" | "queue-refresh", taskId: string, result: JsonValue, flags: JsonRecord): JsonRecord { + const record = jsonRecordValue(result); + return { + action, + taskId, + mutation: true, + ...flags, + task: summarizeQueueTaskWithAttempt(record, taskId), + fullResponseBytes: jsonByteLength(result), + valuesPrinted: false, + drillDownCommands: { + show: `./scripts/agentrun queue show ${taskId}`, + full: `./scripts/agentrun queue show ${taskId} --full`, + }, + }; +} + +function queueMutationDryRunPlan(action: string, taskId: string | null, pathValue: string, body: JsonRecord, method: "POST", confirmCommand: string, task?: JsonValue): JsonRecord { + return { + action: `${action}-plan`, + dryRun: true, + mutation: false, + taskId, + request: { + method, + path: pathValue, + body: summarizeMutationBody(body), + bodyBytes: jsonByteLength(body), + valuesPrinted: false, + }, + ...(task === undefined ? {} : { task: summarizeQueueTaskWithAttempt(jsonRecordValue(task), taskId ?? stringValue(jsonRecordValue(task)?.id) ?? "unknown") }), + next: { + confirm: confirmCommand, + note: "Remove --dry-run to perform the mutation.", + }, + valuesPrinted: false, + }; +} + +function summarizeMutationBody(body: JsonRecord): JsonRecord { + return { + ...compactRecord(body, { keys: ["idempotencyKey", "image", "namespace", "attemptId", "runnerId", "sourceCommit", "managerUrl", "serviceAccountName", "readerId", "reason"] }), + keys: Object.keys(body).sort().slice(0, 32), + payloadBytes: jsonByteLength(body.payload), + executionPolicyBytes: jsonByteLength(body.executionPolicy), + resourceBundleBytes: jsonByteLength(body.resourceBundleRef), + valuesPrinted: false, + }; +} + +function summarizeQueueStats(record: JsonRecord | null): JsonRecord | null { + if (!record) return null; + return { + queue: stringValue(record.queue), + total: numberValue(record.total), + maxVersion: numberValue(record.maxVersion), + byState: jsonRecordValue(record.byState) ?? {}, + byLane: jsonRecordValue(record.byLane) ?? {}, + byBackendProfile: jsonRecordValue(record.byBackendProfile) ?? {}, + generatedAt: stringValue(record.generatedAt), + fullRecordBytes: jsonByteLength(record), + valuesPrinted: false, + }; +} + +function summarizeQueueTaskWithAttempt(record: JsonRecord | null, fallbackTaskId: string): JsonRecord { + const summary = summarizeQueueTaskRecord(record, fallbackTaskId); + const latestAttempt = summarizeAttemptRecord(jsonRecordValue(record?.latestAttempt)); + const sessionRef = jsonRecordValue(record?.sessionRef); + if (latestAttempt) summary.latestAttempt = latestAttempt; + const sessionId = stringValue(sessionRef?.sessionId) ?? stringValue(latestAttempt?.sessionId); + if (sessionId) summary.sessionId = sessionId; + if (record?.readCursor !== undefined) summary.read = record.readCursor !== null; + return summary; +} + +function queueItems(value: JsonValue | undefined): JsonRecord[] { + if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "queue response.items must be an array", { httpStatus: 2 }); + return value.map((item) => { + const record = jsonRecordValue(item); + if (!record) throw new AgentRunError("schema-invalid", "queue response item must be an object", { httpStatus: 2 }); + return record; + }); +} + function sessionEventDrillDownCommands(kind: "trace" | "output", sessionId: string, options: { afterSeq: number; limit: number; runId: string | null }): JsonRecord { const base = `./scripts/agentrun sessions ${kind} ${sessionId} --after-seq ${options.afterSeq} --limit ${options.limit}${options.runId ? ` --run-id ${options.runId}` : ""}`; return { @@ -432,7 +595,7 @@ function sessionEventDrillDownCommands(kind: "trace" | "output", sessionId: stri function summarizeQueueTaskRecord(record: JsonRecord | null, fallbackTaskId: string): JsonRecord { return compactRecord(record, { fallback: { id: fallbackTaskId }, - keys: ["id", "state", "queue", "lane", "title", "priority", "backendProfile", "providerId", "sessionPath", "version", "updatedAt", "cancelledAt", "cancelReason"], + keys: ["id", "state", "queue", "lane", "title", "priority", "backendProfile", "providerId", "sessionPath", "version", "updatedAt", "cancelledAt", "cancelReason", "readerId", "attentionState", "unread", "active"], }); } @@ -684,6 +847,9 @@ async function submitQueueTask(args: ParsedArgs): Promise { const body = await jsonFile(args); const idempotencyKey = optionalFlag(args, "idempotency-key"); if (idempotencyKey) body.idempotencyKey = idempotencyKey; + if (args.flags.get("dry-run") === true) { + return queueMutationDryRunPlan("queue-submit", null, "/api/v1/queue/tasks", body, "POST", "./scripts/agentrun queue submit --json-file "); + } return client(args).post("/api/v1/queue/tasks", body); } @@ -700,7 +866,21 @@ async function listQueueTasks(args: ParsedArgs): Promise { if (limit) params.set("limit", limit); if (updatedAfter) params.set("updatedAfter", updatedAfter); const query = params.toString(); - return client(args).get(`/api/v1/queue/tasks${query ? `?${query}` : ""}`); + const result = await client(args).get(`/api/v1/queue/tasks${query ? `?${query}` : ""}`); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueTaskListResult(result, { limit: integerFlag(args, "limit", 20, { min: 1, max: 100 }) }); +} + +async function showQueueTask(args: ParsedArgs, taskId: string): Promise { + const result = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueTaskShowResult(result, taskId); +} + +async function queueCommander(args: ParsedArgs): Promise { + const result = await client(args).get(`/api/v1/queue/commander${queueQuery(args, { readerId: true })}`); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueCommanderSnapshot(result, { limit: integerFlag(args, "limit", 20, { min: 1, max: 100 }) }); } function queueQuery(args: ParsedArgs, options: { readerId?: boolean } = {}): string { @@ -716,6 +896,17 @@ function queueQuery(args: ParsedArgs, options: { readerId?: boolean } = {}): str } async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise { + const body = await queueDispatchBody(args); + if (args.flags.get("dry-run") === true) { + const task = await client(args).get(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}`); + return queueMutationDryRunPlan("queue-dispatch", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body, "POST", `./scripts/agentrun queue dispatch ${taskId} --json-file `, task); + } + const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/dispatch`, body); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueDispatchResult(result, taskId); +} + +async function queueDispatchBody(args: ParsedArgs): Promise { const body = await optionalJsonFile(args); const copy = (flagName: string, key = flagName.replace(/-([a-z])/gu, (_, letter: string) => letter.toUpperCase())): void => { const value = optionalFlag(args, flagName); @@ -729,9 +920,31 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise { + const body = { readerId: optionalFlag(args, "reader-id") ?? "cli" }; + if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-read", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/read`, body, "POST", `./scripts/agentrun queue read ${taskId}`); + const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/read`, body); if (wantsExpandedOutput(args)) return result; - return summarizeQueueDispatchResult(result, taskId); + return summarizeQueueTaskMutationResult("queue-read", taskId, result, { read: true }); +} + +async function cancelQueueTask(args: ParsedArgs, taskId: string): Promise { + const body = cancelBody(args); + if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-cancel", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/cancel`, body, "POST", `./scripts/agentrun queue cancel ${taskId}${body.reason ? " --reason " : ""}`); + const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/cancel`, body); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueTaskMutationResult("queue-cancel", taskId, result, { cancelled: true }); +} + +async function refreshQueueTask(args: ParsedArgs, taskId: string): Promise { + const body: JsonRecord = {}; + if (args.flags.get("dry-run") === true) return queueMutationDryRunPlan("queue-refresh", taskId, `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/refresh`, body, "POST", `./scripts/agentrun queue refresh ${taskId}`); + const result = await client(args).post(`/api/v1/queue/tasks/${encodeURIComponent(taskId)}/refresh`, body); + if (wantsExpandedOutput(args)) return result; + return summarizeQueueTaskMutationResult("queue-refresh", taskId, result, { refreshed: true }); } async function showRunnerJobStatus(args: ParsedArgs): Promise { @@ -1245,15 +1458,15 @@ function help(args: ParsedArgs, group?: string): JsonRecord { "runner job --dry-run --run-id --command-id --image ", "runner jobs --run-id [--command-id ]", "runner job-status [runnerJobId] --run-id ", - "queue submit --json-file [--idempotency-key ]", - "queue list [--queue ] [--state ] [--cursor ] [--limit ] [--updated-after ]", - "queue show ", + "queue submit --json-file [--idempotency-key ] [--dry-run]", + "queue list [--queue ] [--state ] [--cursor ] [--limit ] [--updated-after ] [--full|--raw]", + "queue show [--full|--raw]", "queue stats [--queue ]", - "queue commander [--queue ] [--reader-id ]", - "queue read [--reader-id ]", - "queue cancel [--reason ]", - "queue dispatch [--json-file ] [--idempotency-key ] [--image ] [--namespace ] [--full|--raw]", - "queue refresh ", + "queue commander [--queue ] [--reader-id ] [--limit ] [--full|--raw]", + "queue read [--reader-id ] [--dry-run] [--full|--raw]", + "queue cancel [--reason ] [--dry-run] [--full|--raw]", + "queue dispatch [--json-file ] [--idempotency-key ] [--image ] [--namespace ] [--dry-run] [--full|--raw]", + "queue refresh [--dry-run] [--full|--raw]", "secrets codex render --dry-run [--profile codex|deepseek|minimax-m3|dsflash-go|] [--codex-home ] [--model-catalog-file ] [--namespace agentrun-v01] [--secret-name ]", "provider-profiles list", "provider-profiles show ", diff --git a/src/selftest/cases/15-cli-events-summary.ts b/src/selftest/cases/15-cli-events-summary.ts index b039558..202d460 100644 --- a/src/selftest/cases/15-cli-events-summary.ts +++ b/src/selftest/cases/15-cli-events-summary.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import type { JsonRecord } from "../../common/types.js"; import { assertNoSecretLeak, type SelfTestContext, type SelfTestResult } from "../harness.js"; -import { renderRunEventSummaryTsv, summarizeQueueDispatchResult, summarizeRunEventPage, summarizeSessionEventPage } from "../../../scripts/src/cli.js"; +import { renderRunEventSummaryTsv, summarizeQueueCommanderSnapshot, summarizeQueueDispatchResult, summarizeQueueTaskListResult, summarizeQueueTaskShowResult, summarizeRunEventPage, summarizeSessionEventPage } from "../../../scripts/src/cli.js"; export default function selfTest(_context: SelfTestContext): SelfTestResult { const summary = summarizeRunEventPage({ items: [ @@ -74,5 +74,51 @@ export default function selfTest(_context: SelfTestContext): SelfTestResult { assert.equal(((queueSummary.expandedOutput as JsonRecord).fullFlag), "--full"); assertNoSecretLeak(queueSummary); - return { name: "15-cli-events-summary", tests: ["runs-events-summary-tail", "runs-events-summary-tsv-redaction", "sessions-events-low-noise-summary", "queue-dispatch-low-noise-summary"] }; + const noisyTask = { + id: "qt_noisy", + state: "completed", + queue: "dev", + lane: "case", + title: "noisy queue task", + priority: 50, + backendProfile: "codex", + providerId: "G14", + version: 9, + payload: { prompt: hugeRunnerTrace }, + latestAttempt: { attemptId: "attempt_noisy", state: "completed", runId: "run_noisy", commandId: "cmd_noisy", runnerJobId: "rj_noisy", sessionId: "sess_noisy", sessionPath: "/api/v1/sessions/sess_noisy" }, + sessionRef: { sessionId: "sess_noisy" }, + unread: true, + active: false, + attentionState: "unread", + }; + + const commanderSummary = summarizeQueueCommanderSnapshot({ + queue: null, + readerId: "cli", + stats: { queue: null, total: 2, maxVersion: 9, byState: { completed: 1 }, byLane: { case: 1 }, byBackendProfile: { codex: 1 }, generatedAt: "2026-06-10T00:00:00.000Z" }, + items: [noisyTask, { ...noisyTask, id: "qt_hidden" }], + generatedAt: "2026-06-10T00:00:00.000Z", + }, { limit: 1 }); + assert.equal(commanderSummary.action, "queue-commander-summary"); + assert.equal(commanderSummary.sourceCount, 2); + assert.equal(commanderSummary.displayedCount, 1); + assert.equal(JSON.stringify(commanderSummary).includes("runner trace line"), false); + assert.equal((((commanderSummary.items as JsonRecord[])[0]?.latestAttempt as JsonRecord).sessionId), "sess_noisy"); + assert.equal(String(((commanderSummary.drillDownCommands as JsonRecord).full)).includes("--full"), true); + assertNoSecretLeak(commanderSummary); + + const listSummary = summarizeQueueTaskListResult({ items: [noisyTask], nextCursor: "9" }, { limit: 20 }); + assert.equal(listSummary.action, "queue-list-summary"); + assert.equal(JSON.stringify(listSummary).includes("runner trace line"), false); + assert.equal(String(((listSummary.drillDownCommands as JsonRecord).next)).includes("--cursor 9"), true); + assertNoSecretLeak(listSummary); + + const showSummary = summarizeQueueTaskShowResult(noisyTask, "qt_noisy"); + assert.equal(showSummary.action, "queue-show-summary"); + assert.equal(JSON.stringify(showSummary).includes("runner trace line"), false); + assert.ok(Number(showSummary.payloadBytes) > 10_000); + assert.equal(String(((showSummary.pollCommands as JsonRecord).trace)).includes("sess_noisy"), true); + assertNoSecretLeak(showSummary); + + return { name: "15-cli-events-summary", tests: ["runs-events-summary-tail", "runs-events-summary-tsv-redaction", "sessions-events-low-noise-summary", "queue-dispatch-low-noise-summary", "queue-list-show-commander-low-noise-summary"] }; }