mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
933dc96038
* fix(daemon,web): stop a refused turn reporting a delivered deck as failed One predicate answered two different questions, and the strict answer is the wrong answer to the loose one. `validateRunDeliverable` asks "did THIS run write the canonical entry". That is the right gate for accepting an Agent's completion claim — a turn must not pass by pointing at a file an earlier turn wrote. It is also `false` for the most ordinary shape of an OD Next refusal: the user types 「继续」, the agent re-checks work an earlier turn already finished, correctly rewrites nothing, and the turn is refused over its machine block. Both safety nets read that one answer, so both went down together. The coordinator's completion inference declined, and `deliveredDespiteBlock` in the web provider — the carve-out that exists to keep a delivered turn out of the failure branch — could never fire. A field report shows the result: a red 「回复已收到,但没能记录下来」card under a finished 16-page deck the user could see rendered in the preview pane beside it. The physical run had exited 0 and the 2.27 MB file was on disk. `validateProjectDeliverable` is the second question: does the user have it, right now. Same entry resolution, same kind contract, same readability check, minus the run-scoped gates. `GET /api/runs/:id` resolves it only for a terminal run whose strategy task settled blocked — the one case a client has to distinguish — and logs the answer, because withholding a failure the user cannot act on is right and withholding it invisibly is not. The verdict does not move: the task still blocks, the strategy contract still refuses to complete on an unproven claim, and a turn that delivered nothing still fails with its card. What changes is that a bookkeeping failure no longer tells a user they lost work that is sitting in their project. This is also what `docs/design/run-errors/error-ux-design.md` §3 already ruled for the case: 「文件生成了但进程报错:算成功」, with the stated reason that doing otherwise "把一堆无害报错变成失败潮". Red spec: apps/web/tests/providers/strategy-blocked-verdict.test.ts 「does not become a failure when the project holds the deliverable」 — red on main, green here. * fix(daemon,web): name the gate that refused a turn, not the parser that noticed The protocol parser raises `od_next_protocol_runtime_state_missing` before `validateAcceptedTurn` — the function that owns the real gates — is ever reached. So every block-less turn was filed under the parser's own code, and `reasonCodes[0]` is what the failure card, the diagnostics export and the analytics bucket all read. The effect on a user who got nothing this round: they were told a marker was missing. True of the reply, useless to them, and not the reason the turn could not succeed — the identical failure with a DECLARED block reported `od_next_canonical_deliverable_invalid`, the host fact they can act on. Two names for one failure, and the less useful one was the one that shipped. `reattributeUndeclaredTurn` already existed for exactly this, scoped to one cell (a clarification turn that asked again). Extend it: when a block-less turn was SHAPED like a completion the host may infer and the inference still declined, the evidence is what refused — both helpers gate on the shape, which just held, and the evidence. Lead with it. The parser's code stays behind it, because `blocked_reason_codes_json` is the durable record of what the turn actually wrote, and a triage that cannot tell "declared badly" from "declared nothing" has lost the one distinction that decides whether a serialization repair was ever possible. ATTRIBUTION ONLY — no verdict moves. The two existing specs that pinned the old name still assert the verdict they own and now assert the new attribution; the turns they describe block exactly as before. `od_next_canonical_deliverable_invalid` gets the card it now needs. It is deliberately not the Runtime State copy: those say "your reply went missing", this says "nothing came out this round", and only the second is true when the project is empty. Retry earns its place here — the deliverable is missing because this turn wrote nothing, and a re-run is the thing that can write it. ⚠️ The copy is engineering's, like the rows around it. `error-ux-design.md` has no cell for it; S23 (跑完没生成文件) is the nearest and is listed there as invisible in telemetry. Product should rewrite the wording, not the routing. Red spec: apps/daemon/tests/strategies/od-next/coordinator.test.ts 「names the evidence that refused an undeclared completion, not the parser」 and 「gives the undeclared and declared shapes of one failure the same name」 — both red on main, green here. * feat(daemon): make an OD Next block countable instead of invisible `run_finished.result` is derived from the physical run status (`runResultFromStatus`: `succeeded → success`), and a refused OD Next turn normally exits 0 with a complete reply on screen. So this entire class of failure reported `result: 'success'` while the user was looking at a red card, and nothing queryable disagreed — the reason codes lived only in the task store's `blocked_reason_codes_json` and in a client-side error object. `error-ux-design.md` §《线上频次与优先级》 already flags the shape of this problem for S23/S24: 「被算成了成功 —— 看不到是坏事:线上到底有多少,我们不知 道」. OD Next made it worse than invisible by pairing the silent metric with a visible failure card. Carrying `reasonCodes[0]` — the same value the card keys on — makes the class countable without changing what `result` means. Read off the run's own terminal projection, the object the SSE `end` payload and the failure card were both built from, so the three cannot drift. Omitted for every run whose strategy task did not block, which is the common case. This is the half that makes the suppression in the preceding commits safe to ship: a failure the user is no longer shown is one we can still count. * fix(web): do not go silent on a turn that produced no reply either The preceding fix credits a deliverable an EARLIER turn wrote, which is right for the case it was written for — the agent re-checks finished work and correctly rewrites nothing — and wrong for the mirror case in #7564: a blank OD Next response into a project that already holds a prototype. There the file is real and the user still got nothing, so withholding the card swaps one wrong answer for the other. Require the turn to have actually said something before the looser field can suppress a failure. The accumulated assistant text is already on hand in the stream consumer, so this costs nothing and needs no new daemon field. The stricter field keeps its existing behaviour untouched: an artifact THIS run produced is delivery whether or not the agent narrated it. Red spec: apps/web/tests/providers/strategy-blocked-verdict.test.ts 「still fails when the turn produced no reply at all」 — verified red with the guard removed, green with it. * refactor(contracts): make the OD Next block dimension a tested pure mapper Observability that nothing exercises is observability that rots. The derivation was inline in the run lifecycle, where the only way to reach it is to boot the whole analytics flow — so it would have failed silently to "field absent", which is indistinguishable from "no run ever blocked". Move it next to `harnessAnalyticsFromRolloutDecision`, which answers the neighbouring question about the same run and already has a suite. The edges that matter are the ones a wrong answer would quietly mislabel: a task that has not settled, one that settled on something other than `blocked`, and a blocked one an older daemon projected without a reason. --------- Co-authored-by: Cheems <216108319+hihicheems@users.noreply.github.com> Co-authored-by: lefarcen <935902669@qq.com>