# 磁盘 GC 与留存参考

- 治理入口：
  - UniDesk 磁盘治理统一使用 `bun scripts/cli.ts gc ...`；
  - 所有清理动作必须先生成结构化 plan，再通过显式确认执行；
  - GC 不是通用 `rm -rf` 或原生命令集合；
  - 保护边界内无法达到目标时，应升级为留存或容量决策，不得扩大清理范围。

- 配置真相：
  - 主 server GC 可调策略由 `config/unidesk-cli.yaml#gc` 唯一拥有；
  - YAML 包含默认 include 开关、保留窗口、输出 limit、session/worktree/state root 和扫描预算；
  - CLI 参数只作为一次性显式覆盖；
  - 代码只校验和渲染，不得写入隐藏策略默认值。

## 命令边界

- `gc plan`：只读生成主 server 清理候选、估算收益、风险等级、保护对象和数据库诊断摘要。
- `gc run --confirm`：只执行当前 plan 可见候选页，默认不执行分页隐藏候选；用 `--limit`、`--result-limit`、`--full|--raw` 控制披露和执行范围。
- `gc policy plan|install`：从 `config/unidesk-cli.yaml#gc.policyTimer` 渲染或安装低风险长期策略，例如 journald cap、每日 allowlisted 文件/tmp 清理 timer、VPN 诊断 pcap retention、`.state` artifact retention 和 VS Code CachedExtensionVSIXs 下载缓存 retention。
- `gc db-trace plan|run --confirm --before-date YYYY-MM-DD --vacuum-full`：显式 trace 遥测留存入口；涉及数据库重写时按维护窗口处理。
- `gc remote <providerId> plan|run --confirm|status --job-id <id>`：
  - 通过 UniDesk SSH 透传执行受控远端 GC；
  - 长任务必须使用异步 job 和 `status` 短查询；
  - 不得让单次 SSH 等待完整 registry GC 或其他长清理。
- `gc remote <providerId> plan --memory-pressure-only` / `gc remote <providerId> run --confirm --memory-pressure-only`：
  - 只规划或执行 WebProbe observer/Chrome 内存压力候选；
  - 不得混入通用磁盘 GC 候选；
  - 执行入口必须显式携带 `--confirm`。
- `gc remote <providerId> memory`：
  - 只读读取 `/proc/meminfo` 的 `MemAvailable`；
  - 不执行 cluster preflight、候选扫描或 mutation；
  - 固定披露 `metric=MemAvailable`、`source=/proc/meminfo`、`swapExcluded=true` 和可用字节数。
- `--include-tool-caches`：本机和远端都必须是显式 opt-in，只清理固定 allowlist 的可重建 npm/npx/Bun 缓存；不得扩大成清理 `~/.npm`、`~/.bun`、`node_modules`、auth/config 或运行面依赖。

所有成功和失败输出都必须是 JSON。`plan` 必须标记 `dryRun=true`、`mutation=false`；`run` 必须要求 `--confirm` 并报告 `diskBefore`、`diskAfter`、`summary`、`results` 和 `protected`。远端 GC 可用 `--target-use-percent N` 显式表达目标根盘水位；`summary.target` 必须给出目标所需释放量、候选估算、预计水位、缺口和 `safeStop` 决策，避免靠人工心算判断是否应该继续扩大清理范围。

默认 `/tmp` GC 只包含 allowlisted 诊断目录和已知低风险路径。非 allowlist 的 stale `/tmp` 一级子项必须显式 `--include-stale-tmp` 才能进入候选；扫描按 `--limit` 有界枚举候选，执行时仍只允许删除 `/tmp` 直接子项，并避开 X11/ICE/font socket、systemd private、tmux、ssh、vscode 等系统/session 前缀。该入口不能递归扩大成通用 `/tmp` 清空器，也不能为了估算全量临时目录而长时间阻塞。

主 server VPN 诊断日志默认不清理。`/root/vpn-server/logs` 中由长期 `tcpdump -G` 产生的 `hy2-udp-ring-*.pcap` 和 `hy2-monitor-ring-*.pcap` 可通过显式 `--include-vpn-diagnostic-logs` 进入候选，默认只选择超过 `--vpn-diagnostic-log-keep-hours 24` 的普通 pcap 文件。执行前必须重新校验路径、文件名、非 symlink/regular file，并用 active-file 检查确认没有进程仍打开该文件。`hy2-server-evidence.jsonl`、stdout/stderr log、最新 pcap 和整个日志根目录始终作为 protected 输出，不得被这个入口删除或截断。

主 server `.state` 历史诊断和部署产物默认不进入手动 GC。需要清理时必须显式传入 `--include-state-artifacts`，保留期通过 `--state-artifact-keep-days N` 设置，默认 14 天且必须是正整数。该入口只选择以下对象：

| 范围 | 候选 |
|---|---|
| `.state/e2e` | 超过保留期的普通文件 |
| `.state/validation` | 超过保留期的普通文件 |
| `.state/jobs` | 超过保留期的普通文件 |
| `.state/codex-queue/output-archive` | 超过保留期的普通文件 |
| `.state/deploy/exports` | 超过保留期的直接子目录 |
| `.state/deploy/resolve` | 超过保留期的直接子目录 |

`gc run --confirm --include-state-artifacts` 执行前必须重新校验路径、保留期、对象类型和 symlink 状态。文件候选必须仍是 allowlist 根下的普通文件；deploy 目录候选必须仍是 `.state/deploy/exports` 或 `.state/deploy/resolve` 的直接子目录。该入口不得递归扩大成通用 `.state` 清空器，也不得选择 `.state` 根目录、allowlist 之外的目录、symlink、active worktree、runtime image 或 snapshot 状态。

`.state` scratch 清理是另一类显式入口：`--include-state-stale-scratch` 只读取 `config/unidesk-cli.yaml#gc.stateStaleScratch` 中声明的 fileRoots/dirRoots 和 keepHours。它用于历史临时验证、性能探针、Web observe、CD scratch 等可重建对象；`.state/recovery`、`.state/codex-queue/codex-home`、`.state/deploy/work`、`.state/baidu-netdisk`、Secret/sourceRef 和 runtime snapshot 仍作为 protected 输出，不允许通过这个入口扩大删除范围。

Codex session 清理由 `--include-codex-sessions` 显式启用，只删除 `config/unidesk-cli.yaml#gc.codexSessions.root` 下超过 YAML keepHours 的普通 session 文件。执行前必须重新校验路径仍在 YAML root 下、对象是普通文件、未被进程打开，删除后只向上清理空目录；`auth.json`、config、profile、Secret 或其他 Codex state 永远不通过该入口删除。

Worktree 清理由 `--include-merged-worktrees` 显式启用，只扫描 `config/unidesk-cli.yaml#gc.mergedWorktrees.root` 下的 worktree，主 worktree、当前执行 worktree、recent worktree、未合入 YAML baseRef 或 cherry-pick 等价未吸收的 worktree 都作为 protected 输出。plan 阶段按 YAML scanBudgetMs 有界扫描，超预算对象 protected；run 阶段删除前重新执行 full `git status --untracked-files=all`、inactive 和 merge/cherry-equivalence 校验，并通过 `git worktree remove` 删除，不用手工 `rm -rf`。

主 server VS Code 下载缓存默认不清理。`/root/.vscode-server/data/CachedExtensionVSIXs` 只用于 VS Code extension VSIX 下载缓存，可通过显式 `--include-vscode-cached-vsix` 进入候选；执行时只允许删除该目录下符合 extension-version 命名的顶层普通文件，并按 `--vscode-cached-vsix-keep-days` 保留近期缓存。执行前必须重新校验路径、文件名、非 symlink/regular file，并用 active-file 检查确认没有进程仍打开该文件。该入口不得触碰 `/root/.vscode-server/extensions`、`/root/.vscode-server/cli/servers`、VS Code user data 或任意 session/auth state。

## 受保护数据

默认 GC 不得删除或 prune 以下对象：

| 对象 | 保护原因 |
|---|---|
| PostgreSQL PGDATA | 数据库权威状态，必须走备份、留存或迁移流程 |
| Docker image/container/volume | 运行面和发布真相可能依赖旧镜像或 volume |
| `.state/recovery` | 恢复状态和人工回滚线索，不属于 artifact retention |
| `.state/codex-queue/codex-home` | Codex sessions/auth/profile 状态，不得作为队列输出归档清理 |
| `.state/deploy/work` | 部署工作目录可能包含 active rollout 上下文 |
| `.state/baidu-netdisk` | Baidu Netdisk token、任务、备份和 staging 状态需单独判定 |
| active worktree、runtime image、runtime snapshot state | 当前执行面和运行面 provenance，不通过 `.state` artifact retention 删除 |
| Codex auth/config | `~/.codex/auth.json`、profile 和 config 等凭证状态；session 文件只能通过显式 `--include-codex-sessions` 按 YAML retention 清理 |
| VPN diagnostic evidence logs | `/root/vpn-server/logs/hy2-server-evidence.jsonl` 等 active evidence 流用于网络排障，不随 pcap retention 删除 |
| VS Code installed extensions/server/user data | 已安装扩展、server 版本和用户配置不是下载缓存，只能由专门 stale-version 规则或 VS Code 自身管理 |
| D601 registry storage | artifact registry retention 需使用专门入口 |
| `/var/lib/rancher/k3s` 与 `/var/lib/rancher/k3s/storage` | k3s 控制面、containerd 状态和 local-path PVC 数据 |
| `/var/lib/kubelet`、`/var/lib/containerd` | kubelet/runtime 状态和可能被 workload 复用的 image cache |
| HWLAB 固定 workspaces | `/root/hwlab`、`/root/hwlab-v02`、`/root/agentrun` 是 source/runtime 约束的一部分 |
| Kubernetes workload、Secret、PVC、PV、Argo、Tekton 对象 | 必须通过对应运行面 retention 子命令或 GitOps/CI 控制入口处理 |

如果需要触碰上表对象，必须先补高层 UniDesk CLI 子命令、dry-run 计划、保护对象、验证命令和失败分类；不能把原生 `kubectl`、`docker prune`、`crictl rmi` 或手写 registry shell 作为长期流程。

`gc policy install` 的每日 timer 按 `config/unidesk-cli.yaml#gc.policyTimer` 启用 `.state` artifact retention、VPN pcap retention 和 VS Code CachedExtensionVSIXs retention，用来限制历史诊断/部署产物、tcpdump ring 文件与 VS Code 下载缓存长期增长；手动 `gc plan/run` 仍默认不清 `.state` 或 VSIX 缓存，必须显式 `--include-state-artifacts` / `--include-vscode-cached-vsix` 才会列出或执行这些候选。policy timer 仍保护上表对象，并把输出限制在 `.state/gc/last-run.json` 和 `.state/gc/last-run.stderr`。

## G14 远端策略

`gc remote G14 ...` 必须先确认目标是 G14 原生 k3s 节点，且 preflight 中节点名包含 `ubuntu-rog-zephyrus-g14-ga401iv-ga401iv`。G14 默认候选只允许：

- systemd journal vacuum 到目标上限。
- Docker `json-file` 日志截断。
- Docker BuildKit cache prune，默认只清理超过 `--build-cache-until` 的 cache。
- apt archive clean。
- allowlisted `/tmp` 诊断目录删除。
- 受限 core dump 删除。

受限 core dump 只匹配 `/root/unidesk/core.<pid>` 普通文件。执行前必须重新校验路径 allowlist、Git 未跟踪、非 symlink、无 `fuser` 活跃引用。估算收益必须按实际分配块数计算，并可另行披露 `apparentSizeBytes`；不能把 sparse core dump 的表观大小当成可回收磁盘空间。

## PK01 远端策略

PK01 是腾讯云 Docker provider，不是 G14 k3s/registry 节点；长期运维边界见 `docs/reference/pk01.md`。`gc remote PK01 ...` 可用于通用低风险候选（allowlisted `/tmp`、Docker json-file 日志、BuildKit cache、apt cache、受限 core dump 和 journald 计划），但 pikanode 的主要增长源由 PK01 节点本地 retention 机制管理，而不是 G14 registry/PVC retention。

PK01 pikanode temp retention 只允许清理 `/home/ubuntu/pikanode/html/temp` 下超过保留窗口的直接子目录，并必须保护 `html/download/`、`html/upload/`、`files/`、证书、Git state、直接日志文件和近期 temp workspace。该策略已固化为 PK01 节点本地 systemd timer 与 logrotate；人工排障时优先查看 `systemctl status unidesk-pk01-pikanode-temp-gc.timer` 和 `/var/log/unidesk-pk01/pikanode-temp-gc.log`。如果 PK01 高水位仍无法通过 temp retention 和通用低风险 GC 降下来，必须停止并进入 pikanode 下载产物留存、Docker image retention 或容量扩容决策，不能把 `download/`、`files/` 或 Docker overlay 当作普通临时目录删除。

## JD01 远端策略

JD01 是 YAML-first k3s provider，承载 AgentRun、HWLAB v0.3、Web probe sentinel 和相关 PVC/state artifact。`gc remote JD01 ...` 的目标节点、lane、namespace 集合、state root、保护路径、扫描预算、artifact cap、retention 窗口、observer stop 策略和输出 limit 必须来自 `config/unidesk-cli.yaml#gc.remote.targets.JD01` 或等价 source of truth；CLI 参数只作为一次性覆盖，不得把 JD01 namespace、路径或阈值写成脚本隐藏默认。

JD01 远端 plan 必须适配短连接：`snapshot` 和轻量 `plan` 返回有界 JSON；涉及 k3s/PVC 实占、state root 深扫、history/trend 或大 protected path size 的长任务必须创建异步 job，并通过 `gc remote JD01 status --job-id <id>` 渐进查询。protected path size 采集必须是 budgeted/progressive：每个 protected item 披露 `sizeState`、耗时、超时或失败原因；`du` 超时后不得回退为无界递归扫描，也不得让保护对象尺寸统计阻塞 plan 返回。

JD01 PVC 归因必须按 YAML 配置的 namespace 集合读取 k8s API，不得复用 G14 专属 namespace 硬编码。报告至少包含 namespace、PVC、PV、host path、requested size、estimated actual bytes、active mount pods、owner/session/PipelineRun/runId、phase 和 reclaim policy。默认只做 plan 和归因；删除 PVC/PV、local-path host path、k3s storage、containerd snapshot/blob 或 workload 对象必须通过对应高层 retention 子命令和 GitOps/运行面 owner 判定，不能由 remote GC 扩大成 raw `kubectl delete` 或 host path 删除。

JD01/AgentRun 这类 PVC retention 确认入口必须适配短连接：确认步骤只提交经过 plan 选中的 Kubernetes 删除请求并快速返回，不能等待 local-path PV 后端同步回收完成；收敛状态通过下一次 dry-run、`gc remote JD01 status` 或专用 status 子命令查询。若一次提交在 transport 窗口内仍不稳定，应降低 YAML/CLI 批量，而不是改成手工 raw kubectl 或 host path 删除。

JD01 local-path storage 中没有 PV 引用的 orphan 目录只能通过 `gc remote JD01 plan|run --include-local-path-orphans` 进入候选。该入口必须从 YAML 读取 storage root、目录前缀 allowlist 和年龄策略，只允许删除 root 的直接子目录，且执行前重新确认无 PV 引用、无 symlink、无打开 fd/cwd；不得把它扩大成通用 `/var/lib/rancher/k3s/storage` 清空或 raw host path 删除。

JD01 host containerd 只能通过 `gc remote JD01 plan|run --include-host-containerd-cache` 进入候选。该入口必须从 YAML 读取 containerd root、socket address 和 namespace allowlist；只有 host containerd 目标 namespace 中没有 task/container 时才允许执行 `ctr images prune --all`，不得直接删除 `/var/lib/containerd` 下的 content、snapshot 或 metadata 路径。

当 host containerd 的 `ctr` 元数据中 images、containers、tasks、leases、snapshots 和 content 全为空，但 YAML allowlist 下仍残留 overlay snapshot 目录或 content blob 文件时，才能把它们分类为 orphan state。orphan state 清理仍必须通过 `--include-host-containerd-cache` 的 plan/run，执行前重新检查元数据为空、路径在 YAML root 下、名称匹配受控形态、无 symlink、无打开 fd/cwd；不得删除 metadata DB 或扩大到 containerd root。

JD01 增长降速 policy 由 `gc remote JD01 policy plan|install|status` 管理。policy install 会把原生 Python runner 和 JSON config 写入 YAML 指定的目标路径，再由 systemd timer 周期触发；runner 不读取 host worktree，也不依赖临时 CLI 输出解析。启用阶段必须逐项来自 `config/unidesk-cli.yaml#gc.remote.targets.JD01.policyTimer`：低风险 journal/apt/tmp、可选 tool cache、Web observe stale artifact、AgentRun session PVC、k3s CRI image prune、host containerd orphan state 和 local-path orphan。每个中风险阶段仍使用各自的 owner-aware 保护条件，失败时记录到 policy state，不得扩大成 raw `rm -rf`、raw kubectl 或 containerd metadata 删除。

JD01 Web observe artifact 是一等 GC 对象。state root 必须来自 YAML；候选按 run 聚合并读取 `manifest.json`、`heartbeat.json`、`pid`、report sha 和 top files。年龄判定以 manifest/heartbeat 的 started/completed/updated 字段、pid 存活和打开 fd 检查为准，不以目录 mtime 为唯一依据，因为手动 GC 或目录遍历可能刷新 mtime。active run、pid alive、open fd、未生成必要 report 的 run 均为 protected。safe 候选只覆盖超过 YAML retention 且可重建的 raw samples、browser-process、network/trace、screenshot 等大 artifact；长期保留 report summary、report json/md、最终截图或诊断摘要由 YAML cap/retention 策略控制。

### Web observe 与 Chrome 逃逸进程

- 启动资格真相源：
  - 唯一配置位于 `config/hwlab-node-lanes.yaml#templates.hwlabV03.webProbeWorkbench.resourcePolicy.memoryStartGuard`；
  - 当前阈值合同为 `4 GiB`，即 `4294967296` 字节；
  - 代码只负责读取、比较和结构化披露，不得另设隐藏阈值。
- 唯一内存指标：
  - 只使用 `/proc/meminfo` 的 `MemAvailable`；
  - 输出必须披露 `metric=MemAvailable`、`source=/proc/meminfo` 和 `swapExcluded=true`；
  - `SwapFree`、`MemFree + swap`、cgroup 虚拟空间或其他 virtual memory 指标不得合并进可用量；
  - swap 只能作为独立压力信号，不能改变启动资格。
- 人工 `web-probe observe start`：
  - `MemAvailable <= 4 GiB` 时返回结构化 blocked；
  - 不创建 observer，也不启动 Chromium；
  - blocked 后只能按以下 scoped 流程人工处置。

```bash
bun scripts/cli.ts gc remote <node> plan --memory-pressure-only
bun scripts/cli.ts gc remote <node> run --confirm --memory-pressure-only
bun scripts/cli.ts gc remote <node> status --job-id <id>
bun scripts/cli.ts gc remote <node> memory
```

- 人工处置顺序：
  - 先复核 `plan --memory-pressure-only` 的候选、保护对象和预计回收量；
  - 只执行参数完全匹配的 `run --confirm --memory-pressure-only`；
  - `run` 返回 job id 后，重复执行 `status --job-id <id>` 直到明确终态；
  - job 未终态时，禁止提前执行 `memory` 或重试 WebProbe；
  - job 终态后执行 `memory`，以新的 `/proc/meminfo` 快照重新判定；
  - `MemAvailable` 仍不大于 `4 GiB` 时，下一次人工 start 继续 blocked；
  - 不得用未带 `--memory-pressure-only` 的通用 plan/run 代替本流程。
- sentinel cadence：
  - 每轮在 provider、凭据和 observer 启动前读取 `MemAvailable`；
  - `MemAvailable < 4 GiB` 时返回 `skipped-wait-next-round`；
  - 不创建 observer、不启动 Chromium、不自动执行 GC；
  - 等待下一轮 cadence 重新判定；
  - 恰好等于 `4 GiB` 时，按 sentinel 的 `<` 比较器允许继续。
- 运行时上限：
  - TTL、sample、screenshot、PerformanceObserver buffer、browser process history、freeze signal history 和 response body 上限均来自 owning YAML；
  - terminal、error 和 signal 路径必须显式关闭 observer page、control page、browser context 和 browser；
  - cap 只能收敛 raw 证据增长，不得删除 report、manifest、heartbeat 或必要摘要。
- 正常生命周期：
  - Chrome 内存治理优先管理 observer runner 生命周期；
  - sentinel 和 quick-verify 的成功、blocked、失败、timeout、异常终态都必须执行受控 stop；
  - stop 流程由 YAML 控制，并使用 `web-probe observe stop`/force stop；
  - stop 后必须验证 runner 与 Chrome process tree 已退出；
  - runner 必须从 scenario/YAML 获得最大运行时长或 max samples 兜底；
  - browser freeze policy 只能用于异常保护，不能替代正常终态 stop。
- 独立无归属 Chrome 判定：
  - 存活窗口必须来自 owning YAML；
  - 必须证明进程树不属于 active observer 或其他在途受控任务；
  - 归属证据包括 observe/sentinel 状态、manifest、heartbeat、PID 记录和完整 process tree；
  - 候选快照必须记录 root 与后代的 `PID`、`PPID`、`SID` 和 `/proc/<pid>/stat` `startTicks`；
  - `startTicks` 用于排除 PID 复用；
  - 任一读取失败、扫描截断、身份不闭合或归属歧义都必须保持 protected；
  - stale observer 进程树标记为 `stale-web-observer-process-tree`；
  - 证据闭合的独立无归属浏览器树标记为 `browser-orphan-process-tree`。
- 精确 TERM→KILL：
  - TERM 前重新核对候选的 `PID/PPID/SID/startTicks`；
  - 只向身份完全匹配的候选发送 TERM；
  - 按 YAML 有界等待后重新枚举原候选；
  - TERM 后残留复核只使用不可变的 `PID/startTicks`；
  - 重挂载场景同时披露原始与当前 `PPID/SID`，不得把可变化的归属字段继续当身份匹配条件；
  - 只对复核后仍属于原身份的残留进程发送 KILL；
  - 成功条件是原始 `PID/startTicks` 身份全部消失，而不是只看进程名或原 PPID；
  - 禁止使用 `pkill -f chrome`、`killall chrome` 或任何按名称批量终止入口。
- 收口验证：
  - 原 root 与后代的 `PID/startTicks` 身份必须全部消失；
  - active observer 必须未受影响；
  - 重新采集 `/proc/meminfo` 的 `MemAvailable`、独立 swap 信号、Chrome/observer 摘要和 web-observe 状态；
  - 结构化记录归属证据、TERM/KILL 动作、PPID 变化、残留身份和终止前后资源数据。

JD01 plan 和 status 应同时披露内存压力摘要：active observer 数、Chrome process 数、Chrome RSS、stale observer 数、state root artifact bytes、last cleanup、last stop failure 和 drill-down 命令。GC run 只能执行 plan 中明确标记 safe 的低风险动作，例如 apt/journal/tmp allowlist、dead web-observe raw artifact retention，以及通过受控 observe stop 处理 stale observer；对 PVC、k3s runtime、containerd、Docker volume、Secret 和 auth/config 状态一律保持 protected 或转交专用 retention 入口。

JD01 的 npm/npx/Bun 缓存可作为中低风险边界候选，但必须通过 `gc remote JD01 plan|run --include-tool-caches` 显式启用。该入口只允许固定 allowlist 的可重建缓存目录，并在执行前重新校验路径、非 symlink 和 allowlist；它不清理 `node_modules`、用户配置、认证状态、k3s/containerd、PVC、Docker image/container/volume 或 GitOps 运行面对象。

Web probe sentinel 的 `quick-verify`、Playwright 或浏览器验证不得作为 CI/CD 发布 gate 自动周期触发。若某个 sentinel 需要周期 quick-verify，必须在 YAML 中显式 opt-in cadence scheduler；默认发布验证只看配置的 health endpoint，quick-verify 只作为人工或独立 post-deploy evidence 入口。这样避免 CI/CD 周期性重启 observer/Chrome 并重新制造内存和 artifact 增长。

## HWLAB 镜像仓库留存

G14 HWLAB registry 清理必须显式使用 `--include-hwlab-registry`，默认 `gc remote G14 plan` 不进入 registry。策略必须保守，不能只留 latest，也不能只删除 tag link 后误判已经释放空间。

默认保留规则：

| 保留项 | 规则 |
|---|---|
| 当前 workload 引用 | 保留所有当前 k3s workload 使用的 tag ref 和 digest ref |
| digest closure | 从当前 workload、保留 tag、protected/base/非业务 repo 出发，保留 manifest config/layer/manifest-list 的 digest 闭包 |
| 近期 tag | 保留 `--registry-min-age-hours` 内全部 tag，默认 48 小时，可显式设为 0 |
| 每 repo 最新 tag | 每个业务 repo 至少保留 `--registry-keep-per-repo` 个最新 tag，默认 20，最小 1 |
| cache/base/protected tag | 保留 cache repo、`latest`、基础镜像 tag 和显式 protected tag |
| 非业务 repo | 默认不删除非 `hwlab/hwlab-*` 的 commit-like tag |

删除范围包括两类：不在保留集内的 commit-like tag，以及 `hwlab/hwlab-*` / `hwlab/cache/hwlab-*` repo 内不在保留 digest closure 的 stale `_manifests/revisions/sha256/<digest>`。执行时先在 registry 在线状态下通过 API 删除 manifest，再缩容 registry pod，删除遗留 tag/revision 目录，运行官方 `registry garbage-collect`，最后恢复 registry。`--registry-gc-only` 只用于中断恢复或人工维护窗口收尾：它不删除任何 tag 或 revision，只运行官方 GC。

BuildKit cache repo 的历史 `latest` 写入会留下大量 untagged manifest revision；只删除 tag 通常只能释放少量 manifest blob，无法释放旧 cache layer。需要大幅降低 `/var/lib/hwlab/registry` 时，必须以 plan 输出的 `deleteRevisions`、`protectedDigestClosure` 和 official GC 后的 `diskAfterBytes` 判断是否真正生效。

Registry 执行必须以远端异步 job 完成，并具备以下维护保护：

- 先记录并暂停 G14/v0.2 branch poller CronJob；目标集群中某条 lane 暂无 CronJob 时记录为 absent，不视为失败。
- 暂停 poller 后再等待 hwlab-ci PipelineRun、TaskRun 和 Job 空闲；不能在暂停前因为 active CI 直接拒绝，否则 poller 竞态会导致 maintenance 反复失败。
- 通过 registry API 删除 manifest 时 registry 必须仍在线。
- registry 下线后只能删除通过 plan 判定的 tag/revision 目录；不得直接删除 blob 目录。
- 缩容 registry 后运行官方 `registry:2.8.3` garbage-collect pod。
- finally 阶段删除 GC pod、恢复 registry replicas、等待 rollout、恢复 CronJob suspend 状态。
- 状态查询使用 `gc remote G14 status --job-id <id>`，不使用长 SSH 会话等待。
- `gc remote` 的 stdout 是有界 JSON：当 `--full` 或大量候选导致结果过大时，完整结果会写入远端 `/tmp/unidesk-gc-remote/jobs/<job>.json`，stdout 只返回摘要、`jobId`、`statePath` 和 `statusCommand`，再用 `gc remote G14 status --job-id <job>` 渐进查询，避免输出爆炸被误判为 JSON 失败。

## G14 CI 工作区留存

G14 的 Tekton workspace retention 不能通过原生 `kubectl delete`、直接删除 `/var/lib/rancher/k3s/storage` 或手工清 PV host path 完成。所有完成态 PipelineRun workspace 清理都必须走对应高层 UniDesk CLI，先 dry-run 输出候选和保护对象，再 confirm 执行。

AgentRun `v0.1` 使用 `agentrun-ci` namespace：

```bash
bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run
bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --confirm
bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --dry-run
bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --confirm
```

HWLAB 使用 `hwlab-ci` namespace：

```bash
bun scripts/cli.ts hwlab g14 control-plane cleanup-runs --lane v02 --min-age-minutes 30 --limit 200 --dry-run
bun scripts/cli.ts hwlab g14 control-plane cleanup-runs --lane v02 --min-age-minutes 30 --limit 200 --confirm
bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit 200 --dry-run
bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit 200 --confirm
```

Retention 入口的长期合同：

- 默认只选择 `Succeeded`/`Failed` 的完成态 PipelineRun，且必须达到 `--min-age-minutes`。
- 默认保护每条 lane 或前缀下最新完成的 PipelineRun，保留当前 CI/CD 状态证据；只有显式定点目标才允许清理最新证据。
- dry-run 必须披露候选 PipelineRun、owned PVC、active mount 保护、预估可回收空间或可回收对象数，以及 confirm 命令。
- confirm 首先删除 PipelineRun，让 Tekton ownerRef 释放临时 PVC；随后用 `cleanup-released-pvs` 处理 local-path 未自动删除的 `Released` PV。
- `cleanup-released-pvs` 只能选择 `local-path`、`Delete` reclaim policy、目标 CI namespace 的 Released PV；不得触碰业务 namespace、runtime PVC、Secret、registry storage 或 GitOps desired state。

CI workspace retention 是 registry retention 之前的低风险步骤。若它清不出足够空间，不能扩大到 raw PV/path 删除；应继续进入 registry retention plan，或进入 safe-stop 决策。

## 安全停止线

磁盘目标可以设为 `<70%` 或维护窗口临时目标如 `<50%`，但安全边界优先级高于目标百分比。目标必须直接传给 CLI，使输出带有可机读缺口：

```bash
bun scripts/cli.ts gc remote G14 plan --target-use-percent 50 --limit 20
bun scripts/cli.ts gc remote G14 plan --target-use-percent 50 --include-hwlab-registry --limit 20
```

当 `summary.target.safeStop=true`、`state` 为 `shortfall` 或 `safe-stop-no-meaningful-candidates` 时，应停止自动清理并提交决策表。若默认 plan 和 registry plan 都无法覆盖目标缺口，剩余空间通常位于 registry 保留集、k3s runtime、containerd image cache 或 PVC 数据中。继续下降需要显式选择以下更高风险方案之一：

| 方案 | 风险 | 需要的前置决策 |
|---|---|---|
| 降低 registry 每 repo 保留数或缩短近期 tag 窗口 | 可能影响切旧 tag 回滚 | 明确旧 tag 回滚窗口和服务 owner 接受标准 |
| containerd/k3s image cache prune | 可能触发 workload 重新拉镜像或旧镜像缺失 | registry 健康、镜像可拉取性和维护窗口 |
| PVC/runtime 数据 retention | 可能删除业务状态或 CI 证据 | 业务 owner 确认数据类别和留存期限 |
| rsyslog 文件日志压缩/截断 | 可能丢失一线故障证据 | 明确日志留存窗口，先补受控 CLI/logrotate 策略 |
| source worktree/cache TTL | 可能删除并行任务上下文或本地依赖缓存 | worktree owner、分支状态、dirty 状态和可重建性判定 |
| 扩容磁盘 | 低运行风险 | 节点容量预算和停机/在线扩容方式 |

G14 进入 50% 这类维护窗口目标时，标准顺序是：

1. `gc remote G14 plan --target-use-percent <N>` 取得默认低风险候选和缺口。
2. 运行 AgentRun/HWLAB CI workspace retention，并二次处理 Released PV。
3. 使用 `--include-hwlab-registry` 规划 registry retention；默认保守保留，维护窗口内可显式把 `--registry-keep-per-repo` 降到 1，但必须保留当前 workload refs 和 digest closure。
4. 再次运行 `gc remote G14 plan --target-use-percent <N> --include-hwlab-registry ...`。
5. 若只剩 KiB 级低风险候选且 `safeStop=true`，停止自动清理，不得为了命中百分比目标手工删除受保护目录。

维护窗口目标接近整数边界时，验收必须同时报告 `df -h /` 和 `df -B1 /`。`summary.target` 使用字节计算，`df` 使用整数展示，二者在 49/50 这类边界可能看起来不一致；收口时以最终 `diskAfter.usePercent`、`df -h` 展示和字节视图共同说明，不得因为四舍五入差异绕过 protected boundary。

## G14 空间归因基线

G14 当前只有一个本机 k3s cluster；空间归因时不要把 `hwlab-dev`、`hwlab-prod`、`hwlab-v02` 理解成独立 cluster，它们是同一 k3s 上的 namespace/runtime slice。长期诊断应按三层归因：

| 层级 | 归因方式 | 判读口径 |
|---|---|---|
| Host path | `du -x` 按 `/var/lib/*`、`/root`、`/usr` 等目录统计 | 判断根盘主要压力源 |
| k3s namespace/PVC | `kubectl get pv,pvc,pod -A -o json` 结合 local-path PV host path | 把可归属数据映射到 namespace/workload |
| Registry repo/tag/revision | registry v2 repository/tag link、manifest revision 与 blob manifest 解析 | 判断镜像历史 tag、cache revision 与共享 layer 的贡献 |

G14 高水位的长期基线分布如下，后续诊断出现同类量级时优先按同一顺序处理。registry 和 CI workspace retention 后，空间压力通常从 `/var/lib/hwlab/registry` 转移到 k3s containerd snapshot/content、local-path PVC、host containerd、rsyslog 文件日志和 source worktree/cache；这些都属于受保护或需策略化处理的运行面，不能为了达到百分比目标直接删除目录。

| 类别 | 路径 | 典型量级 | 归因说明 |
|---|---|---:|---|
| HWLAB local registry | `/var/lib/hwlab/registry` | 高水位约 60GiB；强 retention 后约十几 GiB | 最大头；按 repo/tag/revision retention 管理，不能直接删 blob |
| k3s runtime | `/var/lib/rancher/k3s` | 高水位约 45-55GiB；清理后约 28-35GiB | embedded containerd、local-path PVC 和 k3s server/db |
| k3s containerd snapshots | `/var/lib/rancher/k3s/agent/containerd/.../snapshots` | 高水位约 30-40GiB；清理后约十几 GiB | workload image layer/snapshot cache，共享占用，不能直接按单 pod 删除 |
| k3s containerd blobs | `/var/lib/rancher/k3s/agent/containerd/.../blobs` | 高水位约 8-12GiB；清理后约数 GiB | k3s image content store，共享占用 |
| k3s local-path PVC | `/var/lib/rancher/k3s/storage` | 高水位约 8-10GiB；CI retention 后约 6-8GiB | 可按 namespace/PVC 归因，只能通过运行面 retention 入口清理 |
| host containerd | `/var/lib/containerd` | 约 9-12GiB | k3s 外的 host/containerd cache，默认不由 remote GC prune |
| root workspaces/cache | `/root` | 约 2-8GiB | HWLAB/v0.2 worktree、npm/bun/cache 等；worktree TTL 需 owner/dirty 判定 |
| logs | `/var/log` | 约 1-3GiB | journald 可由 GC cap，rsyslog 文件需受控 logrotate/压缩策略，pod logs 通常较小 |

PVC 的长期判读口径：

| Namespace | 典型占用 | 主要来源 |
|---|---:|---|
| `hwlab-ci` | 约 2GiB | 完成态 Tekton TaskRun workspace PVC；单个 PVC 通常约 0.24GiB |
| `hwlab-dev` | 数百 MiB | `hwlab-code-agent-workspace` 与 dev Postgres |
| `hwlab-prod` | 数百 MiB | `hwlab-code-agent-workspace` |
| `hwlab-v02` | 数百 MiB | `hwlab-code-agent-workspace` 与 v0.2 Postgres |

Registry 的长期判读口径：

| Repo 类型 | 典型形态 | 说明 |
|---|---|---|
| `hwlab/hwlab-cloud-api` | tag 数可超过 20，unique blob 数 GiB | 最大业务 repo，优先观察 tag 增长 |
| `hwlab/hwlab-agent-*`、`hwlab/hwlab-cloud-web`、`hwlab/hwlab-gateway*` | 每 repo 保留约 20 tag 时通常各占数 GiB | 多服务并行构建导致 registry 线性增长 |
| `hwlab/cache/*` | tag 少但共享 layer 多 | 不能按 expanded size 简单相加 |
| base/protected image | tag 少，占用低到中等 | 保留用于构建和运行回滚 |

Registry 报告必须区分 `uniqueBlobBytes` 和 `sharedBlobBytes`。多个 repo/tag 复用同一 layer 时，expanded tag size 会重复计算；判断可回收空间应以 official registry GC 后的根目录变化为准。

## 诊断命令

G14 空间审计默认只读。需要报告时优先采集以下摘要，避免全量 dump 大 JSON：

```bash
trans G14 sh -- 'df -h / | tail -1'
trans G14 sh -- 'du -xh -d 1 / /var /var/lib /root 2>/dev/null | sort -h | tail -40'
trans G14 sh -- 'du -xh -d 2 /var/lib/rancher/k3s /var/lib/containerd /var/log 2>/dev/null | sort -h | tail -80'
trans G14:k3s kubectl get pv,pvc,pod -A -o wide
trans G14 sh -- 'find /var/lib/hwlab/registry/docker/registry/v2/repositories -path "*/_manifests/tags/*/current/link" -type f | wc -l'
```

需要深挖 registry 时，报告字段至少包括 repo、tag count、manifest revision count、latest tags、protected digest closure、unique blob bytes 和 shared blob bytes。需要深挖 k3s runtime 时，报告字段至少包括 namespace/PVC、PV host path、owner workload、PVC 实占、k3s containerd snapshots/blobs 总量。不要把 `/var/lib/kubelet/pods` 与 `/var/lib/rancher/k3s/storage` 简单相加，因为 kubelet pod 目录可能包含 PVC bind mount 或 runtime 元数据，存在重复计数风险。

需要深挖日志和 worktree 时，默认只读报告，不直接清理：

```bash
trans G14 sh -- 'du -xh -d 1 /var/log 2>/dev/null | sort -h | tail -40'
trans G14 sh -- 'du -xh -d 2 /root/hwlab-v02/.worktree 2>/dev/null | sort -h | tail -60'
```

rsyslog 文件日志不属于当前 `gc remote` 默认可变更对象。若 `/var/log/syslog*`、`/var/log/kern.log*` 或同类文件成为 50% 目标的最后缺口，应先新增受控 logrotate/压缩/截断 CLI，并在输出中披露保留 tail、压缩对象、释放估算和失败恢复；禁止直接 `truncate` 或删除日志文件作为长期流程。`/root/hwlab-v02/.worktree` 只能在明确 owner、branch、dirty 状态和可重建性后清理，不能按目录大小直接删除。

JD01 空间和 Chrome 压力审计同样默认只读。需要深挖时，优先通过 `gc remote JD01 snapshot|plan|status` 暴露有界摘要；必要的远端探测只用于补 CLI 证据，不作为长期手工流程。报告字段至少包括 root 水位、inode、水位目标缺口、YAML 配置引用、k8s PVC namespace 归因、web-observe run artifact topN、observer/Chrome process tree 摘要、protected sizeState 和 safe/blocked 分类。Web observe run 的 stale 判定必须说明 manifest/heartbeat/pid/open-fd 依据；不能把目录 mtime 当作唯一证据。

## 验证清单

G14 GC 后必须验证：

```bash
trans G14 sh -- 'df -h / | tail -1'
trans G14 sh -- 'curl -fsS http://127.0.0.1:5000/v2/ >/dev/null && echo ok'
trans G14:k3s kubectl -n hwlab-ci get deploy hwlab-registry
trans G14:k3s sh -- 'kubectl -n hwlab-ci get cronjob hwlab-g14-branch-poller -o custom-columns=NAME:.metadata.name,SUSPEND:.spec.suspend --no-headers && ! kubectl -n hwlab-ci get cronjob hwlab-v02-branch-poller >/dev/null 2>&1'
```

DEV workload 验证应检查非零副本 workload 是否 ready；`0/0` 的显式停用 deployment 不应误报为事故。registry tag 数只作为辅证，不能替代 workload ref 保护和 registry API 健康。

同时必须用高层 CLI 验证受影响运行面仍对齐：

```bash
bun scripts/cli.ts agentrun control-plane status
bun scripts/cli.ts hwlab g14 control-plane status --lane v02
bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run
bun scripts/cli.ts hwlab g14 control-plane cleanup-released-pvs --lane all --limit 200 --dry-run
```

验收结论至少包含：根盘水位、`DiskPressure=False`、registry readiness/API、AgentRun `aligned=true`、HWLAB v0.2 `state=aligned`、active PipelineRun 数，以及 remaining low-risk candidates / safe-stop 决策。若目标百分比未达到但 `safeStop=true`，必须写清剩余缺口和下一类策略选择，不得把未执行的高风险清理伪装成已完成。

JD01 GC 或 Chrome/observer 压力收口还必须补充：

```bash
trans JD01 sh -- 'df -h /; df -B1 /; free -h'
trans JD01 sh -- 'ps -eo pid,rss,comm,args | awk '\''BEGIN{IGNORECASE=1} /chrome|chromium|web-probe observe/ && !/awk/ {print}'\'' || true'
bun scripts/cli.ts web-probe sentinel control-plane status --node JD01 --lane v03 --sentinel <id>
```

若本次变更涉及关闭 cadence scheduler，还应确认 GitOps objects 不再包含 quick-verify CronJob，并确认 runtime/Argo/health endpoint 仍为 ready。

## 长期防膨胀计划

| 措施 | 默认策略 | 预期收益 |
|---|---|---|
| Registry conservative retention | workload refs + 48h tag + 每 repo 20 tag + official GC | 高构建频率下通常是主要收益，可释放十几到几十 GiB |
| CI tag growth cap | 每个 service 对 commit tag 增长设置上限并进入 GC plan | 防止 registry 每周持续膨胀 |
| Tekton/Pipeline retention | 完成态 PipelineRun/TaskRun/Job 按留存期清理 | 释放少量空间，降低 API 噪声 |
| Log/journal cap | journald 上限、文件日志轮转、Docker json log truncate | 稳定防止日志膨胀，收益通常为数百 MiB 到数 GiB |
| Remote tool cache opt-in | `gc remote ... --include-tool-caches` 只清理固定 allowlist 的 npm/npx/Bun 可重建缓存 | 在不触碰运行面状态的前提下释放数百 MiB 级边界空间 |
| VS Code CachedExtensionVSIXs retention | 只清理可重建 extension VSIX 下载缓存，不触碰已安装扩展/server/user data | 防止远程 VS Code 扩展升级缓存长期堆积，收益通常为数百 MiB |
| Core dump limits | 限制 dump 大小或按 allowlist 删除实际分配块 | 防止 crash dump 污染观测；sparse dump 不应被高估 |
| Containerd image audit | 定期只读报告 runtime image cache 构成 | 为维护窗口 prune 提供证据，不默认删除 |
| Worktree TTL audit | 报告 `.worktree` owner、branch、dirty 和 node_modules/cache 占用 | 为安全清理并行任务 scratch 提供证据 |
| Web observe artifact caps | 从 YAML 控制 samples/browser-process/network/screenshot raw artifact cap、summary+tail 保留和 dead run retention | 防止 Web sentinel 长期巡检把 JSONL 与截图产物线性堆满磁盘 |
| Observer lifecycle cap | quick-verify/sentinel 所有终态 stop observer，runner 按 YAML TTL/maxSamples 自停 | 防止 detached observer 与 Chrome process tree 在线性巡检中泄露内存 |
| Capacity trigger | 达到高水位时输出 safe-stop 决策表 | 避免为了百分比目标破坏运行面 |
