# UniDesk Observability Reference

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

## Capability Boundary

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

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

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

## Zero Implicit Fallback

运行时异常、投影写入异常、序列化异常、ReferenceError/TypeError、不可达上游、provider 不可用、trace 同步失败和状态机非法转移不得被隐式 fallback 吞掉。任何 catch 后继续返回旧值、空值、默认值、legacy 查询、二次来源仲裁或“看起来成功”的行为，都必须改成以下两类之一：

- `hard failure`: 当前用户路径必须失败，并把 trace_id、错误类型、错误消息、阶段和可重试性透传到 CLI/Web/API 诊断面。
- `observed degraded`: 只有业务明确允许降级时才可继续，但必须写入 OTel span event/error、结构化日志、诊断字段和用户可见降级提示；提示必须说明缺失的真实能力，不能把降级输出伪装成正常结果。

禁止用下游 repair、多来源投票、旧缓存补洞、空数组/空对象、默认标题、默认耗时、默认 final response、legacy session store 或浏览器端再计算来掩盖上游异常。尤其是 Workbench/AgentRun/WebProbe 链路中，如果 durable projection、message facts、timing facts、terminal event 或 final response 持久化失败，必须让上游写路径失败并产生 OTel 证据，而不是让前端继续用 fallback 渲染。

每次发现隐式 fallback，都应优先修上游 source of truth：先定位第一个吞错点，删除吞错 fallback，补 OTel/error 透传，再复测原入口。只有确认上游事实已经正确产生后，才允许清理前端展示或 CLI 表格。不得通过增加更多采样器判断、前端兜底字段或 analyzer 自动仲裁来“修好”业务结果。

Web/Workbench trace、Web 哨兵和 `web-probe observe` 的人工判定入口以 `$unidesk-webdev` 为准：先用采样器保存的 artifact 渲染 `turn-summary` 和 `trace-frame` CLI 视图，再解释 analyzer finding。自动判别器、聚合计数或额外截图保存源不能压过同一采样帧的 CLI trace 视图；若二者冲突，应登记 analyzer/tooling 精度问题或上游投影问题，而不是用 fallback 视图修业务结论。

Web 哨兵 dashboard/API 展示问题的第一事实源是 sentinel runner 的 `/api/overview`、`/api/runs`、`/api/runs/{id}`、`/api/findings` 和 `web-probe sentinel dashboard verify|screenshot` 远程浏览器证据。OTel/Tempo 查询不到 `hwlab-web-probe-sentinel` service span 或具体 `sentinel-run-*` id 时，只能说明当前 instrumentation 或保留窗口没有覆盖这条 dashboard/API 路径；不得因此把 UI/API 口径问题判为已追穿，也不得阻塞已由 API/DOM 证据定位的修复。需要继续追 runner 内部链路时，应把缺少 Web 哨兵 span 作为 instrumentation 问题登记到对应治理 issue。

## CLI Logs

异步 job 的 stdout 和 stderr 位于 `.state/jobs/`。`job|jobs list` 默认只返回最新 50 条摘要，并为已知异步工作流返回轻量 `progress.summary`；`job status <id>` 与兼容别名 `jobs get/read <id>` 会返回结构化 `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` 参数，并在验收记录中说明为什么需要扩大输出。

面向人工的“显式字段选择”也不能把常用长集合默认展开成 stdout dump。`gh issue view/read --json comments` 应返回 comment metadata、body 长度/SHA 和短 preview，完整正文通过显式 full/raw 或 `trans gh:` drill-down 读取；node-scoped `hwlab nodes control-plane status --full` 是运行面 workload/bridge/SecretRef 的有界 drill-down，完整 JSON 只属于显式 `--raw`。Secret/API key 输出只能披露对象名、key 名、presence、fingerprint 或 `valuesPrinted=false`。

当一个 UniDesk CLI 子流程调用另一个 UniDesk CLI 并使用 `--raw`、`--full` 或机器消费输出时，必须识别全局 stdout guard 返回的 `outputTruncated=true` / `data.dump.path` 包装，并跟随 dump 文件读取真实 JSON payload；不能把 bounded wrapper 当成业务 payload。若内部调用不能安全跟随 dump，应把命令改成更窄的显式 view、id-specific drill-down 或 compact schema，而不是提高全局 dump 阈值。人工 closeout 仍引用有界摘要、hash、run id 和 drill-down 命令，不把 `/tmp/unidesk-cli-output` 当作长期证据源。

本地或远端 `AGENTS.md`、`CLAUDE.md` 或同类 agent 入口文档超过 `10 KiB`、超过 YAML dump 阈值，或被 CLI/SSH/trans 读取时触发自动 dump，不能只把 dump 文件路径当成继续工作的正常入口。该现象表示入口文档已经过长，必须按 `docs-spec` 把入口文件拆成短索引：只保留 P0 规则摘要、关键命令入口和指向权威文档的链接；具体流程、背景、判定标准和长篇约束迁入对应 skill 的 `SKILL.md` 或 `docs/reference/` 长期参考。拆分后入口文档、skill 和长期参考必须互相交叉引用，避免同一规则在多个位置重复展开或产生第二真相。

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。
