mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(daemon): make OD Next opt-in through an app-config switch OD Next shipped switched on: `readOdNextRolloutPolicy` read `active` when `OD_NEXT_STRATEGY_ROLLOUT` was unset, so merging the strategy changed how every design run behaved on every install. The only way back to the ordinary route was an environment variable, which `PACKAGED_CHILD_ENV_ALLOWLIST` strips before the packaged daemon ever sees it — a packaged user had no way to choose either direction. Turn the switch around. An installation that configured nothing runs `off` and takes the ordinary route, and `odNextStrategyMode` in app config is how it opts in. The run route reads that field per request, so configuring it takes effect on the next run rather than the next daemon restart, and it persists across restarts and upgrades because it lives in app-config.json rather than in one process's environment. `OD_NEXT_STRATEGY_ROLLOUT` still outranks the saved mode, so a pinned process stays pinned: an operator debugging one daemon, a packaged smoke run, and a test each set the mode for one process without overwriting what a user saved, and without a user's saved choice overriding theirs. `od strategy rollout status` now reports which authority decided (env / app_config / default), so whoever just configured the mode can confirm theirs is the one in effect instead of inferring it from the resulting mode. * test(daemon): opt the failure-telemetry smoke into OD Next Its `task_hierarchy` cases assert how an ADMITTED Run reports — one `strategy-task:` trace per task execution with a `strategy-stage:` span per Run inside it. That representation only exists for a Run OD Next owns, so with the strategy now opt-in these three cases waited 3s for an observation the daemon had no reason to emit and timed out. Opt in through the same `putConfig` the cases already use. The `single_run` case keeps its representation on top of the same opt-in, because deepseek sits outside the capability gate — which is the contrast the table exists to draw, and it now draws it without depending on an install-wide default. * fix(daemon): refuse an odNextStrategyMode write that is not a mode Review catch (#7362): `doWrite` copies the existing config before applying a partial, so the sanitizer's `delete` did not "leave the previous choice alone" — it removed an already-saved `active`. `od config set odNextStrategyMode acive` therefore switched the installation off, printed success, and left the person who typed it no way to tell that from an opt-out they never asked for. Dropping a bad value is the right trade for a preference, where the cost is one setting falling back to its default. It is the wrong trade for a switch that decides whether OD Next runs at all, because there "dropped" and "deliberately off" are the same state. So a write that names the key with anything other than a mode (or `null`, which stays the deliberate way to opt back out) is refused whole: 400 from `PUT /api/app-config`, non-zero exit from `od config set`, nothing written — neighbouring keys in the same body included. The read path stays fail-soft: a hand-edited or truncated file drops the value and reads as unconfigured, which is `off`, rather than taking the daemon down. The 400 carries the nested error envelope. `od` only finds a code in that shape; from a flat body it fell back to `daemon-not-running` and exited 64, telling a caller to go start a daemon that had just answered. * fix(daemon): stop reading an unreadable app config as an unconfigured one Review catch (#7362), and the same defect class as the write-side fix one commit earlier: an error was being made indistinguishable from a deliberate choice. `readAppConfig` already answers `{}` for both states that legitimately mean "nothing configured" — no file, unparseable file — and throws only when the daemon genuinely cannot read its own config. The `.catch(() => ({}))` on the rollout reads flattened that throw into the same answer, so an installation whose saved `active` became unreadable silently took the ordinary route, and `od strategy rollout status` reported `requestedMode: off` / `requestedModeSource: default` — a claim about the operator's choice made from a fact about the disk. The run route and the status route now let the read fail. Express 5 forwards the rejection, so the operator sees an error instead of a confident wrong answer, and the run fails rather than quietly running unconfigured (it would have had no `agentCliEnv` either). The safety latch is the one place that must not depend on the read, so it takes a thunk now: the latch lands first, and a failed read skips the control-change event rather than reporting a mode the installation never chose. Also gives the opt-in acceptance case an explicit 60s timeout. It is the only case in that file driving two full runs against a real server, and the suite default of 20s leaves it no headroom on a slow runner. --------- Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>