getSystemTotalMb returned os.totalmem() — the host's physical RAM — so a
4GB Docker container on a 32GB host never auto-flagged as low-memory and
the low-memory render profile didn't activate exactly where it's needed
most. Read the cgroup v2 limit (/sys/fs/cgroup/memory.max, with the v1
fallback and its no-limit sentinel handled) and use min(host, cgroup).
The probe is best-effort and non-Linux platforms never touch /sys.
Review follow-ups: worker sizing (calculateOptimalWorkers) and the
getSystemResources diagnostics previously read os.totalmem() directly
and now use getSystemTotalMb(), so container limits actually govern
parallel spawn decisions; CLI telemetry reports the effective total as
well. The cgroup probe result is cached for the process lifetime (the
limit is immutable per process) with a test reset hook; a detected limit
logs once so operators can see which source governs, and a
present-but-unreadable cgroup file warns once instead of failing
silently — absence stays silent. The root-path-vs-/proc/self/cgroup
trade-off is documented at the path constants. cli/tsconfig.json gains
the gcp-cloud-run/sdk source alias (matching the existing producer and
aws-lambda entries) so the cli typecheck resolves from source in a
fresh checkout.
Refs #1193, #1194, #1195, #1236
## What
Adds an auto-detected **low-memory safe render profile**. On hosts at or below 8 GB total RAM, the render pipeline collapses to its cheapest shape instead of running multiple concurrent Chrome instances.
When `lowMemoryMode` is active and the user hasn't passed `--workers`, the orchestrator:
- **skips auto-worker calibration** — no throwaway second Chrome just to time 5 frames;
- **pins to a single worker** — so the probe Chrome is reused for capture, never N concurrent;
- **prefers screenshot capture over BeginFrame** — avoids the BeginFrame protocol-timeout → relaunch churn on slow hardware;
- logs a one-line explanation of what it did and how to override.
Builds on #1221 (merged), which fixed the calibration timeout cap, the `<= 8192` boundary, and added the CLI timeout flags.
## Why
Reported in #1218 / #1219: renders on 8 GB laptops sit at low progress for minutes or stall. Root cause (per the triage thread) is architectural — the default pipeline launches up to 4 Chrome instances sequentially/overlapping (probe, calibration, capture, screenshot-fallback), each ~256 MB+, on machines with ~3 GB free. The concurrent browsers drive memory pressure that makes every CDP call slow and spikes V8 GC pauses.
#1221 made the timeouts and memory flags *apply correctly*; this PR removes the expensive shape entirely on the machines that can't afford it, rather than tuning it. "Smarter by default."
## How
- **`packages/engine/src/services/systemMemory.ts`** (new): one shared `isLowMemorySystem()` / `getSystemTotalMb()`, de-duplicating the `totalmem()` reads previously copied in `config.ts` and `browserManager.ts`. Threshold is inclusive (`<= 8192 MB`) — real "8 GB" hardware reports ~7600–8192 MB after firmware/iGPU reservations, so a strict `<` would skip the optimisation on the very hardware that needs it.
- **`config.ts`**: new `lowMemoryMode` field on `EngineConfig`, resolved tri-state — explicit override → `PRODUCER_LOW_MEMORY_MODE` (on/off) → auto-detect from total RAM.
- **`renderOrchestrator.ts`**: gate calibration off, pin workers to 1, force screenshot capture, and emit a safe-mode log line when `lowMemoryMode` is set and `--workers` is absent.
- **`render.ts`**: `--low-memory-mode` / `--no-low-memory-mode` override (sets the env var the producer's `resolveConfig` reads) + docs table entry.
Fully overridable: an explicit `--workers N` restores calibration-free parallelism; `--no-low-memory-mode` / `PRODUCER_LOW_MEMORY_MODE=false` restores the full default shape.
### Deliberately deferred (separate PRs)
- **Reuse the probe session for calibration**: only executes on the tier *above* 8 GB (safe-mode skips calibration on the target boxes). A correct BeginFrame-mode reuse would lose calibration's fast-fail-to-screenshot timeout — real risk on a path the reported scenario never hits. Better scoped on its own.
- **Retuning `calculateOptimalWorkers`'s `totalmem*0.5/256` memory model**: hot path for *all* renders incl. servers/Lambda, outside this PR's local-laptop scope.
## Test plan
- [x] Unit tests added/updated — `systemMemory.test.ts` (8192 boundary cases), `config.test.ts` (tri-state env resolution + explicit-override precedence). Engine suite passes (25 relevant tests).
- [x] `tsc` clean across engine/producer/cli; `oxlint` + `oxfmt` clean; removed an unused export so the `fallow --fail-on-issues` dead-code gate stays green.
- [x] Documentation updated — `docs/packages/cli.mdx` render-flags table.
- [ ] Manual testing on a real ≤ 8 GB host — not yet run; behaviour is unit-covered and the safe path (1 worker + screenshot) is already a supported render shape.
Note: one pre-existing producer test (`rejects a maliciously crafted key…`) fails identically on `main` — environment-specific path test, unrelated to this change.