feat: tighten code queue supervision cli

This commit is contained in:
Codex
2026-05-20 04:15:18 +00:00
parent 6e86d3600c
commit f9b190945d
11 changed files with 531 additions and 40 deletions
+5 -3
View File
@@ -26,19 +26,21 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- `decision upload/list/show/health` 通过 backend-core 用户服务代理访问 D601 k3s Decision Center,用于上传会议记录/决议 Markdown、列出权威记录、查看详情和健康检查;`decision requirement list/upsert` 在同一 records 模型上管理 `goal|decision|blocker|debt|experiment` 需求记录。它们不得直连 D601 Service、NodePort 或 provider-gateway 业务 HTTP。
- `decision diary import <markdown-file>` 将带 `# YYYY年M月D日``# YYYY-MM-DD``# YYYY/M/D` 标题的工作日志拆成每天一篇 Markdown 日记,按 `YYYY-MM/YYYY-MM-DD.md` 虚拟路径写入 Decision Center PostgreSQL`decision diary list/months/show` 分别用于按月/日期查询、列出月份和查看单日正文;`decision diary edit|upsert <YYYY-MM-DD|id> --body-file <path> [--title text] [--source-file path] [--tag tag]` 通过 `PUT /api/diary/entries/:idOrDate` 创建当天或历史条目并编辑既有条目。
- `deploy check/plan/apply` 默认从根目录 `deploy.json` 读取服务 repo 与 commit 期望状态,join `config.json` 和现有 manifest 后使用 target-side build 单一路径校验或更新已支持目标;`deploy plan --env dev|prod` 只从 `origin/master:deploy.json#environments.<env>` 读取 manifest 并输出 dry-run 环境计划,不使用本地 dirty worktree;当前 `deploy apply --env dev` 支持 D601 `backend-core` target-side rollout,以及 `frontend`/`baidu-netdisk`/`decision-center` artifact consumers`findjob`/`pipeline`/`met-nonlinear` 为 D601 direct Compose artifact consumers`k3sctl-adapter` 只提供 plan/dry-rundev desired-state smoke 使用 `ci run-dev-e2e`;规则见 `docs/reference/deploy.md``docs/reference/dev-environment.md``docs/reference/dev-ci-runner.md``deploy apply --env prod` 同时覆盖 `findjob``pipeline` 的 pull-only Compose CD,但 `met-nonlinear` 仍然只允许 dry-run/plan`k3sctl-adapter` 只允许 plan/dry-run。
- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev workload manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml``src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guardcore manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/ServiceCode Queue dev manifest 必须包含 `code-queue-scheduler-dev``code-queue-read-dev``code-queue-write-dev`dev provider egress proxy。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f <manifest>`,仍不 apply 资源。
- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev workload manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml``src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guardcore manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/ServiceCode Queue dev manifest 必须包含 `code-queue-scheduler-dev``code-queue-read-dev``code-queue-write-dev`dev provider egress proxy,以及只读挂载宿主 `/home/ubuntu/.agents/skills` 到容器 `/root/.agents/skills``skills-dir` volume。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f <manifest>`,仍不 apply 资源。
- `dev-env prewarm-images [--image image] [--provider-id D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]` 创建异步 job,通过 UniDesk SSH 维护桥在 D601 上把开发底座依赖镜像从 Docker 缓存导入原生 k3s containerd。默认镜像是 `postgres:16-alpine``rancher/mirrored-library-busybox:1.36.1`,用于避免 `postgres-dev` 与 local-path helper pod 卡在外部 registry 拉取。该命令固定验证 `/etc/rancher/k3s/k3s.yaml` 指向的 native k3s 上下文,并输出 `dev_env_containerd_image_ready=...` 作为成功判据;它不 apply manifest、不修改生产 `unidesk` namespace。
- `artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service` 管理 D601 host-managed CNCF Distribution registry 的声明、安装、只读检查和 pull-only artifact CD。该 registry 固定为 D601 loopback `127.0.0.1:5000`,由 systemd + Docker Compose 管理,位于 native k3s 故障域外;`deploy-service` 只拉取 CI 已发布的 commit-pinned 镜像、retag/recreate 或导入 native k3s,并做 live commit 验证,不构建 runtime source。`deploy-backend-core` 是 deprecated 兼容名,标准 backend-core prod CD 入口是 `deploy apply --env prod --service backend-core`。长期规则见 `docs/reference/artifact-registry.md`
- `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core``publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/<service>:<commit>` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId``sourceCommit``sourceRepo``dockerfile``imageRef``tag``digest``digestRef`),但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`
- `codex deploy <commitId>` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`
- `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` 只返回结构化请求且不实际入队。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。真实提交会经过本机本地串行化保护和短节流,避免同一指挥端并发 submit 把低内存主机或 `code-queue-mgr` 控制面打抖;返回值会附带 `submitConcurrencyGuard` 说明本次提交的锁与等待信息。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQLD601 scheduler 只轮询并执行已入库任务。
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。
- `codex tasks [--queue id] [--limit N] [--unread-only]` 通过同一私有代理输出一个只读聚合视图,按 `running``completedUnread``recentCompleted` 三个 section 汇总当前需要盯的任务;每个条目都带 `taskId``queueId``status``currentAttempt``updatedAt``finishedAt``unread`/`unreadTerminal``lastAssistantMessage` 摘要和可直接复制的 `commands.show` / `commands.trace``--queue` 限定单个队列,`--limit` 控制各 section 的最大条数,`--unread-only` 只保留未读终态和正在运行的任务
- `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` 摘要,不嵌入完整 Trace、final response 或全量 overview;每个条目都带 `commands.show``commands.trace``commands.output``commands.read``commands.full``--unread``--unread-only` 的别名,必须只保留未读终态;`--status` 必须真实过滤支持的状态,未知参数或未知状态必须结构化失败,不能静默忽略。需要完整当前页任务简表时显式使用 `--view full` `--full`,仍受 `--limit``--before-id` 分页约束
- `codex task <taskId> --trace --tail|--from-start|--after-seq N|--before-seq N --limit N` 按页拉取 Code Queue 的逻辑 trace;响应会返回 `nextAfterSeq``previousBeforeSeq``hasMore``hasBefore` 和下一页/上一页命令,默认 `--trace` 取最新一页,需要完整 prompt/最后 response 时加 `--full`
- `codex output <taskId> --tail|--from-start|--after-seq N|--before-seq N --limit N [--full-text]` 按原始 output seq 分页读取底层记录;当 trace 行提示 `commandOmittedLines``bodyOmittedLines``rawSeqs` 时,用该命令按 seq 补取完整信息,默认仍有单条文本预览上限,显式 `--full-text` 才返回该页全文。
- `codex read <taskId>` 在人工审阅后标记单个终态任务已读;列表、overview 和 supervisor 视图只返回这个命令字段,不得自动执行,也不得批量清空未读状态。
- `codex dev-ready` 查询 Code Queue `/api/dev-ready` 并返回有界 readiness 摘要,包括工具、Docker、Codex config、SSH 和 `devReady.skills``devReady.skills` 只暴露 `UNIDESK_SKILLS_PATH`、是否存在、是否只读、skillCount、`cli-spec` 是否可见和修复建议,不输出宿主 auth/token 文件内容。
- `codex judge <taskId> --attempt N [--dry-run] [--include-prompt]` 通过 Code Queue 私有代理按指定 attempt 单步复现 judge;这是执行面诊断入口,仍依赖 D601 scheduler/runner 侧的真实 judge builder、MiniMax 调用路径和执行环境。默认会真实调用 MiniMax,`--dry-run` 只返回 prompt/payload 大小、attempt 窗口和重建来源诊断,`--include-prompt` 仅用于本地深度排查。
- `codex interrupt|cancel <taskId>` 通过 Code Queue 私有代理请求中断;running/judging 任务会请求 D601 当前 agent run 停止,queued/retry_wait 任务的取消也必须保持与 WebUI 相同代理路径,返回有界 task 摘要和后续查询命令。任何需要接触 active run 的动作仍属于 D601 执行面。
- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues` 列表、`queue create <queueId>` 创建、`queue merge <sourceQueueId> --into <targetQueueId>` 合并、`move <taskId> --queue <queueId>` 迁移;这些队列管理入口默认由主 server `code-queue-mgr` 直管 PostgreSQL,仍通过稳定 `code-queue` 用户服务代理路径访问。同一个 queue 内部串行执行,不同 queue 之间并行执行。迁移只允许尚未被 scheduler claim 的 `queued`/`retry_wait` 任务,必须满足 `startedAt=null``currentAttempt=0` 且没有 active thread/turn;已进入 `running`/`judging` 或已有 claim 标记的任务返回 409,不得被 move/merge 回写成 queued。合并会移动可迁移任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;若 source 或 target queue 存在 active/claimed 任务,合并整体返回 409。合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行,成功迁移 queued/retry_wait 任务后由 D601 scheduler 轮询推进。
- Code Queue 多队列 lane 由 `codex` 命令命名空间管理:`queues [--full|--all]` 列表、`queue create <queueId>` 创建、`queue merge <sourceQueueId> --into <targetQueueId>` 合并、`move <taskId> --queue <queueId>` 迁移;这些队列管理入口默认由主 server `code-queue-mgr` 直管 PostgreSQL,仍通过稳定 `code-queue` 用户服务代理路径访问。`codex queues` 默认只返回 active/nonempty/unread/runnable queue 摘要、全局 counts 和 execution diagnostics;完整队列数组必须显式 `--full``--all`同一个 queue 内部串行执行,不同 queue 之间并行执行。迁移只允许尚未被 scheduler claim 的 `queued`/`retry_wait` 任务,必须满足 `startedAt=null``currentAttempt=0` 且没有 active thread/turn;已进入 `running`/`judging` 或已有 claim 标记的任务返回 409,不得被 move/merge 回写成 queued。合并会移动可迁移任务归属并自动删除源 queue 记录,只保留合并后的目标 queue;若 source 或 target queue 存在 active/claimed 任务,合并整体返回 409。合并后的目标 queue 按任务原 `queueEnteredAt`/`createdAt` 时间顺序串行,成功迁移 queued/retry_wait 任务后由 D601 scheduler 轮询推进。
- 所有 `codex` 查询和管理命令必须走与 WebUI 相同的 backend-core 私有代理路径 `/api/microservices/code-queue/proxy/...`;CLI 不得为了提交、移动、中断、取消或队列管理直接调用 D601 内部 Service、数据库、pod curl 或 k3sctl scheduler 子服务。若该路径失败,应先修复 CLI/backend/provider tunnel 链路,而不是绕过控制面。
- `job list [--limit N] [--include-command]``job status <jobId|latest> [--tail-bytes N]` 查询 `.state/jobs/` 文件系统状态,是异步命令的可观测入口。`job list` 默认只返回最新 50 条摘要;`job status` 默认只返回 stdout/stderr 末尾 12000 字节,并带 `tailPolicy` 与完整日志路径。
- `debug health``debug dispatch``debug task` 走真实内部 core、WebSocket、数据库、provider、系统指标、Docker 状态和 Host SSH 维护桥流程,只用于开发调试,不写入 `TEST.md` 的正式验收步骤。
+7 -2
View File
@@ -47,11 +47,16 @@
常用入口:
- `bun scripts/cli.ts codex queues`:查看队列计数、active task id、完成未读任务和控制面诊断
- `bun scripts/cli.ts codex tasks --view supervisor --limit N`:查看默认有界监督视图,包括 running、完成未读、最近完成、queued/runnable、execution diagnostics 和下一步 drill-down 命令
- `bun scripts/cli.ts codex queues`:查看低噪声队列计数、active task id、完成未读队列、runnable 队列和控制面诊断;只有需要完整队列行时才加 `--full`
- `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 必须显式失败,不能扩大为未过滤结果。
- `bun scripts/cli.ts codex task <taskId>`:查看 attempt、最后 assistant message、最后错误、cancel flag 和当前状态。
- 当摘要不足时,再使用 `bun scripts/cli.ts codex task <taskId> --trace --limit N``codex output`
- 当 master 控制面状态和 D601 scheduler 状态看起来分裂时,使用 `docs/reference/observability.md` 中的活性规则判断。
默认 supervisor 视图必须保持低噪声。每个任务应带 `commands.show``commands.trace``commands.output``commands.full``commands.read`,让下一步渐进披露动作明确;默认不得嵌入完整 queue 列表、完整 final response、raw output 页或完整 trace 行。`commands.read` 只是在人工审阅后的建议命令,listing 命令绝不能自动执行。
队列诊断中的 `split-brain` 表示控制面/执行面观测分裂,不自动证明任务已经死亡。如果任务 heartbeat 新鲜且 trace 仍在推进,应把任务视为 live,继续监督,而不是 interrupt 或替换。队列摘要应显示 `effectiveLiveness=live``splitBrainLive=true``recommendedAction=continue-supervision`;如果 heartbeat expired/missing 或满足 stale-recovery 条件,应显示 `effectiveLiveness=at-risk`
对于 trace 或 heartbeat 新鲜的长任务,通常应保持运行。每几分钟轮询一次优于反复 interrupt/retry。
@@ -60,7 +65,7 @@
当任务离开 `running``judging` 后,其结果仍然是未读工作。指挥官必须检查 final response 和 judge 记录,然后再决定是否补充并发窗口。
禁止在检查前用批量 read 动作清空完成未读任务。每个完成任务必须先单独审阅,再单独标记已读,使未读状态继续代表“仍需审阅”。
禁止在检查前用批量 read 动作清空完成未读任务。每个完成任务必须先单独审阅,再`bun scripts/cli.ts codex read <taskId>` 单独标记已读,使未读状态继续代表“仍需审阅”。
## 指挥工作流
+1
View File
@@ -200,6 +200,7 @@ D601 上必须显式使用原生 k3s kubeconfig`KUBECONFIG=/etc/rancher/k3s/k
- 期望状态部署:Code Queue 仍由 `deploy.json` 的 repo 与 commit 声明版本,但维护通道直连 D601 不能再用 `deploy apply --service code-queue``codex deploy <commitId>` 部署 Code Queue。未来受控 Code Queue CD 应复用 `docs/reference/deploy.md` 的 target-side build 规范在 D601 构建、导入 k3s、rollout 并验证 live commit,不得维护第二套部署语义。
- 更名与灾备恢复:旧版 Codex 队列服务名只允许作为兼容诊断和一次性迁移来源;`code-queue-backend` 容器自身 `/health` 正常但 `microservice health code-queue` 返回 provider 直连错误时,优先判定为 backend-core 仍加载旧 `MICROSERVICES_JSON` 或 adapter manifest 未刷新,必须刷新 `.state/docker-compose.env`、重建/替换 `backend-core``k3sctl-adapter`,随后用 `microservice list` 验证 `code-queue``runtime.orchestrator=k3sctl``backend.proxyMode=k3sctl-adapter-http` 和无业务容器直连摘要。正式 k3s 部署成功后,旧 direct Docker `code-queue-backend` 必须停止并移除,不能与 `code-queue-scheduler` 同时运行;否则会形成双 scheduler、双健康来源和错误的恢复判断。
- Codex 认证:容器必须从 D601 的 `/home/ubuntu/.codex/config.toml` 同步 Codex provider 配置到 D601 `.state/code-queue/codex-home/config.toml`,并只读挂载 `/home/ubuntu/.codex/auth.json` 到容器 `/root/.codex/auth.json` 后同步到 `.state/code-queue/codex-home/auth.json`,让 `codex app-server` 使用与 host 一致的 provider 登录态;同时通过 D601 `.state/code-queue-d601.env` 或 k8s `code-queue-env` secret 透传 `OPENAI_API_KEY``CRS_OAI_KEY` 等 provider 所需变量。这些 provider 环境变量和 auth 文件不得写入仓库,必须由 D601 运行时文件或 k8s secret 注入,确保容器重建和重启后不会丢失认证。新增 provider 的 `env_key` 时必须增加同类运行时透传和 Compose/k8s 持久化,禁止把 Codex 或 MiniMax 密钥写入仓库文件。Code Queue 容器必须只读挂载 D601 WSL host 的 SSH 目录到 `/root/.ssh`(默认 `/home/ubuntu/.ssh`),让容器内 `git push``ssh -T git@github.com` 与 WSL host 使用同一套 GitHub SSH key/known_hosts;不得把私钥复制进镜像或仓库。
- Skill 注入边界:DEV Code Queue scheduler/read/write Pod 必须把宿主 `/home/ubuntu/.agents/skills` 只读挂载到容器 `/root/.agents/skills`,并设置 `UNIDESK_SKILLS_PATH=/root/.agents/skills`,让执行任务能读取 `cli-spec` 等技能;只允许挂载 skill 目录本身,不得把宿主 `~/.agents``~/.codex`、token、auth JSON 或其他隐私配置整体暴露给任务容器。`/health``/api/dev-ready` 必须暴露非敏感 `skills` 状态:路径、exists、available、readonly、skillCount、`cliSpecAvailable` 和修复建议;CLI `codex dev-ready` 可读取该摘要。当前交付只要求 DEV manifest 和旧 direct Compose 诊断路径具备只读 skill 注入;PROD Code Queue 发布前必须单独审查隔离级别,不能把 DEV 桥接模式直接推广为生产默认。
- Develop-ready 镜像:Code Queue 镜像必须在启动前预装 UniDesk/Pipeline 调试所需工具,至少包含 `codex``bun``node``npm`/`npx``git``rg``curl``python3`/`pip3``docker``docker compose``docker-compose``jq``ssh``rsync``make``gcc`/`g++``iptables``tar``gzip``unzip`;不得依赖 Codex 任务运行时再 `apt-get install` 这些基础环境。
- 远程开发容器与任务执行 ProviderCode Queue 必须能通过 live API 拉起 D601 等计算节点上的开发容器,入口为 `POST /api/dev-containers/<providerId>/start`,默认 Provider 为 `D601`。该流程由 Code Queue 调用 UniDesk `ssh <providerId>` 维护桥在目标节点创建 `unidesk-codex-dev-<providerId>`,并在 Code Queue 所在节点与开发容器之间建立 `ssh -w` TUN 点对点链路;服务所在节点负责对开发容器的 TUN 源地址做 NAT/MASQUERADE,开发容器默认路由和 DNS 改走该 TUN,从而让 `ping google.com`、DNS、HTTP(S) 等出网都经主 server 全局代理,而不是依赖 D601 本地网络。提交 Code Queue 任务时必须支持选择执行 Provider:`D601` 在 D601 原生 k3s 的 active Code Queue scheduler/runner Pod 中本机执行,默认工作目录为 `/workspace`,并且 `/workspace` 必须映射 D601 WSL host 的 `/home/ubuntu`;同一个 hostPath 还必须挂载到容器内 `/home/ubuntu`,让 WSL home 里的绝对 symlink(例如 `/workspace/cq-deploy -> /home/ubuntu/unidesk-code-queue-deploy`)在任务中可解析,不能只看到 symlink 名而无法进入目标目录。`/root/unidesk``/app` 必须单独映射 `/home/ubuntu/cq-deploy` 作为服务部署仓库;其他 Provider 在对应 `unidesk-codex-dev-<providerId>` 容器中执行,默认工作目录为 `/home/ubuntu`,可按任务覆盖 `cwd`。远程任务启动前必须自动复用或拉起该 Provider 的开发容器、同步 Codex 配置和允许的运行时 provider 环境变量,并通过同一 master TUN/NAT 链路出网;目标 host 存在 `/mnt` 时,开发容器必须挂载 host `/mnt:/mnt`,确保 D601 这类 WSL 节点的 Windows 盘符路径如 `/mnt/f/Work/ConStart` 在任务容器内可见,避免 agent 因缺少真实工作区而搜索到无关项目。TUN 建立必须幂等处理 stale 状态:启动前清理旧 `tun<id>`、默认路由、旧 tunnel SSH 进程和旧 OUTPUT 跳转,缺失旧设备不能导致失败,冷启动运行时准备要有有界但足够的 timeout。TUN 建立后必须创建 `UD-CQ-EGRESS-<provider>` OUTPUT 链,规则只允许 loopback、既有连接、`tun<id>` 出口以及到 master server 的 SSH tunnel 控制连接,随后 reject 其他 IPv4/IPv6 出站包;这条网络层封口是开发/执行容器的权威外网边界,不能用 `HTTP_PROXY`/`NO_PROXY` 环境变量替代,容器镜像也必须使用已解析出的唯一 `unidesk-code-queue:<provider>` 或显式 `image`,缺失时直接失败,禁止 provider-gateway image、`latest` 或其他隐式镜像 fallback。验收必须保留三类日志:容器建隧道后 `ping google.com` 成功、强制指定原 Docker 网卡直连外网被 `sealed_direct_ping=blocked_expected` 拦截、服务所在节点上对应 `UNIDESK-CODEX-DEV-<providerId>` NAT 链或 `tun<id>` 计数在 ping 前后增长;涉及 WSL 工作区任务时还必须在开发容器内验证目标 `/mnt/...` 路径可读。`GET /api/dev-containers/<providerId>/status` 必须展示默认路由、`route_8_8_8_8``egressFirewallChain` 和 OUTPUT 链跳转。开发容器代理密钥只生成到 `.state/code-queue/dev-proxy/` 与目标节点用户目录,不得提交到仓库。
- 远程维护桥调用:Code Queue 已迁移到 D601 后,Code Queue 后端 Pod 内没有主 server 的 `unidesk-backend-core` 容器,不能再把 `bun scripts/cli.ts ssh ...` 实现为本地 `docker exec unidesk-backend-core`。Code Queue 后端发起的 provider 维护命令必须通过主 server frontend `/api/dispatch` 进入 backend-core,再由目标 provider-gateway 执行 `host.ssh`;需要传递脚本时必须使用 base64 临时文件,超过 Host SSH 单命令长度上限时分块上传到目标 `/tmp` 后再执行,避免恢复到本地 Docker broker、交互 stdin 或手工 shell fallback。
+401 -29
View File
@@ -60,7 +60,10 @@ interface CompactTaskMutationResponseOptions {
interface CodexTasksOptions {
queueId: string | undefined;
limit: number;
beforeId: string | undefined;
unreadOnly: boolean;
statusFilter: string[] | null;
view: "supervisor" | "full";
}
interface CodexTasksEntry {
@@ -73,10 +76,15 @@ interface CodexTasksEntry {
readAt: string | null;
unread: boolean;
unreadTerminal: boolean;
promptPreview: Record<string, unknown>;
queuedReason: Record<string, unknown> | null;
lastAssistantMessage: Record<string, unknown> | null;
commands: {
show: string;
trace: string;
output: string;
read: string;
full: string;
};
}
@@ -84,15 +92,26 @@ interface CodexTasksSection {
count: number;
returned: number;
truncated: boolean;
hasMore: boolean;
commands: {
next: string | null;
full: string;
};
items: CodexTasksEntry[];
}
interface CodexTasksDegraded {
summaryFetchFailedTaskIds: string[];
summaryFetchErrorCount: number;
summaryFetchOmittedTaskCount: number;
reason: string;
}
interface CodexQueuesOptions {
full: boolean;
limit: number;
}
type CodexRequestInit = { method?: string; body?: unknown };
type CodexResponseFetcher = (path: string, init?: CodexRequestInit) => unknown;
type AsyncCodexResponseFetcher = (path: string, init?: CodexRequestInit) => Promise<unknown>;
@@ -300,6 +319,23 @@ function hasFlag(args: string[], name: string): boolean {
return args.includes(name);
}
function assertKnownOptions(args: string[], spec: { flags?: string[]; valueOptions?: string[] }, command: string): void {
const flags = new Set(spec.flags ?? []);
const valueOptions = new Set(spec.valueOptions ?? []);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (!arg.startsWith("--")) continue;
if (flags.has(arg)) continue;
if (valueOptions.has(arg)) {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
index += 1;
continue;
}
throw new Error(`unsupported ${command} option: ${arg}`);
}
}
function textView(text: string, full: boolean, maxChars: number): Record<string, unknown> {
const truncated = !full && text.length > maxChars;
return {
@@ -324,6 +360,21 @@ function compactLastAssistant(value: unknown, full: boolean): Record<string, unk
};
}
function compactQueuedReason(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
code: record.code ?? null,
label: record.label ?? null,
message: textPreview(asString(record.message), 280),
blockerTaskId: record.blockerTaskId ?? null,
blockerQueueId: record.blockerQueueId ?? null,
waitPosition: record.waitPosition ?? null,
activeRunSlotCount: record.activeRunSlotCount ?? null,
maxActiveQueues: record.maxActiveQueues ?? null,
};
}
function compactSchedulerHeartbeat(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
@@ -667,6 +718,10 @@ function compactTracePage(body: Record<string, unknown>, taskId: string, limit:
}
function parseTaskOptions(args: string[]): CodexTaskOptions {
assertKnownOptions(args, {
flags: ["--trace", "--tail", "--from-start", "--first", "--full", "--raw-summary"],
valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--trace-limit", "--limit", "--tool-limit"],
}, "codex task");
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
@@ -685,6 +740,10 @@ function parseTaskOptions(args: string[]): CodexTaskOptions {
}
function parseOutputOptions(args: string[]): CodexOutputOptions {
assertKnownOptions(args, {
flags: ["--tail", "--from-start", "--first", "--full-text", "--raw"],
valueOptions: ["--before-seq", "--beforeSeq", "--after-seq", "--afterSeq", "--limit", "--max-text-chars"],
}, "codex output");
const beforeSeq = nullablePositiveNumberOption(args, ["--before-seq", "--beforeSeq"]);
const afterSeq = nonNegativeNumberOption(args, ["--after-seq", "--afterSeq"], 0);
const fromStart = hasFlag(args, "--from-start") || hasFlag(args, "--first");
@@ -700,14 +759,47 @@ function parseOutputOptions(args: string[]): CodexOutputOptions {
}
function parseTasksOptions(args: string[]): CodexTasksOptions {
assertKnownOptions(args, {
flags: ["--unread-only", "--unread", "--full", "--all"],
valueOptions: ["--queue", "--queue-id", "--limit", "--status", "--view", "--before-id", "--beforeId"],
}, "codex tasks");
const viewValue = optionValue(args, ["--view"]) ?? "supervisor";
if (viewValue !== "supervisor" && viewValue !== "full") throw new Error(`--view must be supervisor or full; got ${viewValue}`);
const statusRaw = optionValue(args, ["--status"]);
const statusFilter = statusRaw === undefined
? null
: statusRaw.split(/[,\s]+/u).map((item) => item.trim()).filter(Boolean);
if (statusFilter !== null) {
const supported = new Set(["queued", "running", "judging", "retry_wait", "succeeded", "failed", "canceled"]);
const unsupported = statusFilter.filter((status) => !supported.has(status));
if (unsupported.length > 0) throw new Error(`unsupported --status value: ${unsupported.join(", ")}; supported: ${Array.from(supported).join(", ")}`);
}
return {
queueId: optionValue(args, ["--queue", "--queue-id"]),
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
unreadOnly: hasFlag(args, "--unread-only"),
beforeId: optionValue(args, ["--before-id", "--beforeId"]),
unreadOnly: hasFlag(args, "--unread-only") || hasFlag(args, "--unread"),
statusFilter,
view: hasFlag(args, "--full") || hasFlag(args, "--all") ? "full" : viewValue,
};
}
function parseQueuesOptions(args: string[]): CodexQueuesOptions {
assertKnownOptions(args, {
flags: ["--full", "--all"],
valueOptions: ["--limit"],
}, "codex queues");
return {
full: hasFlag(args, "--full") || hasFlag(args, "--all"),
limit: positiveIntegerOption(args, ["--limit"], defaultTasksLimit, maxTasksLimit),
};
}
function parseJudgeOptions(args: string[]): CodexJudgeOptions {
assertKnownOptions(args, {
flags: ["--dry-run", "--no-call", "--include-prompt"],
valueOptions: ["--attempt", "--attempt-id", "--attemptIndex"],
}, "codex judge");
const rawAttempt = optionValue(args, ["--attempt", "--attempt-id", "--attemptIndex"]) ?? positionalArgs(args)[0];
let attempt: number | null = null;
if (rawAttempt !== undefined) {
@@ -823,7 +915,7 @@ function isTerminalTaskStatus(status: unknown): boolean {
}
function isActiveTaskStatus(status: unknown): boolean {
return status === "running" || status === "judging" || status === "retry_wait" || status === "queued";
return status === "running" || status === "judging";
}
function taskStatusRank(status: unknown): number {
@@ -855,16 +947,24 @@ function taskUnreadTerminal(task: Record<string, unknown>): boolean {
return isTerminalTaskStatus(status) && (readAt === null || readAt === undefined || readAt === "");
}
function taskQueuedRunnable(task: Record<string, unknown>): boolean {
const status = asString(task.status);
if (status === "queued" || status === "retry_wait") return true;
return asRecord(task.queuedReason)?.code === "ready";
}
function taskWatchEntry(task: Record<string, unknown>, summary: Record<string, unknown> | null): CodexTasksEntry {
const taskId = asString(task.id);
const summaryCommands = summary === null ? null : asRecord(summary.commands);
const summaryLastAssistant = summary?.lastAssistantMessage ?? task.lastAssistantMessage;
const promptPreview = textPreview(asString(task.displayPrompt ?? task.basePrompt ?? task.prompt), 360);
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 outputCommand = `bun scripts/cli.ts codex output ${taskId} --tail --limit ${defaultOutputLimit}`;
return {
taskId,
queueId: asString(task.queueId) || null,
@@ -875,31 +975,51 @@ function taskWatchEntry(task: Record<string, unknown>, summary: Record<string, u
readAt: asString(task.readAt) || null,
unread: taskUnreadTerminal(task),
unreadTerminal: taskUnreadTerminal(task),
promptPreview,
queuedReason: compactQueuedReason(task.queuedReason),
lastAssistantMessage: summaryLastAssistant === undefined || summaryLastAssistant === null ? null : compactLastAssistant(summaryLastAssistant, false),
commands: {
show: typeof summaryCommands?.show === "string" && summaryCommands.show.length > 0 ? summaryCommands.show : showCommand,
trace: typeof summaryCommands?.trace === "string" && summaryCommands.trace.length > 0 ? summaryCommands.trace : traceCommand,
output: outputCommand,
read: `bun scripts/cli.ts codex read ${taskId}`,
full: `bun scripts/cli.ts codex task ${taskId} --full`,
},
};
}
function buildTaskWatchSection(tasks: Record<string, unknown>[], summaries: Map<string, Record<string, unknown>>, limit: number): CodexTasksSection {
function buildTaskWatchSection(
tasks: Record<string, unknown>[],
summaries: Map<string, Record<string, unknown>>,
limit: number,
nextCommand: string | null,
fullCommand: string,
): CodexTasksSection {
const visibleTasks = tasks.slice(0, limit);
const items = visibleTasks.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null));
const truncated = tasks.length > limit;
return {
count: tasks.length,
returned: items.length,
truncated: tasks.length > limit,
truncated,
hasMore: truncated,
commands: {
next: truncated ? nextCommand : null,
full: fullCommand,
},
items,
};
}
function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>): CodexTasksDegraded | null {
if (summaryErrors.length === 0) return null;
function collectTaskWatchDegraded(summaryErrors: Array<{ taskId: string; message: string }>, omittedTaskCount = 0): CodexTasksDegraded | null {
if (summaryErrors.length === 0 && omittedTaskCount === 0) return null;
return {
summaryFetchFailedTaskIds: summaryErrors.map((error) => error.taskId),
summaryFetchErrorCount: summaryErrors.length,
reason: "task summary fetch failed for one or more entries; unread state still comes from task-level overview data",
summaryFetchOmittedTaskCount: omittedTaskCount,
reason: summaryErrors.length > 0
? "task summary fetch failed for one or more entries; unread state still comes from task-level overview data"
: "task summary fetch was intentionally omitted for bounded full view; use commands.show for per-task detail",
};
}
@@ -925,10 +1045,46 @@ function sortCompletedWatchTasks(tasks: Record<string, unknown>[]): Record<strin
});
}
function sortQueuedWatchTasks(tasks: Record<string, unknown>[]): Record<string, unknown>[] {
return [...tasks]
.filter((task) => taskQueuedRunnable(task))
.sort((left, right) => {
const rankDelta = taskStatusRank(asString(left.status)) - taskStatusRank(asString(right.status));
if (rankDelta !== 0) return rankDelta;
const timeDelta = taskTimelineMs(left) - taskTimelineMs(right);
if (timeDelta !== 0) return timeDelta;
return asString(left.id).localeCompare(asString(right.id));
});
}
function taskMatchesStatusFilter(task: Record<string, unknown>, statusFilter: string[] | null): boolean {
return statusFilter === null || statusFilter.includes(asString(task.status));
}
function filterTasksForOptions(tasks: Record<string, unknown>[], options: CodexTasksOptions): Record<string, unknown>[] {
return tasks
.filter((task) => options.queueId === undefined || asString(task.queueId) === options.queueId)
.filter((task) => taskMatchesStatusFilter(task, options.statusFilter))
.filter((task) => !options.unreadOnly || taskUnreadTerminal(task));
}
function taskListCommand(options: CodexTasksOptions, extra: string[] = []): string {
const args = ["codex", "tasks"];
if (options.view !== "supervisor") args.push("--view", options.view);
if (options.queueId !== undefined) args.push("--queue", options.queueId);
if (options.unreadOnly) args.push("--unread");
if (options.statusFilter !== null) args.push("--status", options.statusFilter.join(","));
if (options.limit !== defaultTasksLimit) args.push("--limit", String(options.limit));
if (options.beforeId !== undefined) args.push("--before-id", options.beforeId);
args.push(...extra);
return `bun scripts/cli.ts ${args.join(" ")}`;
}
function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): { summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null } {
const boundedIds = taskIds.slice(0, maxTasksLimit);
const summaries = new Map<string, Record<string, unknown>>();
const errors: Array<{ taskId: string; message: string }> = [];
for (const taskId of taskIds) {
for (const taskId of boundedIds) {
try {
const response = unwrapCodexResponse(fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
const summary = asRecord(response.body.summary) ?? {};
@@ -937,13 +1093,14 @@ function fetchTaskSummaries(taskIds: string[], fetcher: CodexResponseFetcher): {
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
}
}
return { summaries, degraded: collectTaskWatchDegraded(errors) };
return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) };
}
async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexResponseFetcher): Promise<{ summaries: Map<string, Record<string, unknown>>; degraded: CodexTasksDegraded | null }> {
const boundedIds = taskIds.slice(0, maxTasksLimit);
const summaries = new Map<string, Record<string, unknown>>();
const errors: Array<{ taskId: string; message: string }> = [];
await Promise.all(taskIds.map(async (taskId) => {
await Promise.all(boundedIds.map(async (taskId) => {
try {
const response = unwrapCodexResponse(await fetcher(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/summary${queryString({ toolLimit: 1 })}`)));
const summary = asRecord(response.body.summary) ?? {};
@@ -952,7 +1109,7 @@ async function fetchTaskSummariesAsync(taskIds: string[], fetcher: AsyncCodexRes
errors.push({ taskId, message: error instanceof Error ? error.message : String(error) });
}
}));
return { summaries, degraded: collectTaskWatchDegraded(errors) };
return { summaries, degraded: collectTaskWatchDegraded(errors, Math.max(0, taskIds.length - boundedIds.length)) };
}
type CodexTasksTaskPage = {
@@ -966,6 +1123,7 @@ function tasksListQueryString(options: CodexTasksOptions): string {
limit: 200,
priorityLimit: 200,
queueId: options.queueId,
beforeId: options.beforeId,
includeActive: 1,
selected: 0,
compact: 1,
@@ -1000,21 +1158,32 @@ function codexTasksOverviewResult(
summaries: Map<string, Record<string, unknown>>,
degraded: CodexTasksDegraded | null,
): Record<string, unknown> {
const allTasks = options.queueId === undefined ? taskPage.tasks : taskPage.tasks.filter((task) => asString(task.queueId) === options.queueId);
if (options.view === "full") return codexTasksFullResult(taskPage, upstream, options, summaries, degraded);
const allTasks = filterTasksForOptions(taskPage.tasks, options);
const runningTasks = sortRunningWatchTasks(allTasks);
const unreadCompletedTasks = sortCompletedWatchTasks(allTasks).filter((task) => taskUnreadTerminal(task));
const recentCompletedTasks = options.unreadOnly ? [] : sortCompletedWatchTasks(allTasks);
const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit);
const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit);
const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit);
const queuedTasks = options.unreadOnly ? [] : sortQueuedWatchTasks(allTasks);
const nextBeforeId = asString(taskPage.pagination.nextBeforeId) || null;
const sourceHasMore = asBoolean(taskPage.pagination.hasMore);
const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null;
const fullCommand = taskListCommand({ ...options, view: "full" });
const runningSection = buildTaskWatchSection(runningTasks, summaries, options.limit, nextCommand, fullCommand);
const unreadSection = buildTaskWatchSection(unreadCompletedTasks, summaries, options.limit, nextCommand, fullCommand);
const recentSection = buildTaskWatchSection(recentCompletedTasks, summaries, options.limit, nextCommand, fullCommand);
const queuedSection = buildTaskWatchSection(queuedTasks, summaries, options.limit, nextCommand, fullCommand);
const pagination = taskPage.pagination;
const diagnostics = compactExecutionDiagnostics(asRecord(taskPage.queue)?.executionDiagnostics);
return {
upstream,
overview: {
supervisor: {
filters: {
view: options.view,
queueId: options.queueId ?? null,
limit: options.limit,
unreadOnly: options.unreadOnly,
status: options.statusFilter,
beforeId: options.beforeId ?? null,
},
source: {
endpoint: "/api/tasks/overview",
@@ -1026,29 +1195,92 @@ function codexTasksOverviewResult(
nextBeforeId: asString(pagination.nextBeforeId) || null,
includeActive: asBoolean(pagination.includeActive),
},
queue: taskPage.queue,
bounded: true,
disclosure: {
defaultView: "supervisor",
fullCommand,
next: nextCommand,
rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`,
},
counts: {
scanned: allTasks.length,
running: runningSection.count,
completedUnread: unreadSection.count,
recentCompleted: recentSection.count,
queued: queuedSection.count,
},
executionDiagnostics: diagnostics,
degraded,
commands: {
refresh: `bun scripts/cli.ts codex tasks${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.unreadOnly ? " --unread-only" : ""}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
refresh: taskListCommand(options),
unread: taskListCommand({ ...options, unreadOnly: true }),
byStatus: `bun scripts/cli.ts codex tasks --status <status>${options.queueId === undefined ? "" : ` --queue ${options.queueId}`}${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
full: fullCommand,
next: nextCommand,
},
running: runningSection,
completedUnread: unreadSection,
recentCompleted: recentSection,
queued: queuedSection,
},
};
}
function codexTasksFullResult(
taskPage: CodexTasksTaskPage,
upstream: { ok: unknown; status: unknown },
options: CodexTasksOptions,
summaries: Map<string, Record<string, unknown>>,
degraded: CodexTasksDegraded | null,
): Record<string, unknown> {
const filtered = filterTasksForOptions(taskPage.tasks, options);
const visible = filtered.slice(0, options.limit);
const pagination = taskPage.pagination;
const nextBeforeId = asString(pagination.nextBeforeId) || null;
const sourceHasMore = asBoolean(pagination.hasMore);
const nextCommand = sourceHasMore && nextBeforeId !== null ? taskListCommand({ ...options, beforeId: nextBeforeId }) : null;
return {
upstream,
tasks: {
filters: {
view: "full",
queueId: options.queueId ?? null,
limit: options.limit,
unreadOnly: options.unreadOnly,
status: options.statusFilter,
beforeId: options.beforeId ?? null,
},
source: {
endpoint: "/api/tasks/overview",
returned: asNumber(pagination.returned, 0) || null,
total: asNumber(pagination.total, 0) || null,
hasMore: asBoolean(pagination.hasMore),
nextBeforeId,
},
count: filtered.length,
returned: visible.length,
hasMore: filtered.length > visible.length || sourceHasMore,
degraded,
commands: {
supervisor: taskListCommand({ ...options, view: "supervisor" }),
next: nextCommand,
rawOverview: `bun scripts/cli.ts microservice proxy code-queue /api/tasks/overview${tasksListQueryString(options)} --raw`,
},
items: visible.map((task) => taskWatchEntry(task, summaries.get(taskOverviewCandidateKey(task)) ?? null)),
},
};
}
function visibleTaskIdsForOverview(tasks: Record<string, unknown>[], options: CodexTasksOptions): string[] {
const filtered = filterTasksForOptions(tasks, options);
if (options.view === "full") {
return filtered.slice(0, options.limit).map((task) => taskOverviewCandidateKey(task)).filter((taskId) => taskId.length > 0);
}
return Array.from(new Set([
...sortRunningWatchTasks(tasks).slice(0, options.limit),
...sortCompletedWatchTasks(tasks).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit),
...sortCompletedWatchTasks(tasks).slice(0, options.limit),
...sortRunningWatchTasks(filtered).slice(0, options.limit),
...sortCompletedWatchTasks(filtered).filter((task) => taskUnreadTerminal(task)).slice(0, options.limit),
...sortCompletedWatchTasks(filtered).slice(0, options.limit),
...sortQueuedWatchTasks(filtered).slice(0, options.limit),
].map((task) => taskOverviewCandidateKey(task))))
.filter((taskId) => taskId.length > 0);
}
@@ -1211,15 +1443,77 @@ function requireMergeTargetQueueId(args: string[], command: string): string {
return raw.trim();
}
function codeQueues(): unknown {
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
function compactQueueRow(value: unknown): Record<string, unknown> {
const record = asRecord(value) ?? {};
return {
upstream: response.upstream,
queues: response.body.queues ?? [],
queue: compactQueueMutationSummary(response.body.queue),
id: record.id ?? null,
name: record.name ?? null,
total: record.total ?? null,
counts: record.counts ?? {},
unreadTerminal: record.unreadTerminal ?? 0,
activeTaskId: record.activeTaskId ?? null,
runnableTaskId: record.runnableTaskId ?? null,
processing: record.processing ?? null,
updatedAt: record.updatedAt ?? null,
commands: {
tasks: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --limit ${defaultTasksLimit}`,
unread: record.id === undefined || record.id === null ? null : `bun scripts/cli.ts codex tasks --queue ${String(record.id)} --unread --limit ${defaultTasksLimit}`,
},
};
}
function compactQueuesResponse(body: Record<string, unknown>, options: CodexQueuesOptions, upstream: { ok: unknown; status: unknown }): Record<string, unknown> {
const queue = asRecord(body.queue) ?? asRecord(body.summary) ?? {};
const queues = asArray(body.queues).map(compactQueueRow);
const activeIds = stringList(queue.activeQueueIds);
const nonemptyQueues = queues.filter((row) => asNumber(row.total, 0) > 0);
const unreadQueues = queues.filter((row) => asNumber(row.unreadTerminal, 0) > 0);
const runnableQueues = queues.filter((row) => row.runnableTaskId !== null && row.runnableTaskId !== undefined);
const activeQueues = queues.filter((row) => typeof row.id === "string" && activeIds.includes(row.id));
const selected = options.full ? queues : Array.from(new Map([...activeQueues, ...unreadQueues, ...runnableQueues, ...nonemptyQueues].map((row) => [String(row.id), row])).values());
const visible = selected.slice(0, options.limit);
const diagnostics = compactExecutionDiagnostics(queue.executionDiagnostics);
return {
upstream,
queues: {
view: options.full ? "full" : "summary",
bounded: true,
count: selected.length,
returned: visible.length,
hasMore: selected.length > visible.length,
totals: {
totalTasks: queue.total ?? null,
queueCount: queue.queueCount ?? queues.length,
activeQueueCount: activeIds.length,
nonemptyQueueCount: nonemptyQueues.length,
unreadQueueCount: unreadQueues.length,
runnableQueueCount: runnableQueues.length,
},
activeQueueIds: queue.activeQueueIds ?? [],
activeTaskIds: queue.activeTaskIds ?? [],
queuedTaskIds: queue.queuedTaskIds ?? [],
counts: queue.counts ?? {},
unreadTerminal: queue.unreadTerminal ?? 0,
executionDiagnostics: diagnostics,
items: visible,
commands: {
refresh: `bun scripts/cli.ts codex queues${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
full: `bun scripts/cli.ts codex queues --full${options.limit === defaultTasksLimit ? "" : ` --limit ${options.limit}`}`,
tasks: `bun scripts/cli.ts codex tasks --view supervisor --limit ${Math.min(options.limit, defaultTasksLimit)}`,
unread: `bun scripts/cli.ts codex tasks --unread --limit ${Math.min(options.limit, defaultTasksLimit)}`,
raw: "bun scripts/cli.ts microservice proxy code-queue /api/queues --raw",
},
},
};
}
function codeQueues(optionArgs: string[] = []): unknown {
const options = parseQueuesOptions(optionArgs);
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues")));
if (options.full) return { upstream: response.upstream, queues: response.body.queues ?? [], queue: compactQueueMutationSummary(response.body.queue), commands: { summary: "bun scripts/cli.ts codex queues" } };
return compactQueuesResponse(response.body, options, response.upstream);
}
function codexCreateQueue(queueId: string): unknown {
return unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/queues"), { method: "POST", body: { queueId } }));
}
@@ -1275,6 +1569,27 @@ function referenceTaskIdsFromOptions(args: string[]): string[] {
}
function parseSubmitOptions(args: string[]): CodexSubmitOptions {
assertKnownOptions(args, {
flags: ["--prompt-stdin", "--stdin", "--dry-run"],
valueOptions: [
"--prompt-file",
"--file",
"--queue",
"--queue-id",
"--provider-id",
"--provider",
"--cwd",
"--workdir",
"--model",
"--reasoning-effort",
"--execution-mode",
"--mode",
"--max-attempts",
"--reference-task-id",
"--reference",
"--ref",
],
}, "codex submit");
const maxAttempts = args.some((arg) => arg === "--max-attempts")
? positiveIntegerOption(args, ["--max-attempts"], 99, 99)
: undefined;
@@ -1339,6 +1654,19 @@ function compactTaskMutationResponse(task: unknown, options: CompactTaskMutation
};
}
function codexReadTask(taskId: string): unknown {
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath(`/api/tasks/${encodeURIComponent(taskId)}/read`), { method: "POST", body: {} }));
return {
upstream: response.upstream,
task: compactTaskMutationResponse(response.body.task),
queue: compactQueueMutationSummary(response.body.queue),
commands: {
show: `bun scripts/cli.ts codex task ${taskId}`,
unread: `bun scripts/cli.ts codex tasks --unread --limit ${defaultTasksLimit}`,
},
};
}
function compactQueueMutationSummary(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
@@ -1355,6 +1683,42 @@ function compactQueueMutationSummary(value: unknown): Record<string, unknown> |
};
}
function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
const record = asRecord(value);
if (record === null) return null;
return {
path: record.path ?? null,
mountPoint: record.mountPoint ?? null,
exists: record.exists ?? false,
available: record.available ?? false,
readonly: record.readonly ?? false,
skillCount: record.skillCount ?? 0,
cliSpecAvailable: record.cliSpecAvailable ?? false,
repairHint: record.repairHint ?? null,
};
}
function codeQueueDevReady(): unknown {
const response = unwrapCodexResponse(coreInternalFetch(codeQueueProxyPath("/api/dev-ready")));
const devReady = asRecord(response.body.devReady) ?? {};
return {
upstream: response.upstream,
devReady: {
ok: devReady.ok ?? false,
missingTools: devReady.missingTools ?? [],
workdir: devReady.workdir ?? null,
docker: devReady.docker ?? null,
codexConfig: devReady.codexConfig ?? null,
ssh: devReady.ssh ?? null,
skills: compactSkillsStatus(devReady.skills),
},
commands: {
health: "bun scripts/cli.ts codex dev-ready",
raw: "bun scripts/cli.ts microservice proxy code-queue /api/dev-ready --raw",
},
};
}
function codexSubmitTask(args: string[]): unknown {
const options = parseSubmitOptions(args);
const payload = submitPayload(options);
@@ -1399,6 +1763,10 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
if (action === "tasks" || action === "overview") {
return codexTasksQuery(args.slice(1));
}
if (action === "dev-ready" || action === "health") {
assertKnownOptions(args.slice(1), {}, `codex ${action}`);
return codeQueueDevReady();
}
if (action === "output") {
const taskId = requireTaskId(taskIdArg, "codex output");
return codexOutputQuery(taskId, args.slice(2));
@@ -1407,10 +1775,14 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, "codex judge");
return codexJudgeQuery(taskId, args.slice(2));
}
if (action === "queues") return codeQueues();
if (action === "read" || action === "mark-read") {
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexReadTask(taskId);
}
if (action === "queues") return codeQueues(args.slice(1));
if (action === "queue") {
const sub = taskIdArg ?? "list";
if (sub === "list") return codeQueues();
if (sub === "list") return codeQueues(args.slice(2));
if (sub === "create") return codexCreateQueue(requireQueueId(args.slice(2), "queue create"));
if (sub === "merge") {
const mergeArgs = args.slice(2);
@@ -1425,5 +1797,5 @@ export async function runCodeQueueCommand(_config: UniDeskConfig, args: string[]
const taskId = requireTaskId(taskIdArg, `codex ${action}`);
return codexInterruptTask(taskId);
}
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, queues, queue list, queue create, queue merge, move, interrupt, cancel");
throw new Error("codex command must be one of: submit, enqueue, task, summary, show, tasks, overview, output, judge, read, mark-read, dev-ready, health, queues, queue list, queue create, queue merge, move, interrupt, cancel");
}
+9 -5
View File
@@ -44,11 +44,13 @@ export function rootHelp(): unknown {
{ command: "codex deploy <commitId> [--provider-id D601] [--timeout-ms N]", description: "Disabled legacy Code Queue deploy path; use the dev-only artifact consumer instead." },
{ command: "codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]", description: "Submit a Code Queue task through backend-core -> code-queue proxy; --dry-run shows the structured request without enqueueing." },
{ command: "codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]", description: "Fetch a compact Code Queue task summary; trace rows are opt-in and paged with next/previous commands to avoid output explosion." },
{ command: "codex tasks [--queue id] [--limit N] [--unread-only]", description: "Show the current running, unread terminal, and recent completed Code Queue tasks in one JSON view." },
{ command: "codex tasks [--view supervisor|full] [--queue id] [--status status[,status]] [--unread|--unread-only] [--limit N] [--before-id id]", description: "Show the bounded supervisor view by default: running, unread terminal, recent completed, queued, diagnostics, and drill-down commands." },
{ command: "codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]", description: "Fetch paged raw Code Queue output records by seq when a trace row has omitted command/output text." },
{ command: "codex read <taskId>", description: "Mark one reviewed terminal task read; never run automatically as part of listing." },
{ command: "codex dev-ready", description: "Fetch execution-container readiness, including sanitized skill injection status from /api/dev-ready." },
{ command: "codex judge <taskId> --attempt N [--dry-run] [--include-prompt]", description: "Replay one stored Code Queue attempt through the same judge context builder and MiniMax judge call path used by the live queue worker." },
{ command: "codex interrupt|cancel <taskId>", description: "Request interrupt for a running Code Queue task, or cancel a queued/retry_wait task, through the same private proxy." },
{ command: "codex (queues | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List/create/merge Code Queue lanes and move a queued task; merge preserves task queue time order and deletes the source queue record." },
{ command: "codex (queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>)", description: "List low-noise queue summaries by default; full queue rows require --full/--all." },
{ command: "job list [--limit N] [--include-command]", description: "List async jobs from .state/jobs with a bounded default page." },
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." },
{ command: "debug health", description: "Probe internal core, nodes, system/Docker status, frontend, provider ingress, and public boundary." },
@@ -186,17 +188,19 @@ function scheduleHelp(): unknown {
function codexHelp(): unknown {
return {
command: "codex deploy|submit|task|tasks|output|judge|interrupt|cancel|queues|queue|move",
command: "codex deploy|submit|task|tasks|output|read|dev-ready|judge|interrupt|cancel|queues|queue|move",
output: "json",
usage: [
"bun scripts/cli.ts codex deploy <commitId> # disabled legacy deployment entry",
"bun scripts/cli.ts codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue id] [--dry-run]",
"bun scripts/cli.ts codex task <taskId> [--trace --tail|--from-start|--after-seq N|--before-seq N --limit N] [--full]",
"bun scripts/cli.ts codex tasks [--queue id] [--limit N] [--unread-only]",
"bun scripts/cli.ts codex tasks [--view supervisor|full] [--queue id] [--status succeeded,running] [--unread|--unread-only] [--limit N] [--before-id id]",
"bun scripts/cli.ts codex output <taskId> [--tail|--from-start|--after-seq N|--before-seq N --limit N] [--full-text]",
"bun scripts/cli.ts codex read <taskId>",
"bun scripts/cli.ts codex dev-ready",
"bun scripts/cli.ts codex judge <taskId> --attempt N [--dry-run] [--include-prompt]",
"bun scripts/cli.ts codex interrupt|cancel <taskId>",
"bun scripts/cli.ts codex queues | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>",
"bun scripts/cli.ts codex queues [--full|--all] | queue create <queueId> | queue merge <sourceQueueId> --into <targetQueueId> | move <taskId> --queue <queueId>",
],
description: "Operate Code Queue through the stable backend-core private proxy path.",
};
@@ -2,6 +2,7 @@ ARG CODE_QUEUE_BASE_IMAGE=oven/bun:1-debian
FROM ${CODE_QUEUE_BASE_IMAGE}
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
ENV UNIDESK_SKILLS_PATH=/root/.agents/skills
RUN (command -v codex >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && command -v docker >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1) \
|| (apt-get update \
@@ -57,6 +58,7 @@ COPY src/components/shared /app/src/components/shared
WORKDIR /app/src/components/microservices/code-queue
COPY src/components/microservices/code-queue/tsconfig.json ./tsconfig.json
COPY src/components/microservices/code-queue/src ./src
RUN mkdir -p /root/.agents/skills
EXPOSE 4222
ENTRYPOINT ["tini", "--"]
@@ -23,6 +23,7 @@ services:
CODE_QUEUE_SCHEDULER_ENABLED: "${CODE_QUEUE_SCHEDULER_ENABLED:-true}"
CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED: "${CODE_QUEUE_STARTUP_OA_BACKFILL_ENABLED:-false}"
CODE_QUEUE_WORKDIR: "${CODE_QUEUE_WORKDIR:-/workspace}"
UNIDESK_SKILLS_PATH: "${UNIDESK_SKILLS_PATH:-/root/.agents/skills}"
CODE_QUEUE_CODEX_HOME: "/var/lib/unidesk/code-queue/codex-home"
CODE_QUEUE_OPENCODE_XDG_DIR: "/var/lib/unidesk/code-queue/opencode-xdg"
CODE_QUEUE_SOURCE_CODEX_CONFIG: "/root/.codex/config.toml"
@@ -81,6 +82,7 @@ services:
- ${CODE_QUEUE_WORKSPACE_PATH:-/home/ubuntu}:/workspace
- ${CODE_QUEUE_CODEX_CONFIG_PATH:-/home/ubuntu/.codex/config.toml}:/root/.codex/config.toml:ro
- ${CODE_QUEUE_CODEX_AUTH_PATH:-/home/ubuntu/.codex/auth.json}:/root/.codex/auth.json:ro
- ${CODE_QUEUE_SKILLS_PATH:-/home/ubuntu/.agents/skills}:/root/.agents/skills:ro
- ${CODE_QUEUE_SSH_DIR:-/home/ubuntu/.ssh}:/root/.ssh:ro
- ${CODE_QUEUE_LOG_DIR:-../../../../.state/code-queue/logs}:/var/log/unidesk
- ${CODE_QUEUE_STATE_DIR:-../../../../.state/code-queue}:/var/lib/unidesk/code-queue
@@ -384,6 +384,7 @@ function readConfig(): RuntimeConfig {
remoteDefaultWorkdir,
executionProviderIds,
remoteCodexEnvKeys: envList("CODE_QUEUE_REMOTE_CODEX_ENV_KEYS", ["OPENAI_API_KEY", "CRS_OAI_KEY", "OPENAI_BASE_URL", "OPENAI_API_BASE", "MINIMAX_API_KEY", "MINIMAX_API_BASE", "MINIMAX_MODEL"]),
skillsPath: envString("UNIDESK_SKILLS_PATH", "/root/.agents/skills"),
codexHome: envString("CODE_QUEUE_CODEX_HOME", "/var/lib/unidesk/code-queue/codex-home"),
opencodeXdgDir: envString("CODE_QUEUE_OPENCODE_XDG_DIR", resolve(dataDir, "opencode-xdg")),
sourceCodexConfig: envString("CODE_QUEUE_SOURCE_CODEX_CONFIG", "/root/.codex/config.toml"),
@@ -2413,6 +2414,74 @@ function runProbe(command: string, args: string[], timeout = 3_000): { ok: boole
return { ok: result.status === 0, output };
}
function decodeMountInfoPath(value: string): string {
return value.replace(/\\([0-7]{3})/gu, (_match, octal: string) => String.fromCharCode(Number.parseInt(octal, 8)));
}
function mountInfoForPath(path: string): { mountPoint: string | null; readonly: boolean | null } {
try {
const target = resolve(path);
let best: { mountPoint: string; readonly: boolean } | null = null;
for (const line of readFileSync("/proc/self/mountinfo", "utf8").split(/\r?\n/u)) {
if (line.trim().length === 0) continue;
const fields = line.split(" ");
const mountPoint = decodeMountInfoPath(fields[4] ?? "");
const options = (fields[5] ?? "").split(",");
if (mountPoint.length === 0) continue;
const matches = target === mountPoint || target.startsWith(mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`);
if (!matches) continue;
if (best === null || mountPoint.length > best.mountPoint.length) best = { mountPoint, readonly: options.includes("ro") };
}
return best ?? { mountPoint: null, readonly: null };
} catch {
return { mountPoint: null, readonly: null };
}
}
function collectSkillsStatus(): JsonValue {
const path = config.skillsPath;
const exists = existsSync(path);
const mountInfo = mountInfoForPath(path);
let directory = false;
let skillCount = 0;
let cliSpecAvailable = false;
let readonly = mountInfo.readonly === true;
let error: string | null = null;
if (exists) {
try {
const stat = statSync(path);
directory = stat.isDirectory();
if (directory) {
const entries = readdirSync(path, { withFileTypes: true });
skillCount = entries.filter((entry) => entry.isDirectory()).length;
cliSpecAvailable = existsSync(resolve(path, "cli-spec", "SKILL.md"));
if (mountInfo.readonly === null) {
const writeProbe = runProbe("sh", ["-lc", `test ! -w ${shellQuote(path)}`], 2_000);
readonly = writeProbe.ok;
}
}
} catch (probeError) {
error = probeError instanceof Error ? probeError.message : String(probeError);
}
}
const available = exists && directory;
return {
path,
mountPoint: mountInfo.mountPoint,
exists,
directory,
available,
readonly,
skillCount,
cliSpecAvailable,
expectedMount: "host ~/.agents/skills mounted read-only to UNIDESK_SKILLS_PATH",
repairHint: available && readonly && cliSpecAvailable
? null
: "DEV code-queue should mount /home/ubuntu/.agents/skills read-only at /root/.agents/skills and set UNIDESK_SKILLS_PATH=/root/.agents/skills.",
error,
} as unknown as JsonValue;
}
function collectDevReady(): JsonValue {
const now = Date.now();
if (devReadyCache !== null && now - devReadyCache.checkedAtMs < 30_000) return devReadyCache.value;
@@ -2457,7 +2526,9 @@ function collectDevReady(): JsonValue {
const sshKeyProbe = runProbe("sh", ["-lc", "test -d /root/.ssh && find /root/.ssh -maxdepth 1 -type f \\( -name 'id_*' ! -name '*.pub' \\) -perm -400 -print -quit"]);
const githubKnownHostProbe = runProbe("ssh-keygen", ["-F", "github.com", "-f", "/root/.ssh/known_hosts"]);
const sshSharedReady = existsSync("/root/.ssh") && sshKeyProbe.ok && sshKeyProbe.output.trim().length > 0;
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady;
const skills = collectSkillsStatus() as Record<string, JsonValue>;
const skillsReady = skills.available === true && skills.readonly === true && skills.cliSpecAvailable === true;
const ok = missingTools.length === 0 && dockerProbe.ok && composeProbe.ok && workdirExists && dockerSocketExists && codexConfigReady && sshSharedReady && skillsReady;
const value: JsonValue = {
ok,
missingTools,
@@ -2489,6 +2560,7 @@ function collectDevReady(): JsonValue {
githubKnownHostPresent: githubKnownHostProbe.ok,
ready: sshSharedReady,
},
skills,
};
devReadyCache = { checkedAtMs: now, value };
return value;
@@ -5174,6 +5246,7 @@ async function route(req: Request): Promise<Response> {
status: "starting",
databaseReady,
databaseLastError,
skills: collectSkillsStatus(),
startedAt: serviceStartedAt,
}, 503);
return jsonResponse({
@@ -5190,6 +5263,7 @@ async function route(req: Request): Promise<Response> {
schedulerPollIntervalMs: config.schedulerPollIntervalMs,
queue: queueSummary(false, state.tasks),
executionDiagnostics: executionDiagnosticsForTasks(state.tasks),
skills: collectSkillsStatus(),
egressProxy: await providerGatewayEgressProxyStatus(),
oaEventPublisher: oaEventPublisherStatus(),
startedAt: serviceStartedAt,
@@ -8,6 +8,7 @@ export const codeQueueEnvironmentHintTitle = "# Code Queue 运行环境提示";
export const codeQueueEnvironmentHint = [
codeQueueEnvironmentHintTitle,
"如果当前 Code Queue Docker 容器缺少完成任务所需的环境、系统包或语言依赖,可以先在容器内临时安装以推进当前任务;同时必须把该依赖补到 `src/components/microservices/code-queue/Dockerfile`,让后续任务重建镜像后可直接使用。",
"任务可通过 `UNIDESK_SKILLS_PATH`(默认 `/root/.agents/skills`)读取注入的宿主 skills;若需要确认注入状态,查询 Code Queue `/api/dev-ready` 的 `devReady.skills`,缺失时报告该字段的修复建议,不要读取或输出宿主 token/auth 配置。",
].join("\n");
export function stripAutoReferenceHint(prompt: string): string {
@@ -123,6 +123,7 @@ export interface RuntimeConfig {
remoteDefaultWorkdir: string;
executionProviderIds: string[];
remoteCodexEnvKeys: string[];
skillsPath: string;
codexHome: string;
opencodeXdgDir: string;
sourceCodexConfig: string;
@@ -329,6 +329,8 @@ spec:
value: danger-full-access
- name: CODE_QUEUE_APPROVAL_POLICY
value: never
- name: UNIDESK_SKILLS_PATH
value: /root/.agents/skills
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
value: "true"
- name: CODE_QUEUE_EGRESS_PROXY_URL
@@ -377,6 +379,9 @@ spec:
- name: codex-auth
mountPath: /root/.codex/auth.json
readOnly: true
- name: skills-dir
mountPath: /root/.agents/skills
readOnly: true
- name: ssh-dir
mountPath: /root/.ssh
readOnly: true
@@ -433,6 +438,10 @@ spec:
hostPath:
path: /home/ubuntu/.codex/auth.json
type: File
- name: skills-dir
hostPath:
path: /home/ubuntu/.agents/skills
type: Directory
- name: ssh-dir
hostPath:
path: /home/ubuntu/.ssh
@@ -531,6 +540,8 @@ spec:
value: "2"
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
value: "false"
- name: UNIDESK_SKILLS_PATH
value: /root/.agents/skills
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
value: "false"
- name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED
@@ -543,6 +554,9 @@ spec:
- name: repo
mountPath: /root/unidesk
readOnly: true
- name: skills-dir
mountPath: /root/.agents/skills
readOnly: true
- name: logs
mountPath: /var/log/unidesk-dev
- name: state
@@ -580,6 +594,10 @@ spec:
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
type: Directory
- name: skills-dir
hostPath:
path: /home/ubuntu/.agents/skills
type: Directory
- name: logs
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs
@@ -674,6 +692,8 @@ spec:
value: "2"
- name: CODE_QUEUE_EGRESS_PROXY_ENABLED
value: "false"
- name: UNIDESK_SKILLS_PATH
value: /root/.agents/skills
- name: CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED
value: "false"
- name: CODE_QUEUE_CODEX_SQLITE_LOG_EXPORT_ENABLED
@@ -686,6 +706,9 @@ spec:
- name: repo
mountPath: /root/unidesk
readOnly: true
- name: skills-dir
mountPath: /root/.agents/skills
readOnly: true
- name: logs
mountPath: /var/log/unidesk-dev
- name: state
@@ -723,6 +746,10 @@ spec:
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
type: Directory
- name: skills-dir
hostPath:
path: /home/ubuntu/.agents/skills
type: Directory
- name: logs
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/logs