diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 91a3f969..789607f6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -47,7 +47,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI - `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。长 prompt、多行 prompt、含引号/反引号/Markdown 表格/JSON/反斜杠的 prompt 必须优先用 `--prompt-stdin` 或 `--prompt-file`,不要拼进 shell 单个参数;位置参数只适合短单行 smoke prompt。stdin 推荐用 quoted heredoc:`cat <<'PROMPT' | bun scripts/cli.ts codex submit --prompt-stdin --queue --dry-run`,文件路径推荐 `bun scripts/cli.ts codex submit --prompt-file /tmp/code-queue-prompt.md --queue --dry-run`,确认 dry-run 后移除 `--dry-run` 提交同一 payload。dry-run 会额外输出 `routingRecommendation`,包含推荐 route、runner、model、风险信号、prompt 自包含/issue 非唯一来源/prod-secret-DB 禁止/运行态或 release 禁止/证据要求/中等复杂度候选等 guard 状态;同时输出 `policyContract`,固定暴露 GPT-5.5、DeepSeek、MiniMax 的风险分层、并发上限和外部 provider 429 退避处置。该建议只用于指挥官 preflight,不会改写 payload,不改变 runtime admission,也不假设生产 MiniMax 或 DeepSeek 可用。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 submit 把低内存主机或 `code-queue-mgr` 控制面打抖;返回值会附带 `submitConcurrencyGuard` 说明本次提交的锁与等待信息。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQL;D601 scheduler 只轮询并执行已入库任务。 - `codex pr-preflight [--remote] [--push-dry-run --push-dry-run-ref refs/heads/probe/] [--pr-create-dry-run --pr-create-dry-run-head ] [--issue N] [--full]` 通过稳定 `code-queue` proxy 请求 D601 scheduler `/api/runtime-preflight`,用于 PR 型派单 admission。输出会压缩展示 scheduler/runner 的 token 覆盖、Auth Broker source/capability/nextAction、工具、agent port、Git worktree、GitHub egress、repo/issue/PR 只读探测、可选 push dry-run,以及可选 PR body/create dry-run guard;只报告 `GH_TOKEN`/`GITHUB_TOKEN` 是否存在和来源 key,不打印值。当 auth-broker 配置存在时,`tokenCoverage.source="auth-broker"`、`credentialSource="broker-issued-token"` 且 runner env token 不是成功前提;当仅 env token 存在时,`credentialSource="env-token"` 且 `preflight.authBroker.nextAction="use-env-token-until-auth-broker-live"`;两者都缺失时顶层 `ok=false`、`runnerDisposition=infra-blocked`、`degradedReason=auth-broker-needed`,`tokenCoverage.missing` 同时列出 `GH_TOKEN` 与 `GITHUB_TOKEN`,并输出 `preflight.authBroker.source="broker/auth-broker-needed"`、`capability.source="missing-token"`。该 `auth-missing` 的 scope 是 `scheduler-runner-env`,不能简化成“当前 active runner/dev container 不能创建 PR”;输出必须带 `scopeBoundary` 和 `activeRunnerDevContainer`,要求调用方另跑 `bun scripts/cli.ts gh auth status --repo pikasTech/unidesk` 与 PR dry-run 来确认当前 dev container 能力。`preflight.prCapabilityContract` 是 runner-facing 合同摘要,必须包含目标分支、token/auth 来源、`systemGhBinaryRequiredForWrites=false`、UniDesk REST `bun scripts/cli.ts gh` 可用性、push dry-run/PR create dry-run 的 `writesRemote=false`、expected PR handoff、真实 PR 创建需要 commander 授权和 `gh pr merge` 的 `unsupported-command` 边界;系统 `gh` binary 缺失只进入 `tools.systemGhBinary`,不得误判为 UniDesk REST `gh` CLI 不可用。`--remote` 在 runner-like 环境里不再依赖本地 `unidesk-backend-core`、`unidesk-database`、`baidu-netdisk-backend` 容器存在;这些缺失只作为本地观测证据。若远程控制面可达,则继续走远程控制面结果;若远程控制面不可达,则结构化返回 `failureKind=control-plane-missing` / `degradedReason=remote-control-plane-unreachable`,而不是把本地 `backend-core-container-missing` 当作最终阻塞。`--pr-create-dry-run` 不 POST GitHub,只证明 runner 内 PR body 生成、`scripts/cli.ts gh pr create --dry-run` 和 branch 参数形态可用;服务端创建权限仍以 token/auth broker、repo/issue/PR read、push dry-run 和最终授权后的真实 PR 创建结果为准。 - `codex task ` 通过 Code Queue 私有代理按任务 ID 查询结构化审阅摘要;默认只返回任务身份、执行 Provider、工作目录、attempt 计数、原始 prompt、最终 response、最后错误和渐进披露命令,适合指挥官审阅完成未读任务且避免上下文爆炸。需要旧式详细摘要时显式加 `--detail`;需要完整 prompt/response 文本时加 `--full`;需要工具调用、judge、attempt 全量摘要时使用 `--detail --full --tool-limit N`。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。 -- `codex tasks [--view supervisor|full] [--queue id] [--status succeeded|running|queued|failed|canceled|judging|retry_wait[,..]] [--unread|--unread-only] [--limit N] [--before-id id]` 通过同一私有代理输出渐进式披露视图。默认 `supervisor` 是低噪声指挥官视图,只返回 `running`、`completedUnread`、`recentCompleted`、`queued` 和 `executionDiagnostics` 的紧凑行;prompt/body 只给短预览和原始字符数,`recentCompleted` 默认最多返回 5 条且不重复 `completedUnread` 未读终态,不嵌入完整 Trace、final response 或全量 overview。每个条目只保留 `commands.show/detail/trace/output/full/read` 作为精确展开入口,并带 `classification` 标记直接推进、部署修复、验证/报告噪声等类别,帮助指挥官按 #131 聚焦真实推进而不是被 Gate/报告/审查任务牵引。`--unread` 是 `--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败,不能静默忽略。需要更详细当前页任务行时显式使用 `--view full` 或 `--full`,仍受 `--limit` 和 `--before-id` 分页约束。 +- `codex tasks [--view supervisor|full] [--queue id] [--status succeeded|running|queued|failed|canceled|judging|retry_wait[,..]] [--unread|--unread-only] [--limit N] [--before-id id]` 通过同一私有代理输出渐进式披露视图。默认 `supervisor` 是低噪声指挥官视图,只返回 `running`、`completedUnread`、`recentCompleted`、`queued` 和 `executionDiagnostics` 的紧凑行;prompt/body 只给短预览和原始字符数,`running`/`completedUnread`/`queued` 默认只返回一个有界小页并通过 section `commands.next` 继续分页,`recentCompleted` 默认最多返回 5 条且不重复 `completedUnread` 未读终态,不嵌入完整 Trace、final response 或全量 overview。每个条目只保留 task id、队列、状态、issue、分类和短摘要,`show/detail/trace/output/full/read` 放在 section template 中避免重复噪声,并带 `kind` 标记直接推进、部署修复、验证/报告噪声等类别,帮助指挥官按 #131 聚焦真实推进而不是被 Gate/报告/审查任务牵引。`--unread` 是 `--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败,不能静默忽略。需要更详细当前页任务行时显式使用 `--view full` 或 `--full`,仍受 `--limit` 和 `--before-id` 分页约束。 - `codex task --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq`、`previousBeforeSeq`、`hasMore`、`hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,且仍以分页 trace 为主;需要完整 prompt/最终 response 时加 `--full`,需要详细 task 摘要时加 `--detail`。 - `codex output --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines`、`bodyOmittedLines` 或 `rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。 - `codex read ` 在人工审阅后标记单个终态任务已读;列表、overview 和 supervisor 视图只返回这个命令字段,不得自动执行,也不得批量清空未读状态。 diff --git a/docs/reference/code-queue-supervision.md b/docs/reference/code-queue-supervision.md index 4a5770d0..968924ea 100644 --- a/docs/reference/code-queue-supervision.md +++ b/docs/reference/code-queue-supervision.md @@ -201,7 +201,7 @@ bun scripts/cli.ts codex pr-preflight --remote --issue 常用入口: -- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认低噪声监督视图,包括 running、完成未读、最多 5 条最近完成、queued/runnable、execution diagnostics、任务分类和下一步 drill-down 命令。默认行只保留短 prompt/body 预览、原始字符数和 `commands.show/detail/trace/output/full/read`,需要更多内容再按 taskId 展开。 +- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认低噪声监督视图,包括 running、完成未读、最多 5 条最近完成、queued/runnable、execution diagnostics、任务分类和下一步 drill-down 命令。默认行只保留 task id、队列、短 prompt/body 预览和原始字符数;`show/detail/trace/output/full/read` 放在 section template 中,避免每条任务重复刷屏,需要更多内容再按 taskId 展开。 - `bun scripts/cli.ts codex queues`:查看低噪声队列计数、active task id、完成未读队列、runnable 队列和控制面诊断;只有需要完整队列行时才加 `--full`。summary 和 full 都使用稳定 JSON path `.data.queues.items[]` 读取队列行,并从 `.data.queues.counts` 与 `.data.queues.executionDiagnostics` 读取全局计数和执行诊断。 - `bun scripts/cli.ts codex tasks --unread --limit N`:查看完成未读审阅积压;`--unread` 与 `--unread-only` 等价,不能被静默忽略。 - `bun scripts/cli.ts codex tasks --status succeeded --unread --limit N`:按具体终态过滤监督结果;不支持的 status filter 必须显式失败,不能扩大为未过滤结果。 @@ -209,7 +209,7 @@ bun scripts/cli.ts codex pr-preflight --remote --issue - 当默认审阅摘要不足时,再逐级使用 `bun scripts/cli.ts codex task --detail`、`bun scripts/cli.ts codex task --trace --limit N` 或 `codex output`。 - 当 master 控制面状态和 D601 scheduler 状态看起来分裂时,使用 `docs/reference/observability.md` 中的活性规则判断。 -默认 supervisor 视图必须保持低噪声。每个任务应带 `commands.show`、`commands.detail`、`commands.trace`、`commands.output`、`commands.full` 和 `commands.read`,让下一步渐进披露动作明确;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`recentCompleted` 必须默认限量,且不得重复 `completedUnread` 里的未读终态,避免完成历史把当前 running、阻塞和未读审阅挤出视野;需要完整当前页时显式使用 `--view full`。`executionDiagnostics` 只能展示有界 task-id/reason 预览、总数、截断标记和 omitted counts;需要全量诊断时使用输出中的 raw command。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。 +默认 supervisor 视图必须保持低噪声。`running`、`completedUnread` 和 `queued` 即使传入较大的 `--limit`,默认也只返回一个有界小页,并通过 section `commands.next` 继续分页;`--limit` 保留给 full view 和后续分页请求,不得让一次 supervisor 调用输出几十条肥行。每个任务行只应带 task id 和必要摘要,`show`、`detail`、`trace`、`output`、`full`、`read` 使用 section template 表达,让下一步渐进披露动作明确且不重复;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`recentCompleted` 必须默认限量,且不得重复 `completedUnread` 里的未读终态,避免完成历史把当前 running、阻塞和未读审阅挤出视野;需要完整当前页时显式使用 `--view full`。`executionDiagnostics` 只能展示有界 task-id/reason 预览、总数、截断标记和 omitted counts;需要全量诊断时使用输出中的 raw command。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。 这条规则直接服务 HWLAB #132:指挥官要优先看到真实业务推进、部署修复、阻塞和需要人工审阅的未读结果,Gate/报告/审查/诊断类任务只能作为折叠的分类信号存在,不能在默认输出中用长 prompt/body 抢占上下文。 diff --git a/scripts/code-queue-supervisor-disclosure-contract-test.ts b/scripts/code-queue-supervisor-disclosure-contract-test.ts index eaec8f32..40fe870a 100644 --- a/scripts/code-queue-supervisor-disclosure-contract-test.ts +++ b/scripts/code-queue-supervisor-disclosure-contract-test.ts @@ -138,59 +138,106 @@ function fixtureResponse(path: string): JsonRecord { }; } +function manyRunningFixtureResponse(path: string): JsonRecord { + if (path.includes("/summary")) return fixtureResponse(path); + assertCondition(path.startsWith("/api/microservices/code-queue/proxy/api/tasks/overview"), "unexpected path", { path }); + const tasks = Array.from({ length: 40 }, (_, index) => task( + `task-running-${String(index + 1).padStart(2, "0")}`, + "running", + `2026-05-22T00:${String(59 - index).padStart(2, "0")}:00.000Z`, + )); + return { + ok: true, + status: 200, + body: { + ok: true, + queue: { + executionDiagnostics: { + state: "healthy", + databaseActiveTaskCount: 40, + databaseActiveTaskIds: manyIds("running-active", 40), + activeHeartbeatCount: 40, + activeHeartbeatTaskIds: manyIds("running-heartbeat", 40), + heartbeatFreshTaskIds: manyIds("running-fresh", 40), + }, + }, + pagination: { + limit: 200, + returned: 40, + total: 40, + hasMore: false, + nextBeforeId: null, + includeActive: true, + }, + tasks, + }, + }; +} + export function runCodeQueueSupervisorDisclosureContract(): JsonRecord { const supervisor = codexTasksQueryForTest(["--view", "supervisor", "--limit", "20"], fixtureResponse); const full = codexTasksQueryForTest(["--view", "full", "--limit", "20"], fixtureResponse); + const runningFiltered = codexTasksQueryForTest(["--status", "running", "--limit", "40"], manyRunningFixtureResponse); + const unreadFiltered = codexTasksQueryForTest(["--unread", "--limit", "20"], fixtureResponse); const supervisorBody = JSON.stringify(supervisor); const fullBody = JSON.stringify(full); + const runningFilteredBody = JSON.stringify(runningFiltered); + const unreadFilteredBody = JSON.stringify(unreadFiltered); const supervisorData = asRecord(supervisor); const supervisorView = asRecord(supervisorData.supervisor); + const runningFilteredView = asRecord(asRecord(runningFiltered).supervisor); + const runningFilteredSection = asRecord(runningFilteredView.running); + const unreadFilteredView = asRecord(asRecord(unreadFiltered).supervisor); + const unreadFilteredSection = asRecord(unreadFilteredView.completedUnread); const disclosure = asRecord(supervisorView.disclosure); const runningItem = asRecord(asArray(asRecord(supervisorView.running).items)[0]); const recentCompleted = asRecord(supervisorView.recentCompleted); const recentItems = asArray(recentCompleted.items); - const prompt = asRecord(runningItem.prompt); - const lastMessage = asRecord(runningItem.lastMessage); - const commands = asRecord(runningItem.commands); const fullItem = asRecord(asArray(asRecord(asRecord(full).tasks).items)[0]); const completedUnread = asRecord(supervisorView.completedUnread); const fullTasks = asRecord(asRecord(full).tasks); const diagnostics = asRecord(supervisorView.executionDiagnostics); - const liveness = asRecord(diagnostics.liveness); const listBudget = asRecord(diagnostics.listBudget); const omittedCounts = asRecord(listBudget.omittedCounts); assertCondition(supervisorBody.length < fullBody.length * 0.55, "supervisor output should be materially smaller than full output", { supervisorChars: supervisorBody.length, fullChars: fullBody.length }); - assertCondition(supervisorBody.length < 70_000, "supervisor output should remain bounded even with large diagnostics", { supervisorChars: supervisorBody.length }); + assertCondition(supervisorBody.length < 45_000, "supervisor output should remain bounded even with large diagnostics", { supervisorChars: supervisorBody.length }); assertCondition(recentItems.length === 5, "recentCompleted should be capped below --limit by default", { returned: recentItems.length }); - assertCondition(asArray(completedUnread.items).length === 7, "completedUnread should keep unread terminal tasks separate from recentCompleted", completedUnread); + assertCondition(asArray(completedUnread.items).length === 5, "completedUnread should be locally paged and kept separate from recentCompleted", completedUnread); assertCondition(recentItems.every((item) => asRecord(item).unreadTerminal === false), "recentCompleted should not duplicate unread terminal tasks", { recentItems }); - assertCondition(asArray(diagnostics.databaseActiveTaskIds).length === 12, "diagnostic task id lists should be capped", diagnostics); - assertCondition(omittedCounts.databaseActiveTaskIds === 68, "diagnostic omitted counts should preserve full visibility metadata", omittedCounts); - assertCondition(liveness.effectiveLiveness === "live", "supervisor liveness summary should keep split-brain live explicit", liveness); - assertCondition(liveness.recommendedAction === "continue-supervision", "supervisor liveness summary should recommend continued supervision", liveness); - assertCondition(liveness.splitBrainLive === true, "supervisor liveness summary should mark splitBrainLive", liveness); - assertCondition(liveness.activeHeartbeatCount === 80, "supervisor liveness summary should foreground active heartbeat count", liveness); - assertCondition(asArray(liveness.heartbeatFreshTaskIds).length === 12, "supervisor liveness summary should keep heartbeatFreshTaskIds bounded", liveness); - assertCondition(String(liveness.interpretation ?? "").includes("continue supervision"), "supervisor liveness interpretation should not imply scheduler stoppage", liveness); - assertCondition(asArray(diagnostics.reasons).length === 6, "diagnostic reasons should be capped", diagnostics); - assertCondition(diagnostics.livenessSummaryTruncated === true, "long diagnostic liveness summary should be previewed", diagnostics); + assertCondition(diagnostics.databaseActiveTaskIds === undefined, "supervisor diagnostics should not expose verbose databaseActiveTaskIds by default", diagnostics); + assertCondition(omittedCounts.databaseActiveTaskIds === 77, "diagnostic omitted counts should preserve full visibility metadata", omittedCounts); + assertCondition(diagnostics.effectiveLiveness === "live", "supervisor liveness summary should keep split-brain live explicit", diagnostics); + assertCondition(diagnostics.recommendedAction === "continue-supervision", "supervisor liveness summary should recommend continued supervision", diagnostics); + assertCondition(diagnostics.splitBrainLive === true, "supervisor liveness summary should mark splitBrainLive", diagnostics); + assertCondition(diagnostics.activeHeartbeatCount === 80, "supervisor liveness summary should foreground active heartbeat count", diagnostics); + assertCondition(asArray(diagnostics.heartbeatFreshTaskIds).length === 3, "supervisor diagnostics should keep heartbeatFreshTaskIds bounded", diagnostics); + assertCondition(String(diagnostics.interpretation ?? "").includes("continue supervision"), "supervisor liveness interpretation should not imply scheduler stoppage", diagnostics); + assertCondition(asArray(diagnostics.reasons).length === 2, "diagnostic reasons should be capped", diagnostics); + assertCondition(diagnostics.livenessSummary === undefined, "supervisor diagnostics should omit liveness summary preview by default", diagnostics); assertCondition(listBudget.truncated === true && typeof listBudget.rawCommand === "string", "diagnostic list budget should disclose raw command", listBudget); - assertCondition(asArray(runningItem.issueRefs).includes("#132"), "supervisor row should expose issue refs for triage", runningItem); - assertCondition(Number(prompt.chars) > String(prompt.text ?? "").length && prompt.truncated === true, "supervisor prompt must be a short preview with original char count", prompt); - assertCondition(Number(lastMessage.chars) > String(lastMessage.text ?? "").length && lastMessage.truncated === true, "supervisor body must be a short preview with original char count", lastMessage); - assertCondition(commands.show !== undefined && commands.trace !== undefined && commands.output !== undefined && commands.full !== undefined, "supervisor row must keep progressive drill-down commands", commands); - assertCondition(runningItem.promptPreview === undefined && runningItem.lastAssistantMessage === undefined, "supervisor rows must not expose legacy long list fields", runningItem); + assertCondition(asArray(runningItem.issues).includes("#132"), "supervisor row should expose issue refs for triage", runningItem); + assertCondition(Number(runningItem.promptChars) > String(runningItem.prompt ?? "").length && runningItem.promptTruncated === true, "supervisor prompt must be a short flat preview with original char count", runningItem); + assertCondition(Number(runningItem.lastChars) > String(runningItem.last ?? "").length && runningItem.lastTruncated === true, "supervisor body must be a short flat preview with original char count", runningItem); + assertCondition(runningItem.commands === undefined && runningItem.promptPreview === undefined && runningItem.lastAssistantMessage === undefined, "supervisor rows must not expose repeated commands or legacy long list fields", runningItem); assertCondition(asRecord(fullItem.promptPreview).chars !== undefined && fullItem.lastAssistantMessage !== undefined, "full view must retain detailed task row fields", fullItem); assertCondition(fullTasks.returned === 15, "full view must not inherit supervisor recentCompleted cap", fullTasks); - assertCondition(asRecord(disclosure.outputBudget).recentCompletedReturnedLimit === 5, "supervisor must expose output budget metadata", disclosure); + const budget = asRecord(disclosure.outputBudget); + assertCondition(budget.recentCompletedReturnedLimit === 5 && budget.sectionReturnedLimit === 5, "supervisor must expose output budget metadata", disclosure); + assertCondition(asArray(runningFilteredSection.items).length === 5, "running status filter should be locally paged below --limit", runningFilteredSection); + assertCondition(runningFilteredSection.count === 40 && runningFilteredSection.hasMore === true, "running status filter should preserve count and hasMore", runningFilteredSection); + assertCondition(String(asRecord(runningFilteredSection.commands).next ?? "").includes("--before-id task-running-05"), "running status filter should provide next page command", runningFilteredSection); + assertCondition(runningFilteredBody.length < 14_000, "running status filter output should remain bounded", { chars: runningFilteredBody.length }); + assertCondition(asArray(unreadFilteredSection.items).length <= 5, "unread list should be locally paged below --limit", unreadFilteredSection); + assertCondition(unreadFilteredBody.length < 14_000, "unread output should remain bounded", { chars: unreadFilteredBody.length }); return { ok: true, checks: [ "supervisor output materially smaller than full", "recentCompleted capped", + "running/unread locally paged", "split-brain diagnostics capped", "prompt/body previews bounded", "drill-down commands preserved", diff --git a/scripts/src/code-queue.ts b/scripts/src/code-queue.ts index 6e04e402..decca923 100644 --- a/scripts/src/code-queue.ts +++ b/scripts/src/code-queue.ts @@ -12,12 +12,13 @@ const defaultOutputLimit = 20; const defaultTextPreviewChars = 12_000; const defaultTasksLimit = 20; const maxTasksLimit = 100; +const supervisorSectionReturnedLimit = 5; const supervisorRecentCompletedLimit = 5; -const supervisorPromptPreviewChars = 160; -const supervisorBodyPreviewChars = 180; -const supervisorRecentBodyPreviewChars = 80; -const diagnosticsIdPreviewLimit = 12; -const diagnosticsReasonPreviewLimit = 6; +const supervisorPromptPreviewChars = 90; +const supervisorBodyPreviewChars = 90; +const supervisorRecentBodyPreviewChars = 60; +const diagnosticsIdPreviewLimit = 3; +const diagnosticsReasonPreviewLimit = 2; const steerPromptPreviewChars = 320; const minimaxSubmitModel = "minimax-m2.7"; const deepseekSubmitModel = "deepseek-chat"; @@ -193,32 +194,37 @@ interface CodexTasksEntry { }; } +interface SupervisorTextSummary { + text: string; + chars: number; + truncated: boolean; +} + +interface SupervisorMessageSummary extends SupervisorTextSummary { + at: unknown; + source: unknown; +} + interface CodexTasksSupervisorEntry { - taskId: string; - queueId: string | null; + id: string; + queue: string | null; status: string | null; - currentAttempt: number | null; + attempt: number | null; updatedAt: string | null; - finishedAt: string | null; - unreadTerminal: boolean; - issueRefs: string[]; - classification: { - kind: "direct-progress" | "deployment-fix" | "verification" | "management-noise" | "documentation" | "unknown"; - labels: string[]; - managementNoise: boolean; - reason: string; - }; - prompt: Record; - lastMessage: Record | null; - queuedReason: Record | null; - commands: { - show: string; - detail: string; - trace: string; - output: string; - full: string; - read: string; - }; + finishedAt?: string | null; + unreadTerminal?: boolean; + issues: string[]; + kind: "direct-progress" | "deployment-fix" | "verification" | "management-noise" | "documentation" | "unknown"; + noise?: boolean; + prompt: string; + promptChars: number; + promptTruncated?: boolean; + last?: string; + lastAt?: unknown; + lastChars?: number; + lastTruncated?: boolean; + queuedReason?: Record; + read?: string; } interface CodexTasksSection { @@ -229,9 +235,12 @@ interface CodexTasksSection { commands: { next: string | null; full: string; + showTemplate?: string; detailTemplate?: string; traceTemplate?: string; outputTemplate?: string; + taskFullTemplate?: string; + readTemplate?: string; }; items: T[]; } @@ -404,6 +413,16 @@ function compactInlinePreview(value: string, maxChars: number): Record maxChars; + return { + text: truncated ? compact.slice(0, maxChars) : compact, + chars: compact.length, + truncated, + }; +} + function fmtDuration(ms: unknown): string { const value = Number(ms); if (!Number.isFinite(value) || value < 0) return "--"; @@ -1157,6 +1176,35 @@ function compactExecutionDiagnostics(value: unknown): Record | }; } +function supervisorExecutionDiagnostics(value: unknown): Record | null { + const diagnostics = compactExecutionDiagnostics(value); + if (diagnostics === null) return null; + const liveness = asRecord(diagnostics.liveness) ?? {}; + const listBudget = asRecord(diagnostics.listBudget) ?? {}; + return { + state: diagnostics.state ?? null, + effectiveLiveness: diagnostics.effectiveLiveness ?? null, + recommendedAction: diagnostics.recommendedAction ?? null, + splitBrainLive: diagnostics.splitBrainLive ?? null, + activeHeartbeatCount: diagnostics.activeHeartbeatCount ?? null, + databaseActiveTaskCount: diagnostics.databaseActiveTaskCount ?? null, + schedulerActiveRunSlotCount: diagnostics.schedulerActiveRunSlotCount ?? null, + lastObservedAgentEventAt: diagnostics.lastObservedAgentEventAt ?? null, + heartbeatFreshTaskIds: diagnostics.heartbeatFreshTaskIds ?? [], + heartbeatRiskTaskIds: diagnostics.heartbeatRiskTaskIds ?? [], + traceGapTaskIds: diagnostics.traceGapTaskIds ?? [], + reasons: diagnostics.reasons ?? [], + interpretation: liveness.interpretation ?? null, + listBudget: { + idPreviewLimit: listBudget.idPreviewLimit ?? diagnosticsIdPreviewLimit, + reasonPreviewLimit: listBudget.reasonPreviewLimit ?? diagnosticsReasonPreviewLimit, + truncated: listBudget.truncated ?? false, + omittedCounts: listBudget.omittedCounts ?? {}, + rawCommand: listBudget.rawCommand ?? "bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview?limit=30 --raw --full", + }, + }; +} + function compactToolSummary(value: unknown, full: boolean): Record { const record = asRecord(value) ?? {}; const items = asArray(record.items).map((item) => { @@ -1730,7 +1778,12 @@ function taskIssueRefs(task: Record, summary: Record `#${match[1]}`))).slice(0, 8); } -function taskClassification(task: Record, summary: Record | null): CodexTasksSupervisorEntry["classification"] { +function taskClassification(task: Record, summary: Record | null): { + kind: CodexTasksSupervisorEntry["kind"]; + labels: string[]; + managementNoise: boolean; + reason: string; +} { const text = [ asString(task.displayPrompt), asString(task.basePrompt), @@ -1765,23 +1818,14 @@ function taskClassification(task: Record, summary: Record | null { +function supervisorLastMessage(summaryLastAssistant: unknown, maxChars: number): SupervisorMessageSummary | null { if (summaryLastAssistant === undefined || summaryLastAssistant === null) return null; const record = asRecord(summaryLastAssistant) ?? {}; const text = asString(record.text); - if (text.length === 0) { - return { - at: record.at ?? null, - seq: record.seq ?? null, - source: record.source ?? "none", - ...compactInlinePreview("", maxChars), - }; - } return { at: record.at ?? null, - seq: record.seq ?? null, source: record.source ?? "none", - ...compactInlinePreview(text, maxChars), + ...supervisorTextSummary(text, maxChars), }; } @@ -1825,35 +1869,34 @@ function taskWatchEntry(task: Record, summary: Record, summary: Record | null, bodyPreviewChars = supervisorBodyPreviewChars): CodexTasksSupervisorEntry { const taskId = asString(task.id); - const summaryCommands = summary === null ? null : asRecord(summary.commands); const summaryLastAssistant = summary?.lastAssistantMessage ?? task.lastAssistantMessage; - const showCommand = typeof summary?.cliHint === "string" && summary.cliHint.length > 0 - ? summary.cliHint - : `bun scripts/cli.ts codex task ${taskId}`; - const traceCommand = typeof summary?.traceHint === "string" && summary.traceHint.length > 0 - ? summary.traceHint - : `bun scripts/cli.ts codex task ${taskId} --trace --tail --limit ${defaultTraceLimit}`; + const status = asString(task.status) || null; + const unreadTerminal = taskUnreadTerminal(task); + const classification = taskClassification(task, summary); + const queuedReason = compactQueuedReason(task.queuedReason); + const prompt = supervisorTextSummary(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), supervisorPromptPreviewChars); + const lastMessage = supervisorLastMessage(summaryLastAssistant, bodyPreviewChars); return { - taskId, - queueId: asString(task.queueId) || null, - status: asString(task.status) || null, - currentAttempt: typeof task.currentAttempt === "number" && Number.isFinite(task.currentAttempt) ? task.currentAttempt : null, + id: taskId, + queue: asString(task.queueId) || null, + status, + attempt: typeof task.currentAttempt === "number" && Number.isFinite(task.currentAttempt) ? task.currentAttempt : null, updatedAt: asString(task.updatedAt) || null, - finishedAt: asString(task.finishedAt) || null, - unreadTerminal: taskUnreadTerminal(task), - issueRefs: taskIssueRefs(task, summary), - classification: taskClassification(task, summary), - prompt: compactInlinePreview(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), supervisorPromptPreviewChars), - lastMessage: supervisorLastMessage(summaryLastAssistant, bodyPreviewChars), - queuedReason: compactQueuedReason(task.queuedReason), - commands: { - show: typeof summaryCommands?.show === "string" && summaryCommands.show.length > 0 ? summaryCommands.show : showCommand, - detail: `bun scripts/cli.ts codex task ${taskId} --detail`, - trace: typeof summaryCommands?.trace === "string" && summaryCommands.trace.length > 0 ? summaryCommands.trace : traceCommand, - output: `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`, - full: `bun scripts/cli.ts codex task ${taskId} --full`, - read: `bun scripts/cli.ts codex read ${taskId}`, - }, + ...(isTerminalTaskStatus(status) ? { finishedAt: asString(task.finishedAt) || null, unreadTerminal } : {}), + issues: taskIssueRefs(task, summary), + kind: classification.kind, + ...(classification.managementNoise ? { noise: true } : {}), + prompt: prompt.text, + promptChars: prompt.chars, + ...(prompt.truncated ? { promptTruncated: true } : {}), + ...(lastMessage === null ? {} : { + last: lastMessage.text, + lastAt: lastMessage.at, + lastChars: lastMessage.chars, + ...(lastMessage.truncated ? { lastTruncated: true } : {}), + }), + ...(queuedReason === null ? {} : { queuedReason }), + ...(unreadTerminal ? { read: `bun scripts/cli.ts codex read ${taskId}` } : {}), }; } @@ -1891,18 +1934,24 @@ function buildSupervisorTaskSection( const visibleTasks = tasks.slice(0, limit); const items = visibleTasks.map((task) => taskSupervisorEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null, bodyPreviewChars)); const truncated = tasks.length > limit; + const commands = { + next: truncated || nextCommand !== null ? nextCommand : null, + full: fullCommand, + ...(items.length === 0 ? {} : { + showTemplate: "bun scripts/cli.ts codex task ", + detailTemplate: "bun scripts/cli.ts codex task --detail", + traceTemplate: `bun scripts/cli.ts codex task --trace --tail --limit ${defaultTraceLimit}`, + outputTemplate: `bun scripts/cli.ts codex output --tail --limit ${defaultOutputLimit}`, + taskFullTemplate: "bun scripts/cli.ts codex task --full", + readTemplate: "bun scripts/cli.ts codex read ", + }), + }; return { count: tasks.length, returned: items.length, truncated, - hasMore: truncated, - commands: { - next: truncated ? nextCommand : null, - full: fullCommand, - detailTemplate: "bun scripts/cli.ts codex task --detail", - traceTemplate: `bun scripts/cli.ts codex task --trace --tail --limit ${defaultTraceLimit}`, - outputTemplate: `bun scripts/cli.ts codex output --tail --limit ${defaultOutputLimit}`, - }, + hasMore: truncated || nextCommand !== null, + commands, items, }; } @@ -1976,6 +2025,24 @@ function taskListCommand(options: CodexTasksOptions, extra: string[] = []): stri return `bun scripts/cli.ts ${args.join(" ")}`; } +function taskSectionLimit(options: CodexTasksOptions, maxReturned = supervisorSectionReturnedLimit): number { + return Math.min(options.limit, maxReturned); +} + +function sectionNextCommand( + tasks: Record[], + limit: number, + options: CodexTasksOptions, + sourceNextCommand: string | null, +): string | null { + if (tasks.length > limit) { + const lastVisibleTask = tasks[limit - 1]; + const beforeId = lastVisibleTask === undefined ? "" : taskOverviewCandidateKey(lastVisibleTask); + if (beforeId.length > 0) return taskListCommand({ ...options, beforeId }); + } + return sourceNextCommand; +} + function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { summaries: Map>; degraded: CodexTasksDegraded | null } { const boundedIds = taskIds.slice(0, maxTasksLimit); const summaries = new Map>(); @@ -2064,18 +2131,19 @@ function codexTasksOverviewResult( const sourceHasMore = asBoolean(taskPage.pagination.hasMore); const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null; const fullCommand = taskListCommand({ ...options, view: "full" }); + const sectionLimit = taskSectionLimit(options); const recentLimit = Math.min(options.limit, supervisorRecentCompletedLimit); - const runningSection = buildSupervisorTaskSection(runningTasks, summaries, options.limit, nextCommand, fullCommand); - const unreadSection = buildSupervisorTaskSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand); - const recentSection = buildSupervisorTaskSection(recentCompletedTasks, summaries, recentLimit, nextCommand, fullCommand, supervisorRecentBodyPreviewChars); - const queuedSection = buildSupervisorTaskSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand); + const runningSection = buildSupervisorTaskSection(runningTasks, summaries, sectionLimit, sectionNextCommand(runningTasks, sectionLimit, options, nextCommand), fullCommand); + const unreadSection = buildSupervisorTaskSection(unreadCompletedTasks, summaries, sectionLimit, sectionNextCommand(unreadCompletedTasks, sectionLimit, options, nextCommand), fullCommand); + const recentSection = buildSupervisorTaskSection(recentCompletedTasks, summaries, recentLimit, sectionNextCommand(recentCompletedTasks, recentLimit, options, nextCommand), fullCommand, supervisorRecentBodyPreviewChars); + const queuedSection = buildSupervisorTaskSection(queuedTasks, summaries, sectionLimit, sectionNextCommand(queuedTasks, sectionLimit, options, nextCommand), fullCommand); const pagination = taskPage.pagination; - const diagnostics = compactExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics); + const diagnostics = supervisorExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics); const visibleSupervisorItems = [...runningSection.items, ...unreadSection.items, ...recentSection.items, ...queuedSection.items]; const classifierCounts = visibleSupervisorItems.reduce((counts, item) => { - const key = item.classification.kind; + const key = item.kind; counts[key] = (counts[key] ?? 0) + 1; - if (item.classification.managementNoise) counts.managementNoise = (counts.managementNoise ?? 0) + 1; + if (item.noise) counts.managementNoise = (counts.managementNoise ?? 0) + 1; return counts; }, {} as Record); return { @@ -2106,8 +2174,10 @@ function codexTasksOverviewResult( outputBudget: { promptPreviewChars: supervisorPromptPreviewChars, bodyPreviewChars: supervisorBodyPreviewChars, + sectionReturnedLimit: sectionLimit, recentCompletedReturnedLimit: recentLimit, recentCompletedBodyPreviewChars: supervisorRecentBodyPreviewChars, + rowCommands: "task id is the row drill-down key; show/detail/trace/output/full/read are section templates to avoid repeated noise", }, fullCommand, next: nextCommand, @@ -2188,11 +2258,12 @@ function visibleTaskIdsForOverview(tasks: Record[], options: Co if (options.view === "full") { return filtered.slice(0, options.limit).map((task) => taskOverviewCandidateKey(task)).filter((taskId) => taskId.length > 0); } + const sectionLimit = taskSectionLimit(options); return Array.from(new Set([ - ...sortRunningWatchTasks(filtered).slice(0, options.limit), - ...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit), + ...sortRunningWatchTasks(filtered).slice(0, sectionLimit), + ...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, sectionLimit), ...sortCompletedWatchTasks(filtered).filter((task) => !taskUnreadTerminal(task)).slice(0, Math.min(options.limit, supervisorRecentCompletedLimit)), - ...sortQueuedWatchTasks(filtered).slice(0, options.limit), + ...sortQueuedWatchTasks(filtered).slice(0, sectionLimit), ].map((task) => taskOverviewCandidateKey(task)))) .filter((taskId) => taskId.length > 0); }