diff --git a/.agents/skills/unidesk-gc/SKILL.md b/.agents/skills/unidesk-gc/SKILL.md index c79b7a17..e8e6193c 100644 --- a/.agents/skills/unidesk-gc/SKILL.md +++ b/.agents/skills/unidesk-gc/SKILL.md @@ -165,7 +165,9 @@ bun scripts/cli.ts gc remote status --job-id - 只读取 `/proc/meminfo` 的 `MemAvailable`; - 输出必须披露 `metric=MemAvailable`、`source=/proc/meminfo` 和 `swapExcluded=true`; - swap 只作为独立压力信号,绝不计入启动可用量; - - 当前阈值为 `4 GiB`:人工启动在 `MemAvailable <= 4 GiB` 时阻断,sentinel 在 `< 4 GiB` 时跳过本轮。 + - 启动门槛和比较语义只读取 `config/hwlab-node-lanes.yaml#templates.hwlabV03.webProbeWorkbench.resourcePolicy.memoryStartGuard`; + - 文档、skill、源码和测试不得复制 `thresholdBytes` 的当前数值; + - 除非用户明确授权修改 WebProbe 启动门槛,否则不得改变 owning YAML 的 `thresholdBytes`。 - 人工处置命令必须按顺序执行: ```bash @@ -181,10 +183,10 @@ bun scripts/cli.ts gc remote memory - `--full` 只用于下钻完整身份明细,不得作为 scoped run 的默认前置步骤; - `run` 返回 job id 后,重复执行 `status --job-id ` 直到终态; - job 未终态时不得提前执行 `memory` 或重试 WebProbe; - - 终态后执行 `memory`,只有 `MemAvailable > 4 GiB` 才可重试人工启动; + - 终态后执行 `memory`,只有不再命中 owning YAML 的 `manualComparator` 时才可重试人工启动; - 内存处置不得改用未带 `--memory-pressure-only` 的通用 plan/run。 - sentinel cadence: - - `< 4 GiB` 时返回 `skipped-wait-next-round`; + - 命中 owning YAML 的 `sentinelComparator` 时返回 `skipped-wait-next-round`; - 不创建 observer、不启动 Chromium、不自动执行 GC; - 等待下一轮 cadence 重新读取 `MemAvailable`。 diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 4b20a626..37ae709e 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -733,6 +733,7 @@ templates: metric: MemAvailable source: /proc/meminfo swapExcluded: true + # 唯一启动门槛;只有用户明确授权时才允许修改。 thresholdBytes: 2147483648 manualComparator: less-than-or-equal sentinelComparator: less-than diff --git a/docs/reference/gc.md b/docs/reference/gc.md index fe8184cf..3a05288f 100644 --- a/docs/reference/gc.md +++ b/docs/reference/gc.md @@ -191,15 +191,16 @@ JD01 Web observe artifact 是一等 GC 对象。state root 必须来自 YAML; - 启动资格真相源: - 唯一配置位于 `config/hwlab-node-lanes.yaml#templates.hwlabV03.webProbeWorkbench.resourcePolicy.memoryStartGuard`; - - 当前阈值合同为 `2 GiB`,即 `2147483648` 字节; - 代码只负责读取、比较和结构化披露,不得另设隐藏阈值。 + - 文档、skill、源码和测试不得复制 `thresholdBytes` 的当前数值; + - 除非用户明确授权修改 WebProbe 启动门槛,否则不得改变 owning YAML 的 `thresholdBytes`。 - 唯一内存指标: - 只使用 `/proc/meminfo` 的 `MemAvailable`; - 输出必须披露 `metric=MemAvailable`、`source=/proc/meminfo` 和 `swapExcluded=true`; - `SwapFree`、`MemFree + swap`、cgroup 虚拟空间或其他 virtual memory 指标不得合并进可用量; - swap 只能作为独立压力信号,不能改变启动资格。 - 人工 `web-probe observe start`: - - `MemAvailable <= 2 GiB` 时返回结构化 blocked; + - 按 owning YAML 的 `manualComparator` 比较 `MemAvailable` 与 `thresholdBytes`,命中时返回结构化 blocked; - 不创建 observer,也不启动 Chromium; - blocked 后只能按以下 scoped 流程人工处置。 @@ -217,14 +218,13 @@ bun scripts/cli.ts gc remote memory - `run` 返回 job id 后,重复执行 `status --job-id ` 直到明确终态; - job 未终态时,禁止提前执行 `memory` 或重试 WebProbe; - job 终态后执行 `memory`,以新的 `/proc/meminfo` 快照重新判定; - - `MemAvailable` 仍不大于 `2 GiB` 时,下一次人工 start 继续 blocked; + - `MemAvailable` 仍命中 owning YAML 的人工比较条件时,下一次人工 start 继续 blocked; - 不得用未带 `--memory-pressure-only` 的通用 plan/run 代替本流程。 - sentinel cadence: - 每轮在 provider、凭据和 observer 启动前读取 `MemAvailable`; - - `MemAvailable < 2 GiB` 时返回 `skipped-wait-next-round`; + - 按 owning YAML 的 `sentinelComparator` 命中启动门槛时返回 `skipped-wait-next-round`; - 不创建 observer、不启动 Chromium、不自动执行 GC; - - 等待下一轮 cadence 重新判定; - - 恰好等于 `2 GiB` 时,按 sentinel 的 `<` 比较器允许继续。 + - 等待下一轮 cadence 重新读取 owning YAML 并判定。 - 运行时上限: - 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; diff --git a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md index ae23d617..c663af98 100644 --- a/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md +++ b/project-management/PJ2026-01/specs/PJ2026-01060508-web-probe-sentinel.md @@ -768,11 +768,13 @@ CI/CD rollout 门禁仍只验证配置声明的 `/health` endpoint。web-probe q - 启动资格: - 所有会创建 Playwright/Chrome 的 WebProbe 人工入口与 sentinel cadence 必须在真正启动浏览器前读取目标主机 `/proc/meminfo` 的 `MemAvailable`; - 启动阈值、比较语义、最终复核等待和浏览器证据上限只由 owning YAML 提供; + - 启动阈值的唯一事实源是 `config/hwlab-node-lanes.yaml#templates.hwlabV03.webProbeWorkbench.resourcePolicy.memoryStartGuard.thresholdBytes`; + - 规格、长期参考、skill、源码和测试不得复制阈值数值,也不得提供隐藏默认值或 fallback; + - 除非用户明确授权修改启动门槛,否则任何任务、代理、自动链和运行面调试都不得改变该字段; - swap 作为独立压力信号展示,不得计入启动资格。 - 当前 HWLAB v0.3 策略: - - 阈值为 `4 GiB`; - - 人工入口在 `MemAvailable <= 4 GiB` 时阻断,并提示通过受控 GC 完成 plan、run、终态查询和重新读取; - - sentinel cadence 在 `MemAvailable < 4 GiB` 时跳过本轮并等待下一 cadence,不创建 observer 或 Chrome,也不自动执行 GC。 + - 人工入口按 owning YAML 的 `manualComparator` 比较 `MemAvailable` 与 `thresholdBytes`,命中时阻断,并提示通过受控 GC 完成 plan、run、终态查询和重新读取; + - sentinel cadence 按 owning YAML 的 `sentinelComparator` 比较,命中时跳过本轮并等待下一 cadence,不创建 observer 或 Chrome,也不自动执行 GC。 - 并发启动保护: - 只允许使用覆盖“最终重新读取 `MemAvailable` 到确认真实 Chrome/Chromium executable 已就绪”这一实际风险窗口的最小锁; - 确认就绪后必须立即释放; diff --git a/scripts/src/hwlab-node-web-probe-memory-guard.test.ts b/scripts/src/hwlab-node-web-probe-memory-guard.test.ts index b90bb573..057f6742 100644 --- a/scripts/src/hwlab-node-web-probe-memory-guard.test.ts +++ b/scripts/src/hwlab-node-web-probe-memory-guard.test.ts @@ -9,7 +9,9 @@ import { evaluateWebProbeMemoryGuard, runWebProbeMemoryGuard, webProbeBrowserEvi import { nodeWebProbeScriptRunnerSource } from "./hwlab-node-web-probe-runner-source"; const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01"); -const thresholdBytes = 2 * 1024 * 1024 * 1024; +const memoryStartGuard = spec.webProbe?.resourcePolicy?.memoryStartGuard; +assert.ok(memoryStartGuard, "owning YAML must declare the WebProbe memory start guard"); +const thresholdBytes = memoryStartGuard.thresholdBytes; function memoryPayload(availableBytes: number, extra: Record = {}): Record { return { @@ -33,9 +35,10 @@ function exportedFunctionSource(source: string, name: string): string { return source.slice(start, next < 0 ? source.length : next); } -test("WebProbe memory threshold is read from the single owning YAML policy", () => { +test("WebProbe memory threshold and comparison semantics are read from the single owning YAML policy", () => { const policy = spec.webProbe?.resourcePolicy?.memoryStartGuard; - assert.equal(policy?.thresholdBytes, thresholdBytes); + assert.ok(policy); + assert.ok(policy.thresholdBytes > 0); assert.equal(policy?.metric, "MemAvailable"); assert.equal(policy?.source, "/proc/meminfo"); assert.equal(policy?.swapExcluded, true); @@ -59,7 +62,7 @@ test("browser evidence limits and viewport screenshots are read from owning YAML assert.match(assignments, /UNIDESK_WEB_PROBE_MAX_NETWORK_FAILURE_BYTES='131072'/u); }); -test("manual start is blocked at exactly 2 GiB and requires controlled GC then MemAvailable recheck", () => { +test("manual start is blocked at the configured threshold and requires controlled GC then MemAvailable recheck", () => { const guard = evaluateWebProbeMemoryGuard(spec, "manual-start", memoryPayload(thresholdBytes)); const memory = guard.memory as Record; const gcNext = guard.gcNext as Record; @@ -82,7 +85,7 @@ test("manual start is blocked at exactly 2 GiB and requires controlled GC then M assert.deepEqual(gcNext.startRemainsBlockedUntil, ["gc-job-terminal", "MemAvailable-threshold-cleared"]); }); -test("sentinel allows exactly 2 GiB but skips one byte below without considering swap", () => { +test("sentinel allows the configured threshold but skips one byte below without considering swap", () => { const exact = evaluateWebProbeMemoryGuard(spec, "sentinel-cadence", memoryPayload(thresholdBytes, { swapFreeBytes: 12 * 1024 ** 3 })); const below = evaluateWebProbeMemoryGuard(spec, "sentinel-cadence", memoryPayload(thresholdBytes - 1, { swapFreeBytes: 12 * 1024 ** 3 })); const belowMemory = below.memory as Record; @@ -243,7 +246,7 @@ test("final launch guard rechecks MemAvailable under the configured lock", () => const async = webProbeGuardedLaunchShell(spec, "manual-start", "node helper.mjs start", "structured-output"); assert.match(sync, /flock -w '15'/u); assert.match(sync, /MemAvailable:/u); - assert.match(sync, /mem_available_bytes.*-le '2147483648'/u); + assert.match(sync, new RegExp(`mem_available_bytes.*-le '${thresholdBytes}'`, "u")); assert.match(sync, /setsid sh -c/u); assert.match(sync, /exec 9>&-/u); assert.equal(sync.includes("playwright"), false);