# UniDesk Observability Reference

UniDesk 的可观测性优先级高于静默成功。CLI、服务日志、Docker 日志和数据库状态都必须能通过短命令查询。

## Capability Boundary

可观测性是定位、验收和排障手段，不是功能完成标准。Trace、日志、诊断文案、fallback 标记、只读原因、降级提示和 issue 进展只能说明系统在哪里失败、为什么失败、修复后如何验证；它们不能把缺失能力包装成已完成，也不能把未修复功能包装成可接受状态。

当证据表明某条用户路径需要的能力不存在、没有真正接入、没有复用预期资源、没有返回最终业务结果，或仍停留在 mock、fixture、fallback、只读替代和提示文案时，正确动作是补齐能力或修复功能本身。允许先补最小观测来定位根因，但完成条件必须回到原始用户入口的真实行为闭环，并证明能力已经可用。

因此，CLI、Web、trace 和 issue 评论中的状态字段必须避免把“已观测到缺口”“已显示失败原因”“已提供诊断入口”表达成“问题已解决”。如果当前阶段只能增加可见性，输出应明确标记为诊断进展或阻塞定位；不得关闭对应功能 issue，不得把观测增强当作验收通过。

## CLI Logs

异步 job 的 stdout 和 stderr 位于 `.state/jobs/`。`job list` 默认只返回最新 50 条摘要，并为已知异步工作流返回轻量 `progress.summary`；`job status` 会返回结构化 `progress` 与有限尾部，避免输出爆炸，同时保留完整日志文件路径便于继续排查。实现必须只读取日志尾部字节，不得先把完整 job 日志读入 CLI 内存；长时命令的阶段、关键对象名和下一步查询命令应优先沉淀到 `progress`，不能要求调用者先阅读完整日志才能知道是否卡在提交、构建、发布或观测阶段。

## Service Logs

服务日志位于 `logs/{YYYYMMDD}/`，每次 `server start` 都生成新的本地时间戳前缀。新写入的 UniDesk JSONL 日志必须按小时切片：`logs/{YYYYMMDD}/{startStamp}_{YYYYMMDD}_{HH}_{service}.jsonl`，一天一个目录，禁止长期追加到单个巨大 JSONL。所有 UniDesk Bun 服务（frontend、provider-gateway、Code Queue、project-manager、baidu-netdisk 以及后续新增 Bun 服务）必须复用 `src/components/shared/src/rotating-jsonl.ts` 中的 `createHourlyJsonlWriter`；Rust backend-core 必须提供等价的 hourly rotation and retention behavior in `src/components/backend-core/src/logger.rs`。`LOG_FILE` 只作为推导 `logs` 根目录、启动前缀和 service 后缀的 base path，不得长期追加到单个文件。database 通过 PostgreSQL logging collector 写入同一日期目录。

日志保留默认按日志族限制为 `512MiB`：服务写入或 Code Queue 导出日志时必须扫描同一 service 后缀的历史文件，超过上限后自动删除最旧切片；当前活跃切片不能被保留清理删除。全局上限由 `UNIDESK_LOG_RETENTION_BYTES` 控制，服务级上限使用 `UNIDESK_<SERVICE>_LOG_MAX_BYTES`（如 `UNIDESK_FRONTEND_LOG_MAX_BYTES`、`UNIDESK_PROVIDER_GATEWAY_LOG_MAX_BYTES`），历史兼容变量只允许作为过渡入口。主 server Compose 服务必须启用 Docker `json-file` 轮转，默认 `UNIDESK_DOCKER_LOG_MAX_SIZE=20m`、`UNIDESK_DOCKER_LOG_MAX_FILE=3`；该配置在服务重建或重建容器后生效。Codex app-server 的 `logs_*.sqlite` 仅作为 Codex 上游运行时的短暂缓冲，Code Queue 必须周期性导出为同样按小时切片的 `codex-app-server` JSONL，并删除/压缩已导出的 SQLite 行，避免 `logs_2.sqlite` 成为长期大文件。

主 server 应安装 `bun scripts/cli.ts gc policy install` 渲染的低风险防膨胀策略：systemd journal 上限 `512MiB`，并启用每日 `unidesk-gc.timer` 执行文件日志与 allowlisted `/tmp` 低风险 GC。该 timer 不主动 vacuum journal，不触碰数据库、Docker image/volume 或 Baidu staging；输出固定写入 `.state/gc/last-run.json` 和 `.state/gc/last-run.stderr`，不得把全量候选 JSON 打进 systemd journal；数据库 trace 留存仍必须由 `gc db-trace` 显式维护，不得加入默认 timer。

OA Event Flow 的高频 trace 统计不得把每个 `trace-stats-updated` 投影事件长期写入 `oa_events`；持久化真相是 `oa_trace_stats` 与 `oa_trace_steps`，SSE/API 发布时可以返回短暂投影通知用于实时 UI 刷新。需要历史回收时只通过 `gc db-trace plan|run` 做显式维护窗口操作，禁止把数据库 VACUUM FULL 或 trace 表大规模删除接入默认 timer。

新增或迁移服务的长期规范：Dockerfile 必须把 `src/components/shared` 复制到与仓库相同的相对路径，TypeScript 配置必须能解析 shared 引用，Compose 必须传入 `LOG_FILE` 和 `UNIDESK_LOG_RETENTION_BYTES`；如果服务需要在内存中暴露 `/logs`，可以继续维护有限 `recentLogs`，但落盘只能通过统一 hourly writer。业务归档日志（例如 Code Queue task output archive）可以保留 append-only 文件，但不得复用 UniDesk service JSONL 命名族，也不得替代 `/logs` 的结构化服务日志。

`bun scripts/cli.ts check` 必须包含日志轮转门禁：核心 Bun 服务源码不得直接向 `LOG_FILE` append，且必须引用统一 hourly writer；Rust backend-core logger must expose equivalent rotation/pruning markers for the same check. 新增服务如果进入主 Compose，也要纳入该门禁的 checked file 列表。

## Log Access

`bun scripts/cli.ts server logs` 同时读取文件日志和 Docker logs 尾部。文件日志是服务崩溃时的第一现场，Docker logs 是容器启动失败和 stdout/stderr 的辅助来源。默认输出必须包含 tail 字节数、是否截断和完整文件路径；扩大读取范围只能通过显式 `--tail-bytes N`，且 CLI 会对单次 tail 设置硬上限。

## Diagnostic Output Limits

所有诊断型 CLI 输出必须优先摘要化、尾部化或分页化，禁止默认倾倒大 JSON、全量日志、全量 trace 或 `.state`/`logs` 宽泛搜索结果。当前硬限额入口包括：`server logs` 默认 3000 bytes tail、`job list` 默认 50 条、`job status` 默认 12000 bytes tail、`codex task/trace/output` 默认分页与文本预览、`microservice proxy` 默认 body 预览且 `--raw` 仍受硬限额保护。确实需要完整响应时必须显式使用对应的 `--full`、`--full-text`、`--tail-bytes` 或 `--limit` 参数，并在验收记录中说明为什么需要扩大输出。

CLI 写 stdout/stderr 遇到下游 pipe 关闭的 `EPIPE` 必须安静退出，不能打印 Bun stack trace。常见验证命令是 `set -o pipefail; bun scripts/cli.ts server status | head -1`，应只看到第一行 JSON 而无额外错误噪声。

## Task Liveness

backend-core 必须把 queued、dispatched、running 视为待处理任务，并通过 `TASK_PENDING_TIMEOUT_MS` 对长时间没有 provider 终态回报的任务做超时处理。超时任务转为 failed，result 中保留 timeout、previousStatus 和 previousResult 摘要，避免 `态势总览` 的待处理数量长期卡住且无法解释。

## Performance Metrics

backend-core 必须提供 `/api/performance`，返回滚动窗口内的 HTTP 组件请求统计、最近失败请求、内部操作统计、最近慢操作、进程内存、PGDATA 用量和 Code Queue PostgreSQL 存储摘要。组件统计必须包含请求数、失败数、失败率、平均延迟和 P95，内部操作统计必须包含服务名、操作名、次数、平均延迟和 P95；失败和慢操作记录必须保留时间、状态、耗时、路径或细节，避免只给汇总数字而无法定位。

frontend Bun server 必须提供同源 `/api/frontend-performance`，记录 webui 静态资源、登录/session、API 代理和 frontend->core 代理操作耗时。浏览器中的 `运行总览 / 性能面板` 必须把 frontend 与 backend-core 指标合并展示为 Bwebui 曲线、组件汇总、最近失败请求、内部操作汇总和最近慢操作；完整性能 JSON 只能通过显式 `查看原始JSON` 打开。

## Node Resource Status

节点 CPU、内存、磁盘和进程资源指标的实时真相来自 provider-gateway 上报的 `system_status`，Docker 资源摘要来自 `docker_status`；backend-core 负责落库、历史采样和 `/api/nodes/system-status` 聚合。排查资源面板不同步时，必须同时对照 provider-gateway 日志里的 `system_status_sent` / `docker_status_sent`、backend-core 日志里的 `provider_system_status` / `provider_docker_status`、以及 backend-core 内部 API，而不能只看前端旧值或浏览器缓存。

provider 上报的 `collectedAt` 在 backend-core 入库前必须解析为 typed timestamp。Rust backend-core 使用 PostgreSQL 参数时不得把 RFC3339 字符串传给 `$n::timestamptz` 这类 SQL cast；`tokio-postgres` 会按目标 PostgreSQL 类型序列化参数，字符串参数可能在序列化阶段失败，导致 provider 已发送但数据库仍停在旧采样。等价实现应先解析为 `DateTime<Utc>` 或目标驱动支持的时间类型，再传入 SQL 参数。

`/api/nodes/system-status` 必须区分当前指标和最后已知指标。超过 backend stale window 的采样不得继续作为 `current` 冒充实时状态；接口和前端应暴露 `stale`、`currentCollectedAt` 和 `lastKnown`，并在指标过期时显示过期状态。运维验证优先使用 backend-core 内部只读入口：`docker exec unidesk-backend-core sh -lc 'backend-core --fetch-json http://127.0.0.1:8080/api/nodes/system-status?limit=5'`。外部 frontend 同源 API 可能受登录 session 保护，不应把未登录请求的 401 当成资源同步失败。

## Low-Memory Diagnostics

主 server 是低资源、低抖动控制面，排查内存时必须先区分共享内存、容器 cgroup 占用和进程私有占用。PostgreSQL 后端进程的 RSS 会重复显示 `shared_buffers` 等共享映射，不能把多个 `postgres` 进程 RSS 简单相加当成真实内存消耗；优先看 `docker stats unidesk-database`、cgroup memory、`/proc/<pid>/smaps_rollup` 的 PSS/USS、`pg_stat_activity` 连接数和 `pg_settings` 中的 `shared_buffers`/`work_mem`。

如果 PostgreSQL 容器总占用和 PSS 并不异常，不应优先通过压缩 `shared_buffers` 解决主 server OOM。更高优先级是识别非核心、交互式和开发型进程，例如 web terminal、长驻 agent session、一次性日志调查或大输出 CLI，把它们迁移到 D601、增加 TTL/硬上限，或通过 `server logs`、`job status`、`microservice proxy` 的默认输出限额减少瞬时内存尖峰。只有在连接池、真实 cgroup 占用和慢查询证据都指向 PostgreSQL 时，才调整 PostgreSQL 内存参数。

性能优化必须先用这些指标锁定慢操作名称、路径、耗时和代理层级，再改后端查询或前后端通信策略；不得只凭主观体感改 UI。Code Queue 这类控制面页面出现 `core_proxy`、`GET /api/microservices/code-queue/proxy/api/tasks/overview`、`POST /api/microservices/code-queue/proxy/api/tasks/<id>/read` 等超过 1s 的慢操作时，应保留优化前后的性能面板证据，并同时记录 live API 耗时、容器内存、`/health` 存储摘要和是否仍通过 PostgreSQL/append-only archive 重建历史数据。短 TTL cache、warmup 或页面内存缓存只能作为重复请求抖动保护，性能证据必须证明数据库索引/聚合、分页和渐进式披露本身已把核心路径降到目标内，不能用长缓存遮蔽慢 SQL 或全量 JSON 物化。

当最近失败请求集中出现 frontend `core_proxy` 502/503/504，路径为 `/api/microservices/code-queue/proxy/...` 的 overview、trace 或 summary，且 k3s/k8s Pod 仍在运行时，必须先运行 `bun scripts/cli.ts microservice diagnostics code-queue`，区分 provider-gateway online、WebSocket HTTP tunnel、k3sctl-adapter、Kubernetes API service proxy 和目标 Service 五段状态。provider tunnel 类失败必须记录响应 body/headers 中的 `requestId`、`stage`、`failureReason`、`x-unidesk-request-id` 和 `x-unidesk-tunnel-error`；如需主动验证错误结构，运行 `bun scripts/cli.ts microservice tunnel-self-test code-queue`，该自测应返回预期失败但 `ok=true` 的诊断结果。随后再继续判断“Kubernetes API service proxy 不可达”“Code Queue 进程不可达”和“Code Queue event loop 被热路径同步工作饿死”。如果 `debug health` 或 provider-gateway egress health 显示 `providerGatewayEgressProxyActiveTunnels` 持续偏高、`pendingTunnels` 非零或 `oldestTunnelAgeMs` 长时间增长，应先按 provider-gateway egress tunnel 生命周期排障，确认 `egress_tcp_open`、connect timeout、idle cleanup 与 core socket close 清理是否生效。排障顺序是同时查看 `/api/frontend-performance`、`/api/performance`、`k3sctl-adapter` `/api/control-plane`、Kubernetes Pod `/live`、`/health`、overview/trace-step curl、`kubectl top pod` 或 Docker stats、容器 `RestartCount`/`OOMKilled` 和 Code Queue 日志；如果 Pod 内 `/health` 也超时，应优先检查实时 output 发布、archive 读取、transcript 构建、统计计算、启动维护、历史 OA backfill 和远程 Provider 准备/SSH 子进程是否阻塞 event loop，而不是先调整 frontend 渲染或代理超时。Code Queue 默认不得在启动时自动执行历史 OA backfill 或通知表索引维护；显式 backfill 必须作为运维动作记录，并在运行期间并发证明 `/live`、`/health` 与 `/api/tasks/overview` 仍快速返回。涉及 D601 等远程 Provider 时，还要检查 `runCodeQueueSsh`/开发容器准备是否仍存在同步子进程、无 timeout 的 SSH、无上限 stdout/stderr 或 stale TUN 重建等待；修复后必须在远程准备探针运行期间并发证明 Pod `/health` 与 `/api/tasks/overview` 仍快速返回。

Code Queue task 明明产出最终回复却反复 `retry_wait` 时，应优先用任务详情里的 latest attempt 字段核查 `terminalStatus`、`transportClosedBeforeTerminal`、`appServerExitCode`、`finalResponseChars`、`judge.raw._safetyOverride` 和 attempt output。OpenCode 远程任务中，`opencode completed status=completed exit=0` 加当前 attempt 非空 assistant 输出应对应 `terminalStatus=completed`、`transportClosedBeforeTerminal=false`；如果因为缺少 `step_finish` 事件仍触发 `_safetyOverride=terminal_not_completed`，说明协议终态归一化有回归。相反，当前 attempt 没有最终 assistant response 时即使 tool/read/bash 证据完整，也必须 retry，不能用旧 `task.finalResponse` 或 reasoning/tool evidence 代替可见最终回复。

### Code Queue Liveness

Code Queue 的“任务是否卡死”不能由单一控制面字段判断。排障必须同时看 PostgreSQL 中的 `running`/`judging` 任务、D601 scheduler 本地 active run/active slot/active queue、scheduler-owned heartbeat、Trace/OA 持久化进度和 OA publisher pending/lastError。master `code-queue-mgr` 的 `postgres-control-plane` 视图只证明数据库行存在；当它显示 `activeRunSlotCount=0` 但 D601 heartbeat 仍新鲜时，正确结论是 control-plane/execution-plane 分裂，diagnostics 应显示 `split-brain` 或 `degraded`，不能宣称任务未执行或卡死。诊断输出中的 `effectiveLiveness=live`、`splitBrainLive=true` 和 `recommendedAction=continue-supervision` 表示这是 heartbeat 新鲜的观测分裂，应继续监督；`effectiveLiveness=at-risk` 或 `recommendedAction=investigate-heartbeat-risk` 表示存在 expired/missing/stale heartbeat 风险，需要优先人工确认。

Trace/OA 长时间没有新 seq 但 scheduler heartbeat 正常时，应归类为 trace gap 或 publisher degraded，不得自动 retry。只有 scheduler 本地没有 active run，且对应 owner heartbeat 已过期时，才允许进入 stale recovery candidate；缺失 heartbeat 只能触发 degraded 诊断和人工确认。任何恢复入口都必须由 scheduler 执行，使用条件更新和审计事件区分 user interrupt、admin stale recovery 与 service restart recovery；禁止直接修改 production PostgreSQL 任务状态来“修复” active run。
