mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
4831471254e3ff0867b0bf89a93cf9558bdb3d76
3423
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4831471254 | fix(web): keep resize hitbox off chat scroll edge (#7324) | ||
|
|
443fcbd5f9 |
docs(release): add 0.21.0 release notes (#7391)
* docs(release): add 0.21.0 release notes * docs(release): reframe 0.21.0 around recovery * docs(release): adopt product framing for 0.21.0 |
||
|
|
1a40413785 | fix(web): localize missing bundled scenario error (#7325) | ||
|
|
d488b1aa87 |
fix(web): localize skill card descriptions in the plugin library (#7369)
The 插件 → 技能 catalog resolved a skill card's title through localizeSkillName but handed the raw English frontmatter `description` straight to the card, so a zh-CN user saw a Chinese title stacked on an English summary in the same card. docs/skills-protocol.md documents zh_description / en_description as "localized picker description; falls back to description", and docs/skills-contributing.md says `description` is deliberately English because it is the fallback field — so rendering it directly bypassed the documented resolution. Route both halves through one named helper so the invariant "a card's headline and summary resolve through the same locale" reads at the call site. The neighbouring plugin card builder already held it via localizePluginTitle + localizePluginDescription. OPEND-2250 |
||
|
|
fbd97d4510 |
fix(web): return to the originating conversation from design-system create (#7367)
The /design-systems/create page is reachable from the Design systems tab, the composer's design-system picker, the Library, a home card, and the project canvas' 新建设计体系 button. App.tsx wired its Back control to one fixed destination, so whichever surface opened the page, Back dumped the user on the Design systems tab — from inside a running conversation that reads as being thrown out of a task mid-flight (OPEND-2249). Route Back through the router's existing goBack(fallback) helper, behind a named handler whose docblock states the invariant: Back returns to the surface that opened the page, and only falls back to the Design systems tab when the page is the first in-app layer (deep link or fresh load). The standalone Design systems entry keeps its current destination. |
||
|
|
9384791fc5 |
chore(landing): remove homepage Go promo banner (#7385)
Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
172ad3e46a |
test(e2e): retire the Pricing campaign-unlimited assertion with its source (#7383)
main is red and the merge queue is rejecting everything queued after 08:43. The only failing check is this file: FAIL tests/pricing-unlimited-models.test.ts Error: campaignUnlimitedModelNames on the Pricing page not found — did it get renamed? #7349 retired that contract deliberately: it removed campaignUnlimitedModelNames and the per-model access markers from pricing-individual-plans.astro, and updated apps/landing-page/tests/pricing-contract.ts to assert those markers now stay absent. This e2e file is a second copy of the same contract and was missed, so it still scrapes the astro source for a symbol that no longer exists. Drop that assertion and the helper only it used. The two cases that remain are the ones that actually span packages — popular-model ordering, and every Pricing display name mapping to an AMR model id — which is why this file lives in e2e rather than beside the page. |
||
|
|
2862a47f84 |
feat(landing): simplify Pricing while keeping sold-out Go (#7349)
* docs: define Free pricing demo
* docs: plan Free pricing demo implementation
* feat(landing): replace Go pricing with Free
* docs: require Odcrew for GitHub operations
* fix(landing): fail closed when Pricing billing context is missing
Keep static CTAs when session or billing lookup fails instead of
synthesizing a new-checkout context. Drop the obsolete Free demo
superpowers docs now that Go stays sold out.
* docs: drop internal Odcrew GitHub operations from AGENTS.md
That workflow is an internal tool and should not live in the public
repository guide.
* fix(landing): hide image resolution badges on the sold-out Go card
Go has no image access, so the Up to 4K badge contradicted the
No access heading. Restore the paid-only resolution label.
* fix(landing): restore yearly Pricing default and card polish
Signed-out visitors see yearly billing again. Center the Cloud
capability and FAQ blocks with the plan cards, raise the API-key
tooltip above sibling cards, restore the Go unavailable-module fill,
and localize publish/share plus Design Systems benefits.
* fix(landing): click monthly in interval analytics browser test
Yearly is the signed-out default, so clicking yearly is a no-op and
never sends the second bridge batch. Drive monthly instead.
* fix(landing): localize Pricing tooltip, hero, and audience tabs
Move the API-key help tooltip into the pricing content tables for every
plan locale. Drive the hero and Individual/Team labels from those tables
instead of zh/en ternaries, and translate leftover Japanese/Korean Team
labels.
* fix(landing): restore Pricing savings row and left-align Cloud copy
Keep the yearly save/billed notes on one vertically centered row, and
left-align the multimodal Cloud title and description.
* fix(landing): restore Pricing console footnote and Go banner contract
Put {console} back in every localized billing footnote so the Cloud
Console handoff still renders, and re-lock homepage Go copy against
the retained Pricing allowance.
---------
Co-authored-by: 张辉华 <zhanghuihua@refly.ai>
Co-authored-by: mrcfps <mrc@powerformer.com>
|
||
|
|
0944e176ce |
feat(daemon): make OD Next opt-in through an app-config switch (#7362)
* 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> |
||
|
|
a8b571ccd5 |
feat(amr): load Coding Plan models dynamically (#7289)
* feat(amr): load coding plan models from Vela * test(web): mark wallet gate fixtures authoritative Generated-By: looper 0.11.8 (runner=fixer, agent=codex) * fix(web): keep personal plan badges out of team workspaces Generated-By: looper 0.11.9 (runner=fixer, agent=codex) |
||
|
|
9810fc08fe |
fix(daemon): stamp first_visible_output when output is emitted, not decoded (#7364)
* fix(daemon): stamp first_visible_output when output is emitted, not decoded `time_to_first_visible_output_ms` has been a byte-for-byte copy of `time_to_first_token_ms` since it shipped: `noteFirstTokenAt()` marked both `first_token` and `first_visible_output` from one call with one timestamp, and marks are first-write-wins, so the real mark inside `send()` could never fire. Across 205,795 `run_finished` events in the last 7 days the difference was 0 for every single run — p50, p90, p99 and max all 0. The daemon does not emit every token it decodes. The `<od-title>` marker stripper, the fabricated-role-marker guard (#3247) and close-time stdout buffering can all hold bytes back after a token has been recognised, and that withheld window is exactly what this field exists to measure. `send()` is now the sole owner of the mark: it is the last thing every stream handler passes through, so a mark there means the bytes really left the daemon. `stdout` now counts as visible output too — the plain / BYOK / antigravity family has no structured `agent` stream, and antigravity buffers stdout until close, so its first token and its first visible byte can be a whole run apart. No user-visible change. `first_token` keeps its current meaning, the `?? firstTokenAt` fallback still covers runs that never emit anything, and `last_observed_phase` keeps the same labels — a later `first_visible_output` can only move a run from `tool_execution` to `stream_output`, which is the more accurate answer. * fix(daemon): read the decode clock before emitting the first text delta Splitting `first_visible_output` off `first_token` exposed an ordering flaw the shared timestamp had been hiding. On the qoder / json-event- stream / pi-rpc, Copilot and ACP paths the gate reads if (emitTitleFilteredGuardedTextDelta(delta)) noteFirstTokenAt(); so the emit — which fans the delta out to every SSE client and stamps `first_visible_output` on the way — completes BEFORE the clock is read for the token. Two consequences: `time_to_first_token_ms` silently included our own write latency, and on a loaded box the visible-output mark could land a millisecond AHEAD of the token that produced it, making the gap negative. Capture the decode timestamp before the gate and hand it to `noteFirstTokenAt`. The Claude and plain-stdout paths already stamped the token first and are unchanged. With all five sites token-first, `firstVisibleOutputAt >= firstTokenAt` now holds by construction rather than by both marks sharing one call, and the straight-through spec asserts it. * fix(daemon): mark visible output on the strategy close-time tail The OD Next machine protocol withholds any text that might still complete a reserved `<open-design-...>` block. `finish()` is what finally rules that out, at child close, and that release is persisted and broadcast directly instead of going through `send()`. So a reply whose visible text was withheld in its entirety recorded no visible output at all, and the analytics fallback collapsed a real close-time wait back to `firstTokenAt` -- reproducing the exact dead-field symptom this change exists to end. Both emission paths now apply one shared visible-output rule; `first_model_event` stays on the live path only, so a withheld reply cannot drag the phase anchor to run end. Telemetry also stops being able to break the product. Every lifecycle mark in this file rides the path the model's bytes travel on, so a throw from instrumentation cost the user the whole reply rather than one analytics field: a red test that injects a throwing tracer failed the run outright before this. The marks now run inside a failure-isolated helper scoped to the marks alone, logging once per run instead of swallowing silently. The regression suite no longer inherits the OD Next rollout default -- that default has been `active`, is expected to flip after 0.21.0, and is a user-facing switch besides. Each case names the mode it runs under and the strategy case asserts admission actually happened. `run_finished` delivery is awaited through the daemon's own analytics flush instead of an arbitrary 15-second cutoff, while still failing when the event never arrives. |
||
|
|
a8518fb861 | fix(daemon): isolate task observation manifest env (#7373) | ||
|
|
767e22a6c3 |
fix(web): three viewer defects from the 0.20 dogfooding batch (#7358)
* fix(web): three viewer defects from the 0.20 dogfooding batch Three separate reports, all landing on the same viewer surface and all sharing one root cause: the preview is a sandboxed, opaque-origin iframe, so nothing that happens inside it reaches the host document. Each fix supplies the host with a signal it can actually observe. OPEND-2035 — the device/viewport menu would not close when the user clicked the preview. The click-outside handler is correct and works, but it listens for pointerdown on the host document and a click landing on the preview dispatches inside the frame instead. Measured against a real runtime: clicking host chrome delivered 1 host pointerdown and closed the menu; clicking the preview delivered 0 and left it open — and the preview is the largest, most natural place to click to dismiss a menu. Close on focus moving into a frame, which is the one signal that click does give the host. Requiring the frame to actually hold focus keeps an ordinary app switch from closing a menu the user never dismissed. OPEND-2156 — entering presentation left no way back. The overlay covers the whole content area and, measured live, contained zero buttons. Esc does exit, but only while focus is still on the host: the first click on a slide moves focus into the sandboxed frame and from then on neither pointer nor key events arrive, so Esc stops working too. That leaves the OS window button as the only thing on screen that looks like "close", which quits the app. Give the overlay its own exit control, in the host document where focus cannot strand it. The OS close semantics are left alone — nothing here intercepts window close. OPEND-2160 — the version-history open button read as "share". The icon (external-link-line) and the action (open in a new window) were both defensible; what was missing was the destination, so "Open preview" next to an arrow-leaving-a-box left users guessing between share, export and open. Name the destination in all 19 locales, which makes the existing glyph read the way it was meant to without turning an icon choice into a guess. The open control also reused the close control's class name; two controls that do opposite things now spell differently in the markup. Each fix has a red spec that fails on main. `FileViewer.test.tsx` had two assertions pinned to the old copy and is updated with it. * fix(web): route present exit through full teardown The overlay exit control only unmounted the overlay. Presenting a deck also opens the speaker-notes presenter popup, so exiting that way left the user still presenting in a second window -- half of OPEND-2156. Fullscreen presentations could also skip a clean exitFullscreen(). Route the control through closeInTabPresentation(), the same helper Esc and presenter-close already use, and extend the spec with a deck case so a leftover presenter popup fails. * test(web): use a valid TrackingProjectKind in the deck present spec The new deck case passed projectKind="deck", which is not in the TrackingProjectKind union. Vitest does not typecheck, so it only surfaced in Preflight. The analytics dimension for a deck is slide_deck. |
||
|
|
e52b866d17 | fix(web): hide Go campaign touchpoints (#7361) | ||
|
|
79dee13d8a |
fix(download): ignore OS-managed root artifacts when claiming a base (#7268)
ensureManagedBase() rejected a managed download base as not-owned whenever any file sat at its root before the first claim, including .DS_Store, which Finder writes automatically the moment that folder is ever viewed. This is the same defect class fixed for the desktop updater's own store root in #7259 (apps/desktop/src/main/updater/store.ts), but in a separate, unpatched directoryIsEmpty() in this package's own fs-io.ts. It's reachable from the same desktop updater via downloadsRoot (downloadCopyAndClear -> managedDownload -> ensureManagedBase). Add a package-local OS_MANAGED_ROOT_ARTIFACTS allowlist (duplicated rather than shared with apps/desktop, since apps/desktop depends on this package and not the reverse) and apply it before deciding a fresh base has real, blocking content. Verified via two independent adversarial reviews (Claude Code and Codex on gpt-5.6-terra) that no other rejection path in this package (remove.ts, prune.ts, run.ts, manifest.ts, lock.ts, transfer.ts) has the same unpatched pattern. Fixes #7266. |
||
|
|
8d70b98277 | fix(web): suppress foreground completion notifications (#7329) | ||
|
|
204ee7a06d |
fix(daemon): explain an ACP handshake rejection instead of retrying it (#7303)
* fix(daemon): explain an ACP handshake rejection instead of retrying it
An agent CLI that answers `initialize` and then refuses `session/new` /
`session/load` surfaced as a bare `json-rpc id 2: Internal error`, and the
retry policy treated it as transient — so the run failed, retried, and failed
identically while the user read a JSON-RPC frame.
Classify a handshake-numbered protocol error as `session_init` /
`install_cli`, keep every later protocol error on `child_close`, and lead the
user-facing message with the CLI and its detected version. The raw line stays
appended because `run.error` is also the classifier's input.
* fix(daemon): route the ACP handshake guidance onto the path users hit
The guidance copy was only reachable through `rewriteKnownAgentStreamError`,
which the ACP failure path never calls. An ACP agent's error goes
`attachAcpSession` -> `fail()` -> the bridge's `send('error', payload)`, and
that callback forwarded the payload verbatim; the close handler then returned
on `acpFatalErrorObservedBeforeCancellation && hasFatalError()` before the
stderr-tail rewrite further down. So a Kimi user still read
`json-rpc id 2: Internal error`.
Rewrite the payload at the bridge instead. That one object is the whole
user-visible surface of an ACP failure: `send` streams it to SSE clients and
`design.runs.emit` reads `run.error` out of it, so correcting it there fixes
both surfaces at once. `withAcpHandshakeFailureGuidance` rewrites only the
message fields, and only for a handshake-numbered JSON-RPC error, so
`AMR_MODEL_UNAVAILABLE`, promoted opencode errors, and post-session protocol
errors keep the shape their own handling depends on. The raw line stays
embedded because `run.error` is also the classifier's input.
Cover it through the full server run cycle with a fake ACP CLI that answers
`initialize` and rejects `session/new`, asserting on what a client and the
telemetry pipeline actually observe. Against the pre-fix tree the run error is
the bare `json-rpc id 2: Internal error`; against the pre-PR tree the daemon
also opened `session/new` twice.
* fix(daemon): read the handshake failure's cause, not just its id
Running a real Kimi CLI while signed out produces `json-rpc id 2:
Authentication required`. The handshake reading keyed on the JSON-RPC id
alone, so that user was told their CLI version was incompatible and they
should update or reinstall it — a prescription for a problem they did not
have, in place of the one sentence that pointed at the fix.
The id answers *when* a failure happened; it can never answer *why*. Split
the two questions: `isAcpHandshakeRpcErrorText` stays structural, and
`isAcpCliSessionRefusalText` is what the copy and the `install_cli` verdict
now hang off — handshake numbering AND no cause with a remedy of its own.
The "has its own remedy" half reuses `classifyAgentServiceFailure` rather
than growing a second signature list: it is agent-agnostic, already covers
auth / quota / upstream, and carries its own suite. It is deliberately
unrelated to, and narrower than, `isCliNotInstalledText` — that predicate
answers "was the binary even there", which a CLI that already answered
`initialize` has plainly settled.
A handshake failure that names its cause now keeps its own text and falls
through to the branch that owns that cause, so telemetry and copy agree: an
unauthenticated handshake is `auth` / `login`, a throttled one is
`rate_limit`. A bare refusal (`Internal error`, `Method not found`) is
unchanged and still gets the CLI guidance and the retry suppression.
No pure-function test could have caught this: the helpers were consistent
with themselves. The wiring test drives a fake ACP CLI that rejects
`session/new` with `Authentication required` and asserts on the text a
signed-out user actually reads.
* test(daemon): pin the category each named handshake cause is filed under
Verifies the routing claimed in the PR body rather than asserting it. Caught
one: `insufficient balance` has its own `insufficient_balance` category, not
`rate_limit`.
* fix(chat): name the ACP session refusal instead of writing its sentence
The guidance this PR added reached the user as a paragraph the daemon
composed: "The Kimi CLI (0.38.0) accepted the connection but refused to start
a session. … Details: json-rpc id 2: Internal error". That is the wrong layer,
and it showed:
- The daemon has no locale. The string went straight into `run.error` and
the chat rendered it verbatim, so a Chinese UI put a Chinese title over an
English body. No amount of rewording fixes that; only moving the sentence
to the client does.
- The paragraph restated the raw agent line, which the card's details block
already prints — the same text twice in one card.
So the daemon now NAMES the failure and stops there: `AGENT_CLI_SESSION_REFUSED`
plus `details: { kind: 'agent_cli', action: 'update_cli', agent, agentCliVersion }`,
with `retryable: false` because a build that refuses `session/new` refuses the
identical request identically. `withAcpHandshakeFailureGuidance` stays the single
invariant point (the ACP bridge payload feeds SSE and `run.error` at once) and now
stamps only the structured half; the stderr-tail fallbacks wrap their payloads in
it too, so no path is left composing prose.
`run.error` keeps the agent's `json-rpc id N: …` line byte for byte. It is the
classifier's input — rewriting it degrades this failure class to `unknown` in
telemetry — and it is what the details block shows, so leaving it alone is also
what removes the duplicate.
The web maps the code to localized copy in `resolveRunFailureUi`, resolved before
every other branch because the copy is not a fixed sentence: it names the CLI
build the daemon detected, and falls back to a version-less wording when there
was none (the same rule the rolling model-window copy follows). The version rides
to the client as data, on the status event next to `code` / `failureDetail`.
Three keys in all 19 locales.
Red first, on the path users hit: the wiring suite (real `startServer` + a fake
ACP CLI) went red on `run.error` being the English paragraph, and a new ChatPane
render spec went red on the raw line appearing twice in one card. Both green here.
Verified in a real browser against a local runtime: the same failed run renders in
zh-CN and in en from one persisted event.
* test(daemon): stop the wiring suite pinning a racy CLI-version probe
CI run 32683047377 caught this on the previous head: the wiring test demanded
`0.38.0` inside `run.error` and got a version-less sentence, because
`getDetectedRuntimeVersions` is a process-wide cache that `probe()` clears
before each probe and refills after — a concurrent `/api/agents` refresh can
leave it empty exactly while a run is failing. Locally it always won the race;
on CI it did not.
The new assertions inherited that: `details.agentCliVersion` was pinned to an
exact string on both wiring cases. Assert the property that actually holds
instead — the version is either the one the daemon detected or absent, never
some other value — and say in the helper why presence is not asserted here.
Both copy paths (version present, version absent) stay covered
deterministically at the layers that own them: the payload unit suite and the
two web suites.
Making detection itself deterministic is a separate finding, still open on
this PR, and deliberately not fixed here.
* fix(daemon): let the classifier, not the wording, name a handshake refusal
Three review findings, one shape: the ACP handshake verdict was being decided
in more than one place, so the places disagreed.
1. The handshake reading sat inside `isAgentProtocolErrorText`, whose JSON-RPC
arm matches only `json-rpc id N: Internal error`. A CLI that refuses
`session/new` with `Method not found` or `Invalid params` skipped it
entirely and was promoted to a retryable `fatal_rpc_error` on `child_close`
— the no-auto-retry behaviour this PR claims never applied to it — or, once
the payload rewrite had stamped its code, fell all the way through to
`unknown` / `finalize`, which is exactly the untriageable bucket the module
docblock says the rewrite must never produce.
The wording an agent picks for its own rejection is not a signal. The
handshake branch now sits AFTER every cause branch instead of inside the
protocol one, so precedence is a fact of the control flow rather than a
promise: whatever auth, rate limit, balance, upstream, prompt size, or CPU
already claimed keeps it, and only an unexplained refusal reaches the last
line. Ids 3 and up never enter it and keep the transient treatment they
have always had.
2. `handshakeFailureNamesItsOwnRemedy` knew only the three
`classifyAgentServiceFailure` classes, so every other cause the run
classifier can already advise on was rewritten into the CLI-upgrade copy.
A user whose content was too long read "your CLI version is incompatible"
while the same run was filed as `prompt_too_large` / `reduce_context`.
`isAcpCliSessionRefusalText` now IS the classifier — it runs
`classifyRunFailure` over the text and reports whether that reading is the
handshake refusal — so the card the user gets and the bucket the run lands
in cannot prescribe two different fixes. The id-only helpers moved to
`runtimes/acp-handshake-id.ts` so the classifier can read the id without the
id-reader having to reach back into the classifier.
3. The guidance read the process-wide detected-version cache at FAILURE time,
while `probe()` cleared each entry before re-reading it. Any `/api/agents`
refresh overlapping a failing run decided whether the user was told which
build refused them; CI caught it (run 32683047377) and the assertion was
loosened instead of the race closed.
Both halves are closed. A probe now publishes its reading once, at the end,
and only when no later probe has already published, so the cache holds the
newest COMPLETED reading rather than the state of one in flight. And the run
freezes the version it is spawning with, so failure-time guidance reads the
run and never the live cache.
Coverage is at the wiring layer, not just the predicates: the fake ACP CLI
gained named gates so a run's handshake and a concurrent `/api/agents` refresh
can be held open together and the failure built at the exact moment the cache
is mid-probe — deterministic, no sleeping on wall-clock timing. The wiring
suite's CLI-version assertion goes back to strict.
One deferral found by the existing suite and now pinned: a handshake-numbered
frame carrying an OS-level crash banner (Windows STATUS_ILLEGAL_INSTRUCTION
from the bundled opencode) reports a child that died, not one that refused, and
stays with the crash reading.
* test(daemon): describe the overlap the gate test actually constructs
The comment said the failure is built "at the precise moment the cache is
blank". That was the pre-fix window; the cache no longer goes blank mid-probe,
which is half of what the test proves. Say what it now holds open — a probe in
flight — and name both halves it exercises.
* fix(daemon): drop the duplicate detection import that blocks startup
`getDetectedRuntimeVersions` was imported twice — once standalone and
once inside the `./runtimes/detection.js` group — so the module has two
declarations of the same lexical binding. Node's ESM loader rejects that
outright with `Identifier 'getDetectedRuntimeVersions' has already been
declared`, i.e. the daemon could not boot.
The suite never caught it: vitest transforms through esbuild, which
tolerates the duplicate, so 179 tests passed against a module real Node
refuses to load. Verified by importing server.ts under Node directly —
SyntaxError before, gone after.
* fix(daemon): report the CLI build this run actually started, on both surfaces
A version-specific refusal (`AGENT_CLI_SESSION_REFUSED`) names the CLI build
that refused, so the user knows which one to change. Two things could make
that name wrong.
**The persisted event dropped it.** `send()` stores every run event on the
assistant message before emitting it, and `runSseEventToPersistedAgentEvent`
serialized only `detail` and `code`. The live card named the build; a reloaded
one fell back to the version-less sentence — exactly when the user comes back
to act on it. The mapper now lifts `error.details.agentCliVersion` out of the
frame, validated as a non-blank string, because `details` is a pass-through bag
that also carries the agent's own JSON-RPC `error.data`.
**The spawn-time capture could name a different executable.**
`getDetectedRuntimeVersions` answers per agent id and says nothing about which
binary the reading came from. A user who repointed the CLI after Settings last
listed agents (Settings writes `KIMI_BIN`; PATH changes) ran one build and was
told to change another. The capture now resolves through
`ensureDetectedRuntimeVersions` with the same `configuredAgentEnv` that
produced this run's `agentLaunch` — cached when the launch identity is
unchanged, re-probed through the same bounded `--version` read when it is not,
and taken before the spawn marks so preflight work is accounted to preflight.
Both are pinned end to end in `acp-handshake-failure-wiring.test.ts`: one test
reads the conversation back through the messages route a reloading client
calls, the other repoints `KIMI_BIN` between detection and the run and reads
the two fake CLIs' invocation logs to establish which child actually ran before
asserting what the failure reports.
* fix(daemon): keep a runtime that never started out of the CLI-refusal verdict
An ACP handshake rejection was decided on the JSON-RPC request id alone, so any
failure numbered 1 or 2 became "this CLI build cannot open a session; change
it". AMR is the population that reaches this path, and what it runs underneath
is OpenCode: when vela's bundled OpenCode child fails to come up it reports
`start opencode server: opencode exited before readiness: exit status 3` from
inside `session/new`, which arrives handshake-numbered while saying nothing
about the agent CLI's own build.
Two things went wrong at once. The user was told to replace a perfectly healthy
CLI, and the automatic retry that actually recovers a startup race was
withdrawn — `process_exit / agent_protocol_error / session_init`,
`retryable: false`, `install_cli`, with the ACP wrapper stamping
`AGENT_CLI_SESSION_REFUSED` on top. A startup race clears on the second
attempt; a CLI-build verdict never does, so the two must not collapse.
`isManagedRuntimeStartupFailureText` names the distinction: a CLI reporting
that a runtime IT manages never came up has named the moving part itself, so
its build is not the only variable left. The markers are vela's own wrapper
text, already trusted by `isCpuUnsupportedCrashText` for the crash-banner
sibling of this shape and now shared with it. Both deferrals now read the same
way: the handshake frame is the envelope, not the evidence — a child that DIED
(crash banner) or one that never STARTED (readiness) is not a refusal.
Also stops this suite's runtime from being a property of the host. The wiring
cases drive `GET /api/agents`, which probes every shipped runtime and resolves
binaries from PATH plus the machine's toolchain directories, so a host with
real agent CLIs installed turns each refresh into a dozen real spawns: ~2.8s
here, ~28s on a reviewer's machine — past the 20s test timeout for the
mid-refresh case, which waits on two. `isolateAgentDetection` points
`OD_AGENT_HOME` at an empty per-test directory, detection's own mechanism for
scoping the search, so the only CLI anything can find is the fixture's. Same
route, same probe path, same deterministic gates and overlap assertions; the
file drops from 109s to 14s and the mid-refresh case from 30s (timing out) to
1.2s.
* revert(daemon): drop the CLI-version half of the handshake-refusal card
The copy wanted to name the build that refused, and the chain that
followed ended in the launch path: the version had to be frozen before
the spawn, freezing meant awaiting a `--version` probe there, and an
await before `spawn()` sits after the run's only pre-spawn cancellation
guard. Stop clicked during that window finishes the run as canceled,
the probe then resolves, and the child is spawned onto an already
terminal run that nothing will signal.
The whole sub-branch goes rather than a second guard. What it bought
was `(0.38.0)` in a sentence that already read correctly without it;
what it cost was a run the user asked to stop starting anyway.
Removed: the pre-spawn `ensureDetectedRuntimeVersions` await,
`run.spawnedAgentCliVersion`, and the version half of
`agentFailureIdentity`; detection's probe generation guard;
`agentCliVersion` persistence, its `PersistedAgentEvent` field, its web
pass-through, and the `{version}` copy variant in all 19 locales.
`server.ts` no longer touches the run lifecycle at all — its diff is
four `send('error', ...)` call sites and one helper.
Detection's generation guard fixes a real, independent daemon bug (for
the length of every `/api/agents` refresh the daemon cannot report the
version it detected). It ships as its own PR, and naming the build in
the copy follows it.
Kept: the localized `AGENT_CLI_SESSION_REFUSED` card in 19 languages,
no auto-retry for a handshake-stage refusal, `run.error` exactly equal
to the agent's own line, auth/quota/upstream/managed-runtime-startup
each keeping their own verdict, and ids 3+ staying out of the branch.
The cases that asserted a version number now assert the same behaviour
without one, plus `not.toHaveProperty('agentCliVersion')` and a locale
guard against a stray `{version}` placeholder, so a partial re-land
cannot slip half of this back in.
|
||
|
|
9989c756e2 | fix(web): reposition Settings autosave feedback (#7322) | ||
|
|
6f6da7d80b | fix web campaign provider logos (#7326) | ||
|
|
edfa6b5f44 |
fix(desktop): retry empty Chromium captures in PPTX layered export (#7337)
* fix(desktop): retry empty Chromium captures in PPTX layered export Software GL and Xvfb can return a fully transparent Page.captureScreenshot for CSS filter layers. The merge-queue flake compared that empty reference to a correct navy export. Retry CDP shots until the PNG has paint, then fail with a transparent-capture error instead of a huge meanChannelDelta. * fix(desktop): treat any visible PNG alpha as painted Empty Chromium frames are all-zero alpha. Counting only alpha>=16 as paint rejected legitimate low-opacity layered captures. Scan BGRA for any alpha>0, and cover the contract with bitmap plus real PNG probes. |
||
|
|
f1a73f0d89 |
feat(od-next): structure-only layout primitives for the prototype task profile (#7327)
* feat(od-next): ship structure-only layout primitives with the prototype task profile Blind evals and the pet-grooming runs showed the same layout defects over and over — stacked spans that never became blocks, chrome height constants reused as padding, authored copy crammed until it wrapped — while the prose rules that forbid them sat unchanged in the prompt. The rules are now backed by something the Build can copy instead of re-derive: - `assets/task-profiles/prototype/layout.css`: a ~50-line, structure-only stylesheet (`@layer od-layout`, `:where()`, `od-` prefix) for stacked information, truncation/clamp of data text, media ratio, rails, and an optional screen skeleton. Declared as a prototype resource, staged into `.od-frames/` under the existing ownership manifest, and quoted into the stable request context as the `layout-primitives` fact on every prototype run. - Prototype rule card 2.2.0: a "Variable-length text and stacked information" section that separates authored copy (rewritten to a budget, never truncated) from data text (truncate/clamp in lists, wrap in full on confirmation and detail screens), keyed to the real class names; the quality floor's layout bullet now points at the primitives. - Core 2.1.0: the variable-length-text paragraph rewritten as actions (three fates before the text is written) instead of prohibitions. - Run-finish observation `od_next_layout_primitives` (verbatim / modified / linked / absent) so the rollout can tell whether a model tier reproduces the block faithfully enough for inlining. * fix(od-next): let aspect-ratio win on .od-media and pin two rule-card gaps The first verification runs exposed one primitive bug and two recurring model slips: `.od-media` lacked `height: auto`, so an `<img height=…>` attribute kept its pixel height and the ratio never applied (tall cropped thumbnails); cards built on `<a>` shipped with browser-default link styling; and product tokens declared on an inner wrapper left sheet/dialog overlays mounted in the shell's screen without a surface. The stylesheet and the rule card now say so. * fix(od-next): keep .od-media at its intrinsic ratio; make cropping an explicit opt-in Review: the fallback aspect-ratio of 1 plus an unconditional object-fit: cover turned every unset .od-media into a square crop — contradicting the core image-geometry rule (content-bearing images render their full frame; cover is reserved for deliberate crops) and the primitive's own comment, exactly on the media the rule card points at product shots. The primitive now defaults to aspect-ratio: var(--od-ratio, auto) with no object-fit; a uniform tile crop sets --od-ratio and adds .od-media-cover together, and the commerce-card row of the rule card says so. * fix(od-next): carry finish-time observations on run_finished, not the frozen baseProps Review: od_next_device_platform*/od_next_layout_primitives were spread into baseProps, which is built at run creation — before any deliverable exists — and run_finished only spreads that frozen object. The daemon log printed the observations while the analytics event never carried them, which defeats the rollout signal these fields exist for. They now join finishedProperties, read live from the run after the physical run resolves. (The device-shell fields shipped with the same trap in the shells PR; fixed here on the same lines.) --------- Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com> |
||
|
|
24a10d143c |
feat(landing): focus Individual Unlimited campaign models (#7320)
Co-authored-by: hazel-ye <80680199+hazel-ye@users.noreply.github.com> |
||
|
|
fab173f7c1 |
fix(landing): mark the Go plan as sold out (#7352)
Disable new Go checkouts on the public pricing page and show a localized sold-out CTA. Existing Go subscribers still see current-plan actions. Co-authored-by: mrcfps <mrc@powerformer.com> |
||
|
|
1aa7327c95 |
fix(web): stop the Website-clone scaffold from following the user between tabs (#7344)
Website clone is the only create chip that writes into the composer: an empty composer gets a localized "clone this site:" scaffold, because the scenario is meaningless without a target URL. Every other create chip is a pure mode switch that preserves whatever draft is already there. Those two rules are each right, and they collide. Once written, the scaffold is indistinguishable from something the user typed, so the draft-preserving rule faithfully carries it into Slide deck, Image, Document, HyperFrames and Audio — tabs where "Website URL to clone:" means nothing. It only ever happens once, because after the first write the composer is never empty again and the seed condition stops firing; that one stale line then rides along until the user deletes it by hand. Give the scaffold an identity so the composer can tell it apart. Leaving the chip hands back the empty state the scaffold replaced; the moment the user types into it, it stops being ours and the draft-preserving rule takes over. Re-picking Website clone writes no seed (the composer is not empty), so that pass must not be mistaken for the scaffold having been released — otherwise a double click orphans it and the leak returns. The remembered value is the exact string written rather than a fresh `t()` lookup, so switching locale between seeding and leaving cannot strand a scaffold the check no longer recognizes. Also translate the scaffold into the 15 locales still shipping the raw English fallback. This key is unusual: most copy is shown to the user, but this one is typed into their composer and becomes the opening line of the prompt they send — an English fallback here puts English inside a Japanese user's own message. |
||
|
|
d03bf69d58 |
feat(standalone): complete Terminal exact lifecycle (#7319)
* feat(standalone): bootstrap terminal exact lifecycle * feat(standalone): complete terminal exact lifecycle * fix(ci): build standalone during postinstall * fix(standalone): repair exact lifecycle recovery Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): make exact updates replay-safe Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(terminal): enforce replay-safe exact carrier Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): align active update replay state Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(standalone): preserve exact channel lifecycle Generated-By: looper 0.11.2 (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com> |
||
|
|
7bdf9978aa | fix(web): keep account menu open after avatar click (#7330) | ||
|
|
ba3e8ef2c1 |
fix(runtimes): pass Antigravity prompt via -p argument (#7205)
* fix(runtimes): pass Antigravity prompt via -p argument agy -p - treats "-" as the literal prompt and ignores stdin, so print mode never receives the user message. * chore: retrigger CI after flaky workspace-restoration P0 * chore: retrigger CI after repeated workspace-restoration flake |
||
|
|
bc93dfe680 | feat(mcp): make stdio idle exit configurable (#7288) | ||
|
|
0918d79fbd |
fix(daemon): make an ACP stage-watchdog stall attributable (#7310)
* fix(daemon): make an ACP stage-watchdog stall attributable
An ACP turn that goes silent is ended by attachAcpSession's own stage
watchdog, which fails the turn and then kills the child. Two telemetry
fields that exist to explain that terminal were wrong:
- `last_progress_age_ms` is contractually "near the inactivity ceiling on
a stall". The daemon's ACP `send` wrapper stamped
run.lastAgentActivityAt for every emission, including the watchdog's own
error, so the daemon's timeout event became the last recorded progress
and a 30-minute silence reported 664ms.
- `terminal_trigger` was unset, so the run surfaced only the child's exit
code (AGENT_EXIT_130) - indistinguishable from a user interrupt.
Field case: run 14b04dd3-56b0-4d44-926b-db6cee3017ab (2026-07-28, OD
0.16.1, amr_cloud). 37.2 minutes, killed after ~30 minutes of silence,
reported last_progress_age_ms=664, no terminal_trigger. Triage read that
as "still working when killed" and chased the wrong window.
* fix(daemon): exclude host-synthesized ACP tool pairs from the progress clock
The previous commit stopped the ACP bridge's terminal `error` from stamping
`run.lastAgentActivityAt`, but that is only half the failure path. Every ACP
fatal path calls `flushOpenAcpTools(true)` *before* `send('error')`, and that
helper emits a synthetic `tool_use` + errored `tool_result` pair for each tool
the agent left open. Those ride the ordinary `agent` channel, so they stamped
the progress clock microseconds before the run finalized — a stall that died
with a tool in flight still reported a near-zero `last_progress_age_ms`.
That is the common stall shape, not an edge case: the new regression fixture
(open tool, then a silent stage timeout) reported 605ms against a 2000ms
silence, which is the same magnitude as the field incident's 664ms.
Carry the provenance out-of-band instead of in the payload: `send` takes an
optional `AcpEmissionMeta`, `flushOpenAcpTools` tags its pairs
`hostSynthesized`, and `runtimeEmissionCountsAsAgentProgress` excludes them
alongside the terminal error. The transcript, SSE wire, and Langfuse metadata
are unchanged — the pairs are still emitted, they just no longer count as the
agent making progress.
* test(daemon): name the emission that re-stamps the ACP stall progress clock
The progress-age assertion is green locally and red on CI, and a bare
"expected 16 to be >= 1600" cannot say which emission moved the clock.
Attach the run_finished summary and the persisted run event tail to the
failure so the next CI run identifies the culprit directly.
* fix(daemon): stop the progress clock when the daemon gives up on the turn
CI proved the previous two fixes were still partial. With the failure path's
synthetic tool pairs suppressed, an ACP stall on CI still reported
`last_progress_age_ms` of 17ms and 12ms, and the persisted event tail named the
culprit:
... agent:text_delta, error, agent:diagnostic, diagnostic:runtime_close
Emissions keep arriving AFTER the terminal error. The child's shutdown line on
stderr (`child.stderr.on('data')` stamps every byte), and the diagnostic
promoted from it, both land after the daemon has already decided to kill the
turn — and each one re-stamped the clock, so the reported age measured our own
teardown instead of the agent's silence. That ordering is timing-dependent,
which is why these specs were green locally and red on CI.
Freeze the clock at the verdict instead of hunting emissions one at a time.
`noteAgentActivity` stops advancing `run.lastAgentActivityAt` once this attempt
has a terminal verdict; everything else it does (tool-token TTL, watchdog
re-arm) is unchanged. The freeze is attempt-scoped: retries and the
resume-failed reseed re-enter `startChatRun` with a fresh closure, so the next
attempt measures its own silence. User cancel returns before the freeze and
keeps its own terminal path.
The flushed-tool marker is still required and not redundant: `fail()` flushes
open tools BEFORE it sends the error, so those pairs precede the freeze.
Adds a third regression case: the agent logs to stderr while shutting down.
Without the freeze it reports ~400ms against a 2000ms silence, deterministically.
* docs(daemon): tie the progress-clock predicate and freeze into one rule
* fix(daemon): let the ACP verdict own the attempt's terminal attribution
`freezeProgressClock` stopped the timestamp but left the outer chat inactivity
watchdog armed. `attachAcpSession.fail()` issues one direct SIGTERM and nothing
escalates it, so a child that lingers past the inactivity ceiling lets
`failForInactivity` fire on a run it does not yet consider terminal — the close
handler is what terminalizes — overwriting `terminal_trigger` with
`inactivity_watchdog` and emitting a second failure. The stall then reads as the
wrong clock, which is exactly the confusion `acp_stage_timeout` exists to
remove.
Red spec (a fixture that swallows SIGTERM, with the outer ceiling set just above
the stage timeout) reproduced it deterministically:
AssertionError: expected 'inactivity_watchdog' to be 'acp_stage_timeout'
run event tail: [... "agent:text_delta", "error", "error",
"diagnostic:runtime_close", ...]
Two `error` events, and the run finished under AGENT_EXECUTION_FAILED instead of
the child's own terminal code.
`retireAttemptOnAcpVerdict` now hands the attempt to the close handler under the
verdict the ACP bridge already reached: freeze the clock, retire the outer
watchdog, and escalate the teardown. The escalation is the necessary other half
— retiring the watchdog alone would leave a SIGTERM-ignoring child with nothing
to reap it. `scheduleForcedChildShutdown` captures this attempt's child, so a
retry that swaps `run.child` inside the grace window is unaffected, and user
cancel still returns before any of this.
* fix(daemon): stop every clock of an attempt that already has a verdict
Retiring the outer watchdog at the ACP verdict was not enough on its own.
`noteAgentActivity` only skipped the timestamp when frozen and still fell
through to the timer block, which clears and re-arms `inactivityTimer` — and
the raw `child.stderr` handler routes every late byte through that helper. So a
child that logs its shutdown line while ignoring SIGTERM re-armed the very
watchdog `retireAttemptOnAcpVerdict` had just cleared, and that timer firing
before forced shutdown reaped the child terminalized the run a second time.
The two existing fixtures missed it: one ignores SIGTERM without logging, the
other logs and exits. The gap is a child that does both. With the fixture logging
once (as a real CLI announces shutdown) rather than per signal — repeating it
would keep pushing the timers out and hide the race:
AssertionError: expected 'inactivity_watchdog' to be 'acp_stage_timeout'
run event tail: [... "agent:text_delta", "error", "stderr", "error",
"diagnostic:runtime_close", ...]
`error, stderr, error` is the whole story: the late stderr between the two
verdicts is what re-armed the clock.
`noteAgentActivity` now returns as soon as the attempt is frozen, so neither the
timestamp, the tool-token TTL, nor the watchdog can be restarted by a child that
is already being torn down. The new case also asserts exactly one terminal
`error` reaches the transcript, which is the property the trigger field depends
on.
---------
Co-authored-by: lefarcen <ontf116@gmail.com>
|
||
|
|
fb26b5cb8a |
fix(runtimes): support the dsh release line users are actually served (#7339)
* fix(runtimes): support the dsh release line users are actually served Two days after #7153 widened the version policy from one release candidate to the `0.1.0-rc.N` line, upstream published `0.1.1-rc.1` and then `0.1.1-rc.2`, and made the latter the `latest` tag. Scoping to `0.1.0-rc.N` was still a pin, just a longer one: every user on what npm now serves is told their CLI is untested, our installer hands out a version two releases behind, and the connection component's peers do not resolve against the host they land on. That last one is not a warning. semver only lets a prerelease satisfy a range when some comparator carries the SAME major.minor.patch tuple AND a prerelease tag, so `>=0.1.0-rc.6` matches nothing on the 0.1.1 line — a verified `satisfies('0.1.1-rc.2', '>=0.1.0-rc.6') === false`, the same rule that made the original pin uninstallable. Move all three to the 0.1.x line: the policy accepts any `0.1.N`, release candidate or stable; both installers pin `0.1.1-rc.2` with the resolution window reset to just after its publish time; the peers gain a comparator for the new line. This will expire again. A comparator per prerelease line is what semver allows, so `0.1.2-rc.1` will not resolve until someone adds one, and nothing in the repo notices when upstream moves. The check that catches installer/policy drift only compares the two against each other. * ci: watch upstream dsh releases instead of waiting for a bug report Twice in one week `@deepseek-ai/dsh` published a new release line while this repo kept pinning the old one, and both times the detection mechanism was a person reporting that their install did not work. The check added with the first fix compares our installers against our agent def. Both are our own config, so it stays green precisely when they are equally out of date — which is the state that shipped, twice. Nothing here looks at the registry. Add a daily scheduled comparison against npm's `latest` that separates the two failures worth telling apart: `behind` means our installer hands out an older version than the registry serves, everything still works; `unsupported` means a user who has what npm serves is outside what we claim to support, which is an "untested" warning in Settings and peer ranges that do not resolve. The card names all three files that must move together, because forgetting the peer ranges is the one that breaks an install rather than warning about it. Standalone and outside the merge gate, like nix.yml and docker-image.yml: an upstream release is not a reason to fail someone's PR. The workflow runs `self-check` before touching the registry so a broken checker is loud rather than quietly in-sync. * fix(ci,runtimes): stop the drift watch losing what it exists to report Four defects from review, each of which would have made this quieter than having built nothing. The widened policy over-claimed. `/^0\.1\.\d+(?:-rc\.\d+)?$/` accepts `0.1.2-rc.1`, which the peer ranges cannot install — so the daemon would suppress the untested warning for a version whose companion fails to resolve, which is worse than the warning and silent. Narrow prerelease acceptance to the tuples and floors the peers actually carry; stable releases on the line need no comparator and stay accepted. This also retires an over-claim that predates the branch: `0.1.0-rc.2` and `rc.3` are published and below the `>=0.1.0-rc.6` floor. `e2e/tests/dsh-installer-version-policy.test.ts` now runs both authorities over a version matrix and fails when the def accepts anything the peers reject, so the two cannot drift apart again. Importing the script ran the CLI. Collecting the new test fetched the live registry — measured at 354ms with the verdict printed — and once drift existed would have attempted a real Feishu post before any assertion. Move the dispatch behind a direct-entry check and prove it with a test that stubs fetch and re-imports. Feishu answers a rejected webhook with HTTP 200 and a nonzero code, so treating every 2xx as delivered discards the alert and logs success doing it. Mirror the landing notifier's contract: code 0 or absent is delivered, 429/5xx/9499 retry, anything else raises. The workflow chose the landing webhook but signed with an unpaired secret, so a bot that verifies signatures would reject the card. Pair them the way the landing workflow does, and pin the mapping in the topology test. * test(e2e): check each dsh peer range on its own The compatibility guard scanned the whole manifest and merged every comparator into one tuple table, so a single updated peer answered for the other four. A bump that updates one range and forgets the rest — the realistic way this goes wrong — read as compatible, which is the partial state the guard exists to catch. Parse `peerDependencies`, keep the `@deepseek-ai/dsh-*` entries, and require every one of them to admit a version before calling it installable. `cordis` is excluded: it has its own version line and never moves with dsh. Two cases cover it: a fixture where one peer carries the new comparator and the others do not, and the floor itself, since a comparator for the right tuple still rejects a candidate below it. Verified against the real manifest by downgrading four of the five ranges, which reds the matrix with `expected [ '0.1.1-rc.1', '0.1.1-rc.2' ] to deeply equal []`. |
||
|
|
6ea03de5d3 |
fix(web): left-align onboarding Local CLI chips (#7338)
* fix(web): left-align onboarding Local CLI chips The Local Agent step's detected-CLI chips are flex rows that never set `justify-content`, so they inherit `center` from the global `button` primitive. Each chip is a fixed-width grid cell, so the icon+name group floats by however much slack that particular row has — measured live in Chrome, the icon's offset from the card edge ranged 114px to 158px across seven CLIs, and the two columns read ragged. Opt the chip out of the primitive's centering with an explicit `justify-content: flex-start`, which drops the spread to zero. The spec renders the real onboarding flow with `primitives.css` and `entry-layout.css` loaded in app order and asserts the computed `justify-content`, so it fails on the cascade rather than on the text of the stylesheet. * test(e2e): baseline the onboarding Local Agent CLI list The visual suite stops at the onboarding sign-in step, so the one screen whose whole job is to line up a two-column grid of detected CLIs had no pixel coverage. The nearest-sounding case, `visual-avatar-local-agent-list`, captures the avatar menu's agent list — a different component — and sat at 0px through this PR's alignment change, which is exactly how a gap like this stays invisible. |
||
|
|
f92c2d957c | fix(viewer): stabilize Electron HTML previews (#7336) | ||
|
|
fe94564a71 |
fix(daemon): stop sending Kimi 0.37+ an MCP transport it rejects (#7313)
Kimi Code CLI 0.37.0 replaced the stdio branch of its ACP `session/new`
MCP handler with a throw:
{"code":-32603,"message":"Internal error","data":{"details":
"ACP stdio MCP server open-design-live-artifacts does not declare
a runtime identity"}}
OD attaches a stdio MCP server (`open-design-live-artifacts`) to every
`mature-acp` runtime, so every Kimi run on 0.37+ failed at the handshake
— which is why the observed failure rate is 84–92% rather than flaky.
The message names a "runtime identity" but no accepted value exists.
Kimi validates `session/new` against a zod union whose stdio member
declares no `type` key, so an explicit `type: "stdio"` is stripped by
validation and the handler then sees no `type` and throws. Verified
against the published tarballs by driving a real ACP handshake: 0.35.0
and 0.36.1 accept a stdio entry with or without `type`; 0.37.0, 0.37.1,
0.37.2 and 0.38.0 reject both spellings. Only http/sse survive.
So the session now withholds stdio MCP servers from builds that reject
them, judged against the version the agent reports in its own
`initialize` result — the build that is actually about to parse
`session/new`, and therefore more current than any earlier `--version`
probe. Which builds reject is runtime-def data
(`acpStdioMcpRemovedInVersion`), following the `acpMcpEnvFormat`
precedent, so hermes / trae-cli / reasonix declare nothing and keep a
byte-identical payload.
Kimi 0.37+ runs lose live-artifact tooling, which is unreachable on
those builds either way; a session that opens without MCP beats one that
cannot open at all. 0.36.x keeps the capability.
Co-authored-by: lefarcen <ontf116@gmail.com>
|
||
|
|
d85e78ab7d |
fix(runtimes): reach a working agent CLI past a broken shim (#7153)
* fix(runtimes): reach a working agent CLI past a broken shim
Resolving an agent's name on PATH only proves a file exists there, never
that it runs. Detection stopped at the first hit, so a wrapper orphaned by
a half-finished `npm i -g` shadowed a perfectly good CLI of the same name
in a later search directory — and because the not-invocable branch dropped
the resolved path, the picker (which renders an unavailable agent only when
it carries one) hid the agent entirely. The user saw "scan found nothing"
with no way to act.
Detection now walks past candidates that cannot be executed, and reports
the path it tried when every candidate fails, so the row stays actionable.
Also on this chain:
- `@open-design/dsh-runtime` pinned its DeepSeek Harness peers to one exact
release candidate, so it became uninstallable the moment upstream shipped
the next one. Peers now take ranges. `scripts/guard.ts` gains a narrow,
logged exemption for external-host plugin manifests — their peers describe
a host this repo neither installs nor controls, which is the one case
where an exact pin is wrong rather than reproducible.
- The DSH installers pinned only the entry package while its ~190 siblings
used caret ranges over prerelease floors. Once a newer release candidate
existed, the transitive tree floated onto it and its mutually exclusive
peer sets sent npm into an ERESOLVE backtrack that never converged. Both
installers now freeze the resolution window alongside the pin.
- The rescan notice counted agents the list never renders, so it announced
"3 available" above two rows.
* fix(runtimes): make detection's chosen executable the one spawn sites use
The candidate walk taught detection to skip a binary it cannot execute, but
every other resolution — chat, the connection test, the memory summariser,
companion install — called `resolveAgentLaunch` with no skip list and got
back the first file that merely exists on PATH: the broken shim detection
had just rejected. Settings advertised the agent as installed while each
turn exec'd the wrapper, so the fix only ever reached the picker.
Detection is the only stage that spawns anything, so it is the only one that
learns which candidate works. It now publishes that winner, and resolution
prefers it over a fresh first-hit walk. An explicit `*_BIN` override and a
packaged built-in still outrank it, a winner whose file has since vanished
is ignored, and detection clears the entry before each pass so a rescan
after a repair never resolves against a stale one.
This restores the invariant the file already documented — detection probes
the exact path the runtime will spawn — which the walk had broken.
Reported by @mrcfps in review.
* fix(runtimes): keep the remembered executable inside the live candidate set
The winner map made resolution stop being a pure function of the current
environment. It only checked that the remembered file still existed, so a
path learned in a richer environment survived an emptied PATH, a sandboxed
OD_AGENT_HOME, and any other narrowing of the search — resurrecting a binary
the caller could no longer see.
That is not just a test artifact: `GET /api/integrations/vela/status` must
answer `amr-runtime-unavailable` when the vela binary cannot be resolved, and
it started answering "ready" instead, because an earlier detection in the
same process had remembered a path.
The memory now reorders candidates rather than introducing them: it applies
only when the live search already offers the remembered path. An emptied
PATH, a sandbox, and an uninstalled CLI all keep meaning "not found", while
a broken shim shadowing a working binary is still resolved to the winner.
This also settles the scope question raised in review — no separate cache key
is needed, because the candidate set already carries the resolution scope.
* fix(runtimes): treat a launcher whose target is gone as not invocable
A stale global npm wrapper on Windows is a `.CMD`, which OpenDesign runs
through `cmd.exe /d /s /c`. When the package behind it has been
uninstalled, cmd.exe starts, node starts, and only then does node fail to
load the script the wrapper names — a plain exit 1 with MODULE_NOT_FOUND
on stderr. None of the spawn-level signals the classifier looked for
(ENOENT / EACCES / ENOTDIR, exits 126 and 127) can occur along that
chain, so detection read the dead wrapper as a healthy CLI that merely
disliked `--version`, stopped the candidate walk on it, and never reached
the working `dsh.CMD` later on PATH.
Classify invocability from the launcher's own vocabulary as well: cmd.exe's
9009 (the Windows analogue of POSIX 127), node's missing-module report, and
cmd.exe / PowerShell's missing-command and missing-path reports. Exit status
alone still means nothing — a CLI that runs and exits non-zero is a real
answer from the right binary and keeps its place in the walk.
Drop the win32 early return from every case in the fallback suite and write
fixtures per platform, so `dsh.CMD` resolution through PATHEXT and the
cmd.exe wrapping are exercised natively. The orphaned-wrapper regression is
reproduced on every platform — the defect is a classification bug, not an OS
API one, and the daemon suite only runs on Linux in CI.
* fix(runtimes): carry forward the dead paths, not detection's winner
Detection published the candidate it settled on and resolution preferred
it over the first live PATH hit. That pinned the daemon to one binary for
its whole lifetime and let it silently outrank the caller's own PATH: a
CLI that became visible earlier afterwards — a fresh install, a version
manager swapping shims, a caller resolving under its own environment —
could no longer be reached.
The daemon suite caught it on a machine that has a real `vela` installed:
`chat-route` and `connection-test` inject a fake agent CLI ahead of PATH
per case, and the remembered winner outranked it, so 15 cases spawned the
machine's real CLI instead of the fixture. CI never saw it because its
runners have no such CLI to remember.
Carry the proven-dead paths forward instead. Skipping them keeps every
candidate still standing in plain PATH order, while chat, the connection
test, the memory summariser, and companion setup still avoid the shim
detection rejected — which is the whole point of publishing anything.
* docs(runtimes): say why detection keeps its own launcher-error list
`isCliNotInstalledText` in run-failure-classification.ts reads similar
output, and the next reader will reasonably ask why the two are not one.
They answer different questions: that one buckets a finished run for
telemetry, where over-matching costs a mislabelled event, while this one
decides whether to abandon a binary mid-resolution, where over-matching
sends a working CLI's user to a different install.
* fix(runtimes): accept the dsh release line, not one release candidate
This branch moved the installers to 0.1.0-rc.8 and left the agent def
naming 0.1.0-rc.6 as the only supported version, so following our own
one-line install produced "0.1.0-rc.8 has not been tested with this
OpenDesign build" in Settings on a clean machine. That is the same
pinning defect the installers had, one layer up.
`dsh` publishes a stream of release candidates whose sibling packages
peer-require each other, so the version our installer hands the user
moves with upstream. Version policies can now declare the release line
they accept alongside the exact versions exercised, and DeepSeek Harness
declares its own. A version off that line still warns — the point is to
stop pinning, not to stop checking.
Add a cross-boundary guard for the drift itself: the installers and the
agent def cannot import each other, so `e2e/tests` reads both and fails
when an installer pins a version the daemon would not accept.
* docs(contracts): correct what shim-broken now means
The reason's docstring said "exit 126/127", which stopped being the whole
story once detection learned to read a Windows wrapper's stderr: a `.cmd`
starts an interpreter successfully and reports the missing target there,
so no POSIX exit code appears at all.
* test(runtimes): cover the launcher signatures no fixture can produce
`launcherTargetMissing` recognises six ways a launcher says it never
reached its target, and until now two of them had any coverage — the two
a POSIX shell can print. cmd.exe's wording, PowerShell's, and exit 9009
were load-bearing for the Windows fix and untested anywhere.
A fixture cannot close that gap. POSIX masks exit statuses to eight bits,
so a shell asked for 9009 reports 49, and /bin/sh does not speak cmd.exe.
The daemon suite runs on Linux in CI, so driving the classifier directly
is the only way the merge gate can catch a typo in a signature the fix
depends on for the platform the bug came from.
Extract the catch arm verbatim as `classifyVersionProbeFailure` — same
branches, same order, no behaviour change — and feed it the rejection
shape `promisify(execFile)` actually produces. Verified by mutation:
deleting the 9009 branch reds one case, deleting the stderr branch reds
six.
The suite also pins the constraint from the review by name: exit 1 with an
ordinary argument complaint, with no stderr, or with wording that belongs
to run-failure telemetry ("not installed") all stay `spawned`. That last
one is why detection keeps a narrower list than `isCliNotInstalledText`.
|
||
|
|
18273c01b8 |
fix(packaged): recover from stale web sidecars (#7279)
* fix(packaged): recover from stale web sidecars * fix(packaged): stop stamped web owner before socket takeover Unlinking only the unresponsive web socket left the old Next process alive. Its later JsonIpcServer.close() could delete the replacement pathname. Stop the stamped owner through platform primitives first. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(packaged): strip handoff parent pid and skip unsafe web unlink Strip OD_TOOLS_DEV_PARENT_PID from the legacy payload desktop spawn environment so the replacement does not exit after waiting for the outer PID. Skip web socket unlink when a stamped owner is still running after stop. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(packaged): skip web unlink when process discovery fails listProcessSnapshots returns [] on enumeration failure, so an empty table is not proof that no stamped owner exists. Leave the live socket in place unless discovery succeeded and every matched owner is gone. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(daemon): hold parent monitor until handoff journal commits Packaged daemons now exit when the outer Electron dies. Desktop SHUTDOWN acks before asynchronously exiting, so the parent can disappear while armed handoff, attempt, and runtime writes are still pending. Hold the parent-death exit across that window. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(daemon): defer SHUTDOWN until handoff journal commits Packaged beforeShutdown still sends sidecar SHUTDOWN after desktop acks, and that path ignored the parent-monitor hold. Wait for the same release before exiting so handoff.json, attempts.json, and runtime.json can finish. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(daemon): defer SIGTERM until handoff journal commits Packaged closeManagedChild waits five seconds then stopProcesses() sends SIGTERM. That handler still exited immediately and could cut off the held journal writes. Route signal shutdown through the same hold as SHUTDOWN. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(daemon): hold parent monitor before payload spawn A crash after the replacement emits spawn but before the journal hold lets the parent monitor exit the daemon while handoff.json is still prepared. Acquire the hold before spawn and release it on failure. Generated-By: looper 0.12.0 (runner=fixer, agent=opencode) * fix(packaged): wait for deferred handoff shutdown before SIGKILL Daemon SHUTDOWN now reports deferred:true while the handoff journal hold is active. closeManagedChild then waits a bounded 30s for self-exit before stopProcesses, so SIGKILL cannot cut off the three sequential journal writes. A POSIX regression exercises the real stopProcesses path and checks all three files commit before forcedPids. |
||
|
|
7320f9b8ab |
feat(daemon): make OD Next the default design strategy (#7016)
* refactor(observability): isolate legacy run exporter
* refactor(daemon): extract internal run creation service
Route /api/runs and compatibility /api/chat through one daemon-owned create, claim,
resume, and start seam. Preserve HTTP responses, SSE timing, session bookkeeping,
and durable Run formats.
Validated with daemon tests/build/typecheck, workspace guard, and workspace typecheck.
* test(plugin-runtime): freeze OD Next strategy fixture boundaries
Add exact pre-Build route and stage cases plus deterministic deletion-boundary mutations without shipping an evaluation harness.
Preserve the current official and community fallback pipeline and composed verification/critique prompt sections as golden witnesses.
Validated with plugin-runtime tests and types, focused daemon prompt composition, daemon types, guard, and workspace typecheck.
* fix(daemon): finalize terminal telemetry delivery
Persist a versioned delivery record so normal failures stop after bounded retries
and only an unfinished crash window resumes on restart.
Reuse the persisted identity across Vela and Relay transport, isolate stable
direct Langfuse event ids by delivery purpose, and log only redacted sink data.
Validated with focused daemon and contracts tests, workspace typechecks, and
guard. The full daemon suite had one timeout; its four-test file passed alone.
* feat(plugins): add OD Next strategy scenario and contracts
Ship the V2 strategy package as inactive bundled content and validate its contracts without changing ordinary plugin manifest compatibility.
Keep bundled strategy bytes out of the catalog and generic apply path until a later hash-gated activation owner binds them.
* feat(observability): add normalized agent observation contract
Define a pure versioned contract for identity, Prompt, usage, timing, and status evidence with explicit availability and per-value provenance.
Add a structured main-Run compatibility adapter that preserves unknown facts, separates clocks, and omits provenance-ambiguous derived totals without changing legacy export behavior.
* feat(plugins): bind OD Next strategy content identity
* feat(daemon): persist OD Next strategy task chains
Add the daemon-owned SQLite truth for immutable strategy task identity, ordered physical Run mappings, canonical Plan Contracts, revision-CAS transitions, and terminal reconciliation.
Pin task-owned Snapshots and exclude their durable references from pruning so mixed GC sweeps remain safe.
Validated with 103 focused daemon tests, contracts tests/typecheck, daemon and workspace typechecks, and guard.
* feat(daemon): gate OD Next runtime capabilities by fixtures
Add exact CLI/adapter/fixture tuple resolution, stable capability snapshots, and offline
replay graph invariants for the four initial runtime paths.
Keep production registry empty until sanitized real fixtures exist; synthetic evidence and
version drift remain unknown.
Validated with contracts tests, focused daemon and e2e boundary suites, guard, and
workspace typecheck.
* feat(daemon): add hash-gated OD Next prompt recipe
Compose the bundled strategy from validated content, one Task Skill, and the exact stage policy.
Fail closed on package, profile, pipeline, or atom drift while preserving ordinary quality paths.
Validated with focused tests, package typechecks, workspace typecheck, and guard.
* feat(observability): aggregate OD Next task observations
Build stable task, Run, Child, model, and tool hierarchies from durable StrategyTaskExecution mappings and normalized facts while keeping missing coverage explicit.
Map task aggregates to legacy ingestion behind the consent gate. Preserve owner/exclude hierarchy while accounting usage once, enforce kind-specific Prompt boundaries, and accept absolute time only from one Unix-epoch evidence record.
Validated with 228 focused daemon tests, 353 contracts tests, daemon/contracts and workspace typechecks, guard, and the task harness.
* feat(daemon): collect bounded Codex child evidence
Read declared Codex rollout sessions without scanning arbitrary homes.
Normalize verified child facts while redacting prompts and preserving Run semantics.
* feat(daemon): add OD Next planning coordinator
Add a daemon-owned planning reducer, deterministic Resolver and Preflight facts, and a stream-safe machine protocol boundary backed by the durable task store.
Lock one clarification and one serialization-only repair across restart, preserve the Plan hash, and keep ordinary Run text unchanged.
* feat(daemon): collect native OpenCode child evidence
* feat(observability): add Langfuse OTLP task exporter
Map normalized task observations to complete OTLP/JSON v4 spans with stable parent context, allowlisted metadata, usage accounting, and bounded terminal delivery.
Fail closed on drift across root and operation input, model, timing, type, hierarchy, status, and complete usage provenance while dual remains a local-only OTLP shadow.
Keep legacy rollback, consent gates, and X5 live self-host verification boundaries intact.
* feat(daemon): normalize Claude native child evidence
Collect matched Claude Task sidechain lifecycle facts without changing main stream turn, stdin, or error bookkeeping, and adapt them to provider-neutral observations with unavailable Prompt and usage evidence.
Bind session, Task id, and parent identity immutably; poison rebinding, session drift, and conflicting terminals so ambiguous evidence cannot reach L2. Keep production capability unknown until X1 supplies an exact-version sanitized real fixture.
* feat(daemon): consume versioned Vela child evidence
Negotiate schema v1 only on AMR and enforce run-scoped start-to-terminal graph, status/source coherence, and exact usage source/completeness bindings. Emit allowlisted Normalized child facts without Vela private I/O.
Pin the unpublished producer golden for offline compatibility while keeping production capability unknown until the X1/X3 evidence gates are satisfied.
Validated with 147 focused daemon tests, 353 contract tests, daemon/workspace typechecks, guard, and the task harness.
* feat(daemon): automate OD Next simple production
Connect parsed OD Next terminal output to the durable coordinator, claim clarification, repair, and production Runs atomically, and start continuations only after the source Run ends.
Converge task failure and cancellation before physical end, require daemon-owned execution facts and canonical delivery, and keep ordinary Runs compatible.
Validated with focused daemon route and coordinator tests, contracts tests, daemon and workspace typechecks, guard, and the task harness.
* feat(daemon): enforce verified complex production
* feat(web): follow OD Next task chains across Web and CLI
Persist daemon-issued task handles and visible predecessor-prefix boundaries, then follow projected active Runs through one terminal outcome in Web and od run.
Normalize reloads to daemon activeRunId before subscribing, transfer controller ownership atomically across successors, discard stale cursors, and preserve visible prefixes without replaying predecessor machine payloads.
Validated with focused Web and daemon tests, contracts tests, workspace typecheck, guard, diff checks, and the task harness.
* feat(daemon): roll out OD Next task observability
* feat(daemon): add controlled OD Next rollout
* fix(daemon): align OpenCode and Vela evidence tuples
Pass GPT-5.6 Sol's verified high variant to OpenCode, pin native evidence to 1.18.18, retain locally recorded success and failure seeds as explicitly unattested, and align the Vela consumer golden to the local versioned producer.
Validated with 5 focused daemon files (175 tests), daemon and workspace typechecks, guard, the real local Vela ACP Sol smoke, and a minimal test-relay telemetry write.
* fix(daemon): honor OpenCode model variant capabilities
Parse the live verbose OpenCode catalog per model and forward only variants advertised by the exact detected binary. Keep fallback and one-off OPENCODE_BIN connection tests on the base model when variant support is unknown.
Record machine-checkable OpenCode 1.18.18 root, tool, child, recovery, cancel, timeout, and resume evidence without promoting the unattested seed into the production capability registry.
Validated with focused runtime and connection tests, host process-group and watchdog tests, contracts tests, daemon/workspace typechecks, guard, and an isolated SIGKILL delivery-recovery exercise.
* feat(daemon): add OD Next analytics operations report
Reduce normalized task aggregates into source-aware operations metrics without
copying the Langfuse dashboard or exposing user trace links.
Preserve main/Child accounting, stage-scoped coverage, unavailable facts, and
external-signal-only quality inputs.
Validated with focused and contract tests, the full daemon suite, guard,
workspace typecheck, and exact-trace Langfuse readback.
* feat(observability): complete child prompt and runtime provenance
Export bounded redacted child-injected Prompt content and exact CLI, companion, and
adapter versions through task-level legacy and OTLP mappings.
Register the OpenCode 1.18.18 best-effort replay tuple while keeping unknown versions
and incomplete Codex evidence fail closed.
Validated with 353 contracts tests, 196 focused daemon tests, daemon/workspace
typechecks, and guard.
* fix(daemon): recognize Codex child failure terminals
Parse Codex 0.147.0 Child task_complete.error as the runtime-owned failed terminal, honor session_meta id over its legacy root session_id, and map interrupt_agent aborts to canceled.
Add a redacted seven-path best-effort replay and exact Codex capability tuple. Unknown versions remain fail-closed and ordinary Codex runs are unchanged.
Validated with 92 focused daemon tests, 353 contracts tests, daemon and workspace typechecks, guard, and the 25-unit task harness.
* fix(daemon): report verified child runtime versions
Use versions observed from resolved CLI probes or Vela's ACP handshake when annotating Child observations. Correct Native OpenCode's allowlist key, add Claude and Vela/OpenCode companion provenance, reject Vela probe/handshake disagreement, and ignore the legacy 0.0.0 placeholder.
Validated with 252 focused daemon tests, daemon and workspace typechecks, guard, the task harness, and a real local Vela --version/-v/ACP initialize build smoke.
* test(observability): record Vela child evidence matrix
Add a sanitized provider-backed Terra replay for main, tool, child success, child failure with parent recovery, cancel, timeout, and cross-process resume. Keep the unpublished Vela tuple outside the production registry while pinning its exact candidate and OpenCode companion evidence.
Validated with focused Vela consumer/capability tests, daemon typecheck, workspace typecheck, guard, and the task harness.
* feat(daemon): enable OD Next production profiles
Make prototype, presentation, marketing, and HyperFrames the exact global OD Next production set while preserving explicit off/observe, user plugin authority, and fail-closed runtime capability gates. Feed runtime-owned profile, route, output, input, and capability facts into the planning contract and keep all other artifact kinds on ordinary Open Design.
Use bounded executable-scoped CLI version probes so headless and restarted daemons can resolve exact tuples without a prior agent-list request or cross-binary cache contamination.
Validated with four provider-backed Terra CLI chains, focused daemon/contracts/plugin-runtime suites, daemon and workspace typechecks, guard, and the 25-unit task harness.
* feat(daemon): classify OD Next exact agent inputs
Route prompt contributors through one daemon-owned choke point and version their final-text, semantic-fact, transport, and exclusion ownership for the forthcoming canonical bundle. Send OD Next continuation stages directly through native sessions without rewrapping or re-seeding stable instructions.
Validated with focused daemon and contracts tests, daemon/workspace typechecks, and guard.
* fix(daemon): close OD Next input ownership gaps
Bind semantic request facts to their real daemon-system or request producers, cover the DSH profile exact-text transport, and reject lookalike prompt-bundle root names.
Validated with focused exact-input, production-server and DSH tests, daemon typecheck, task harness, and guard.
* feat(daemon): emit canonical OD Next prompt bundles
Add strict pure TypeScript Bundle and request-Turn serializers, then route OD Next request and continuation text through their versioned XML contracts.
Split Web prior transcript from the current turn without text subtraction, keep mutable Skill and attachment inputs explicitly pending, and preserve ordinary prompt behavior.
Validated with focused contracts, daemon and web tests, the real server fixture, package and workspace typechecks, and guard.
* fix(daemon): align OD Next bundle input ownership
Register the real canonical Bundle contributors and keep current-turn, prior-transcript, pending-fact, and research-query ownership explicit at the production seam.
Reject continuation task_run_index zero and cover Web framing, ordinary compatibility, fail-closed contributor drift, and real server prompt capture.
* feat(daemon): freeze OD Next user skills
Capture explicit Skill bodies and audited side files before task claim, persist verified task-owned bytes, and replay only the frozen package for OD Next runs.
Keep legacy single-Skill CLI requests and ordinary live staging compatible while adding Web/CLI multi-Skill coverage and fail-closed snapshot checks. Validated with focused server, contract, CLI, Web, typecheck, and guard suites.
* fix(daemon): harden OD Next skill snapshots
Require an explicit frozen package for every mapped OD Next task and fail corrupt task state before any live Skill staging.
Use bounded no-follow descriptor reads with race checks, preserve legacy CLI compatibility, and make materialized roots collision-resistant. Validated with focused, real-server, workspace typecheck, and guard suites.
* feat(daemon): freeze OD Next task input snapshots
Freeze OD Next attachments and canonical configuration before the atomic run/task claim, then route runtime access through durable task-scoped snapshots while preserving ordinary attachment behavior.
Validate immutable identities, transport references, continuation propagation, tamper failure, and the four production task types with focused contract and daemon coverage.
* fix(daemon): isolate OD Next runtime input projections
Keep canonical task snapshots daemon-only, rebuild a read-only per-Run projection, and deliver frozen document resources through ACP or OD_TASK_INPUT_DIR.
Harden source and snapshot reads with no-follow same-fd identity checks and bounded caps. Validated with focused runtime tests, contracts, daemon/workspace typechecks, guard, and the task harness.
* fix(daemon): harden OD Next snapshot transport boundaries
Reject intermediate symlinks and realpath escapes while loading canonical snapshot files, including identical-byte replacement targets.
Preserve ordinary ACP duplicate image ordering while suppressing only OD Next resource/image cross-duplicates. Validated with focused tests, daemon typecheck, guard, and the task harness.
* fix(daemon): preserve structured OD Next snapshot failures
Defer frozen Skill package errors from the Task 04 preclaim lookup to the existing post-claim failure boundary so mapped Runs remain durable JSON failures without orphan input snapshots.
Validated with the combined real-server repair, missing-package, and tampered-package regressions.
* fix(daemon): keep OD Next continuation cwd stable
Separate authoritative project cwd resolution from best-effort file inventory and tolerate entries deleted between directory enumeration and stat.
This keeps native-session identity stable while frozen task inputs replace live attachments during continuation.
* feat(daemon): persist exact OD Next prompt inputs
Persist the canonical first-turn Bundle and every deterministic continuation Turn with UTF-8 and SHA-256 identities, then send only those validated texts through runtime transports.
Fail closed on mapped task drift, tamper, legacy NULL identities, missing mappings, and cold continuation while leaving ordinary Runs on the existing live composer.
Validated with the 15-case automatic server acceptance twice, 223 focused daemon tests, daemon typecheck, guard, and the task harness.
* feat(daemon): observe exact OD Next prompt inputs
* feat(daemon): bind and observe Claude native subagents
Register the replay-verified Claude 2.1.233 tuple, bind complex Build Packages to daemon-issued native Agent handles, and derive completion only from durable child lifecycle evidence.
Export bounded parent-tool, Child Agent, and Child-tool spans through the existing legacy and OTLP task hierarchy without exposing raw tool inputs, outputs, provider ids, paths, or credentials.
Validated with focused contracts and daemon suites, the 17-case real-server chain, workspace typecheck, guard, and the 26-unit task harness.
* fix(daemon): repair OD Next rollout authority and controls
* fix(daemon): harden OD Next public canaries
* refactor(daemon): extract OD Next prompt preparation
* test(daemon): align plugin fixture with project schema
* test(e2e): assert daemon-owned prototype routing
* fix(daemon): restore OD Next task observability
Default eligible OD Next tasks to privacy-aware Langfuse hierarchy delivery through relay or direct sinks, with a versioned safe quality projection.
Persist lightweight representation ownership before suppressing mapped Runs, retain best-effort startup retry semantics, and preserve single-Run compatibility when Task delivery is not eligible.
Adopt the active payload relay for historical packaged launchers and cover the real sidecar environment handoff.
Validated with the full daemon suite, focused contracts/packaged/e2e/tools-pack suites, workspace guard, and workspace typecheck.
* fix(daemon): make OD Next capability fallback invisible
Admit capability-certified runtime adapters across CLI version drift while keeping recorded fixture versions as auditable evidence and reserving a narrow incompatibility denylist.
Fall back before Run claim on preparation failures, keep continuation bound to the frozen capability snapshot, and report truthful child coverage across Langfuse legacy and OTLP without replaying an active task.
Validated with focused contracts and daemon suites, the automatic server suite, pnpm guard, pnpm typecheck, daemon typecheck, LSP diagnostics, and git diff --check.
* fix(strategy): isolate OD Next automatic task routing
* fix(daemon): persist blocked strategy-task attribution
Every blocked strategy task now stores its gate reason codes and the
agent-visible turn text (blocked_reason_codes_json / blocked_visible_text)
so operators can diagnose a blocked task from the store alone. Covers the
coordinator blockTask gate, the automatic-continuation fail-closed path,
and startup run-terminal reconciliation.
* fix(daemon): accept form-only clarification turns by deterministic inference
A first Full-Plan request turn that renders exactly one question form and no
machine-protocol block has exactly one valid protocol meaning: outcome
clarification_required with an unlocked execution mode. The coordinator now
recovers that state deterministically instead of dead-ending the task as
blocked, stamps od_next_protocol_runtime_state_inferred into the accepted
state's reasonCodes for attribution, and keeps every ambiguous shape (extra
forms, recovered plan blocks, later stages, spent clarification budget,
any other protocol issue) fail-closed. This closes the observed field
failure where the agent's first reply produced a discovery form, the task
went blocked, and the user's form answer then failed with 409
STRATEGY_TASK_STATE_MISMATCH.
* feat(contracts,daemon): project blocked strategy-task attribution to clients
A task the OD Next protocol gate settles as blocked persists its
attribution (reasonCodes + agent-visible text) since
|
||
|
|
e34d823160 |
fix(web): gate PPTX export on a daemon-advertised renderer capability (#7224)
* fix(web): gate PPTX export on a daemon-advertised renderer capability
The export entry was offered whenever the artifact looked like a deck, so
in a deployment without an off-screen renderer (headless / container) the
button was always shown and always failed: `POST /export/pptx` answers 501
whenever `desktopSlideRenderer` is absent, and the check sits *before* the
editable/screenshot branch, so the mode picker in the modal was a choice
that could not affect the outcome either.
Ask the daemon instead of guessing. `/api/version` now carries an optional
`capabilities.slideRenderer`, computed from the very binding the export
routes guard on, so the advertisement cannot drift from the 501 they
return. It rides on the version payload because that is already the
"what runtime am I talking to" channel (packaged/channel/platform/arch),
already fetched once at boot, and — unlike `/api/app-config` — never
persisted: the same data dir can be opened by a desktop daemon today and a
headless one tomorrow, so a stored answer would be a stale claim.
The flag deliberately cannot be derived from `packaged`: a packaged binary
started as a plain daemon has no renderer.
Mixed versions are the delicate part. The field is optional and the client
treats absent as *unknown*, not false, so a newer web bundle keeps showing
PPTX against an older daemon that supports it but does not advertise it
yet. Only an explicit `false` hides the entry. A present-but-malformed
block is dropped rather than trusted, and an unresolved answer stays
retryable so one early fetch against a not-yet-ready daemon cannot pin
"unknown" for the session.
Client-side `isOpenDesignHostAvailable()` was not used for this even though
it is the existing desktop-only gate: it reports whether the web UI runs
inside the Electron shell, which is wrong in both directions here — a
browser pointed at a desktop-hosted daemon does have a renderer, and a
packaged daemon may not.
Also splits the two states that `unavailable: true` collapsed. A dead
daemon and a runtime with no renderer both produced "PPTX export is not
available here", which sends someone whose daemon is down to look at the
wrong problem. Callers keep the existing `'unavailable' in res` fallback
behaviour; only the message differs.
* test(web): pin the two unavailable reasons in the export specs
The three specs that asserted the old flat `{ ok: false, unavailable: true }`
now pin which reason they exercise: the two 501 cases as `no-renderer` and
the transport failure as `unreachable`. Without this the split is only
enforced by types, so a later edit could collapse both back onto one value
and every assertion would still pass.
* fix(web): let a late capability answer reach an already-mounted viewer
Review catch (thanks @nettee): the first shape probed once on mount and
ignored an unresolved result, so a probe that raced daemon startup or
blipped left `available` at null for the life of that mount. The gate then
stayed permissive and the entry could still be clicked into the very 501
this change exists to prevent. Worse, the module raced the app's own boot
fetch with a second request, so a failed probe here could keep the entry
showing even when the boot response had already reported `false`.
Three changes, all aimed at that gap:
- The boot fetch now feeds the cache. `fetchAppVersionInfo()` hands its
parsed payload to `recordAppVersionInfo()`, so the common path costs no
extra request and the two paths cannot disagree. The dependency runs
registry -> runtime-capabilities only, so there is no cycle; this module
no longer imports the registry and does its own minimal probe when it
still has to.
- Consumers subscribe. An answer arriving from boot, from a retry, or from
another mounted consumer now reaches every mounted viewer instead of
leaving each one pinned on its own failed probe.
- Unresolved probes retry with a bounded backoff (500ms / 1.5s / 4s) and
then stop. An unreachable daemon is its own visible problem and should
not be polled forever; a startup race is exactly what needs one more look.
Covered at the level the review asked for: a FileViewer spec where the
first probe cannot resolve, the entry is still offered (unknown means
assume available), and a later explicit `false` removes it. Verified red by
restoring the old single-shot behaviour — the late answer reaches nobody
and the entry stays.
* fix(web): recognise proxy-answered daemon outages as unreachable
Review catch (thanks @nettee): the daemon-down diagnostic did not actually
fire on the packaged / sidecar path, which is the common one. The proxies
in front of the daemon do not reject `fetch` when it is down — they answer.
The web sidecar ends a connection error as a plain-text 502, and Next's dev
rewrite as a plain-text 500 when OD_PORT points at a dead port. Both landed
in the generic `!resp.ok` branch and surfaced as "export request failed
(502)", so the new message this PR added never appeared where it was most
needed.
`state/projects.ts` already had exactly the right detector for this, kept
private. Extracted it to `runtime/daemon-proxy-failure.ts` and used it from
both places rather than growing a second copy.
Kept deliberately narrow: only a 500/502 that is `text/plain` AND carries a
connection errno counts. Widening to any 5xx would relabel genuine upstream
failures as an outage — the same misleading move in the opposite direction,
which is why the original detector was written this way. A JSON 502 stays a
semantic error, and there is a spec pinning that.
* fix(web): honour the unknown contract on both capability cache edges
Two compatibility edges from review (thanks @nettee), both of which broke
the same rule this PR is built on: absent or malformed means unknown, and
unknown must keep the entry visible.
- A resolved `false` outlived the daemon that gave it. If a page cached
`false` from a headless daemon and then talked to one that stops
advertising the field — a restart or a downgrade — the stale `false` kept
hiding the entry against a daemon we are supposed to know nothing about.
A fresh, valid response with no capability now drops the cache back to
unknown and tells mounted consumers, so listeners take `boolean | null`.
- The module's own probe trusted the capability without validating the
envelope it arrived in, so `{version:{capabilities:{slideRenderer:false}}}`
could publish `false` and hide the entry. That was self-inflicted: the
validation went missing when this module stopped importing the registry to
avoid a cycle, and nothing replaced it.
`isAppVersionInfo` now lives here and the registry imports it, so there is
one validator instead of two and the dependency still runs one way only.
Regression specs for both: `false` followed by an absent field returns to
unknown, and a capability in a malformed envelope is refused.
* fix(web): subscribe mounted viewers even when the cache is already resolved
Follow-on from the invalidation added in the previous commit, and the same
mistake one layer out: a viewer that mounted while the answer was already
`false` took the early-return path and never registered a listener, so when
a later valid response omitted the capability and invalidated the cache to
unknown, that viewer never heard it. Its entry stayed hidden against a
daemon the compatibility contract says we know nothing about — exactly the
bug the invalidation was added to prevent, just moved from the cache to the
consumer.
Subscribe unconditionally and only skip the probe when the cache is already
resolved. Spec covers the mounted case the loader-level tests could not:
render with `false` cached and the entry absent, invalidate, entry returns.
Verified red by restoring the early return.
* refactor(web): collapse the capability store into one atom with one writer
Four review rounds found six compatibility edges in this module, all the
same shape: `cached`, `inFlight` and the listener set were separate mutable
pieces, and each fix exposed another pair that could disagree — an
invalidation that forgot the in-flight promise, a validation path that
forgot the invalidation, a subscription that forgot to register. Patching
the seventh instance was not going to be different from the sixth.
So the state is now one snapshot behind one writer. `setSnapshot` is the
only thing that mutates it and always drops any in-flight probe, because a
promise created under a superseded answer can only report something this
state has already replaced. `loadSlideRendererAvailable` clears the promise
on settle rather than only on success, so an unanswered probe stays
retryable. Consumers read through `useSyncExternalStore`, so every mounted
viewer sees the same value including a later invalidation — there is no
longer a subscribe-or-not branch to get wrong.
That makes the two edges from this round unrepresentable rather than merely
fixed: a settled probe cannot be served after an invalidation, and a
malformed payload now routes through the same invalidation path as an
absent capability instead of early-returning past it.
One deliberate asymmetry: a transport failure does not invalidate. It is
not an answer, unlike a malformed body, so it must not erase what we know.
Covered by its own spec.
* fix(web): version probes and treat an unusable answer as unknown
Blocking catch (thanks @nettee): clearing `inFlight` stopped a stale promise
being *served*, but not a request already in the air from writing when it
lands. A probe issued before the boot fetch could resume afterwards and put
its `true` back over the boot answer's `false` — the entry returns on a
headless daemon and the next export hits the 501 this change exists to
gate. That was the half of "collapse the state" I had not done: the writes
were unified, but time was not.
Each probe now carries the generation it was issued under and drops its
result once a write has superseded it, so a request from the old world
cannot write into the new one.
Second, transport and parse failures are no longer the same branch. A 200
whose body will not parse IS an answer, just an unusable one, and the
contract says that means unknown — collapsing it with a dead socket let a
stale `false` survive it and keep hiding the entry. Both the boot fetch and
the direct probe now invalidate on an unparseable OK response while leaving
a known answer alone on a transport failure.
Specs: a deferred probe that resolves after a `false` is recorded, an
unparseable 200 through the boot path, and a transport failure that must
not erase what we know. The first is verified red by removing the guard.
Also replaced a spec that passed without exercising its path — a cached
answer short-circuits the direct probe, so the parse-failure case only
reaches the code through the boot fetch.
* refactor(web): read the capability from app state instead of a client store
Seven review rounds produced eleven findings, and ten of them were in the
client-side capability store rather than in what this PR is actually about.
The pattern was not bad luck: each fix added a variable to reconcile the
existing ones (`cached` -> `+inFlight` -> `+listeners` -> `+generation`),
which widened the interaction surface that produced the next finding. The
latest pair — a parsed JSON `null` throwing past the invalidation, and a
probe's `finally` clearing a newer probe's promise — are both interactions
between those variables, not defects in the gate.
The store existed to avoid prop-drilling a boolean that `App` already holds
in React state from its boot `/api/version` fetch. That trade bought a
tidier diff and paid for it with cache coherence, staleness, concurrency,
retry and subscription lifecycle — a distributed-state problem invented for
a value that was never distributed.
So the store is gone. `App` passes `appVersionInfo?.capabilities
?.slideRenderer ?? null` down the same path `metricsConsent` already takes,
and the gate reads a prop. No cache, no probe, no retry, no generations, no
subscription: every failure mode those rounds found is now absent rather
than fixed.
The three cases the contract cares about — `false` hides, `true` offers,
`null` (unknown) offers — are three renders in `FileViewer.test.tsx`. The
specs that exercised probe lifecycle went with the probe; they were testing
a mechanism, and the mechanism is what turned out to be wrong.
* fix(web): close the two paths that could still reach the 501
Both blocking notes from review, and both are consequences of how I threaded
the capability rather than of the gate itself.
- The design-system detail route mounts `FileWorkspace` directly, so it never
received the prop and `FileViewer` fell back to its `null` default. Since
unknown deliberately fails open, a headless daemon kept offering PPTX for
`system/artifacts/deck.html`. Threaded from the same App-level value the
project route uses; one source, two callers, rather than a second mechanism
for the second route.
- `appVersionInfo` came only from the one-shot boot fetch, and that helper
yields null on any failed or non-OK response. A daemon merely slow to come
up therefore left the capability unknown for the life of the page — and
unknown fails open, so the gate was defeated by a startup blip. It is now
re-asked once the daemon is reachable, which is the narrowest retry that
fixes it: keyed on the daemon becoming live rather than on a timer, and it
stops as soon as an answer arrives.
* fix(web): make the capability refetch schedule its own retry
Third incarnation of the same mistake, so worth naming: retrying on a
dependency change cannot recover from a failed probe, because a failure
changes nothing — `appVersionInfo` stays null, `daemonLive` was already
true, neither dependency moves, and no further attempt is ever issued. The
failure is sticky, `slideRendererAvailable` stays unknown, and unknown
deliberately fails open, so a headless daemon keeps offering an export it
can only answer with 501.
The retry now schedules its own next attempt instead of waiting to be
re-triggered: bounded with backoff (500ms / 1.5s / 4s / 10s), stopping the
moment an answer arrives. Bounded because an unreachable daemon is its own
visible problem and must not be polled for the life of the page.
* fix(web): cap the capability retry interval instead of the attempt count
Closing the residual case I flagged in my own audit rather than waiting for
it to be found: with a bounded number of attempts, once they were spent
`daemonLive` never flips again and nothing could ever ask, so the capability
stayed unknown for the life of the page — and unknown deliberately fails
open. That is the same hole the retry was added to close, just with a
smaller window (four consecutive failures against a daemon that had already
health-checked as live).
I argued for bounding attempts to avoid polling for the life of the page. On
reflection that traded a rare slow poll for a permanent fail-open hole,
which is the worse side of the trade. The backoff now settles into a steady
30s interval instead of stopping: it only runs while the answer is unknown,
it stops on the first answer, and it leaves no state the page cannot recover
from.
* fix: forbid caching /api/version and stop the two writers clobbering
Both blocking notes, and the first is a consequence of the change itself
that I should have caught: adding a capability turned `/api/version` from a
near-static fact about the build into an answer that differs by which daemon
is behind the URL. It was still served with no `Cache-Control`, so a browser
or proxy could reuse a pre-swap response and a stale `slideRenderer: true`
would be consumed as authoritative — reopening the export the gate exists to
hide. Now `no-store`, with a spec pinning it.
Second, `appVersionInfo` had two unsynchronised writers: the boot pass and
the capability retry. A boot response that resolved `null` after the retry
had obtained an explicit `false` would erase it and reopen the fail-open
entry; a slow but valid boot response could likewise overwrite a newer
answer. The boot pass now never writes `null` and never overwrites an answer
that is already present — a late boot response is by definition the older of
the two.
* refactor(web): ask for the capability at the point of use
Fourteen findings on this PR, and nearly all of them asserted one property:
the capability must not get stale or stuck, because unknown fails open. That
is a global property, and global properties cannot be established by local
patches — which is why each fix produced the next finding rather than
ending the series.
The property is global because the gate was reading a client-side replica of
remote state. A replica needs an initial fetch, a retry, invalidation, write
ordering, cache directives, multi-writer synchronisation, distribution to
consumers, and a refresh when liveness changes. Every one of those was a
finding here.
Deleting `runtime-capabilities.ts` last time did not end it because it moved
the replica into `App` rather than removing it. This removes it: the export
surface asks the daemon when it opens, uses the answer, and discards it with
the menu. If the daemon is swapped, the next open simply asks again.
Gone with it: the prop threaded through four components, the boot-fetch
coupling, the retry with its backoff, and the two-writer synchronisation.
Kept: the daemon-side capability, `Cache-Control: no-store`, the optional
field tolerance, and the export-reason split.
The pending window still fails open, and that is deliberate — but it is one
round trip inside a single menu opening and nothing carries it forward, so
unlike the replica it cannot get stuck.
* fix(web): start each export-menu opening from an unknown capability
The probe state outlives the menu — the component stays mounted — so the
previous opening's answer was still present on reopen. If the daemon was
replaced in between, the gate decided on an answer describing a daemon that
is gone: a stale `true` offers an export the new daemon can only 501.
My previous commit message claimed the answer was "discarded with the menu".
It was not; only the fetch was tied to the opening. Now the state is reset
when the menu opens, so the claim and the code agree.
The regression spec is not in this commit, deliberately. My first attempt
passed with and without the fix — the second response landed too quickly for
the stale window to be observable, so it asserted nothing. The second
attempt, holding that response open, fails in both directions, which means
my assumptions about the close/reopen path are wrong rather than the code
being wrong. Shipping either would be worse than shipping neither, so the
fix goes in now and the spec follows once I understand that path.
* fix(web): revalidate at the menu-to-modal handoff and state the contract
The blocking finding: clicking the PPTX row closes the menu and opens the
modal in one batched update, and the modal's confirm reads the same
capability value — so a `true` fetched at menu-open outlived its surface and
kept the confirm enabled against a daemon that may since only answer 501.
Because the two booleans flip in one batch, an OR of them never changes
across the handoff; the effect now depends on the booleans themselves, so
the handoff resets to unknown and asks again.
The capability contract is now stated once, at the probe:
1. An answer never outlives the surface opening that fetched it — every
opening, including the handoff, starts from unknown and re-asks.
2. Unknown fails open; only a resolved `false` hides the entry.
3. The daemon is the authority of last resort: a swap after the probe
resolves is unclosable client-side, and is caught by the export POST's
501, which now surfaces the no-renderer message.
Tests: the two vacuous positive cases now hold the response open and assert
against the resolved answer, and the reopen regression I owed exists. Its
three earlier failures had one cause worth recording: a stale `true` is
observationally identical to a pending probe — both fail open — so the
true-then-swap direction asserts nothing. The observable stale direction is
a lingering `false` wrongly hiding the entry on reopen, and that is what
the spec pins. Red-check matrix: removing the reset turns the reopen spec
red; reverting the deps to the OR turns the handoff spec red; mapping an
absent field to false turns the absent spec red.
* fix(web): recognise EPIPE as a proxy connection failure; drop a stray file
Both follow-ups from review.
EPIPE belongs in the errno set: the sidecar's own replay check classifies
exactly ECONNRESET and EPIPE as connection-level, and when a replay is not
applicable the EPIPE surfaces as a plain-text 502 containing `write EPIPE` —
which fell through to the generic "export request failed (502)" instead of
the daemon-down message. The classifier also had no direct tests since its
earlier ones were deleted along with the capability store, so it now has a
dedicated spec pinning both halves of its contract: every connection shape
the proxies actually emit is recognised, and JSON 5xx / errno-less text /
other statuses are not. Removing EPIPE turns exactly its case red.
`__diag.test.tsx` should never have existed in the repo: it was a local
scratch file for probing menu toggle mechanics, swept in by a `git add -A`
in the previous commit. It logged instead of asserting and leaked its fetch
stub into the rest of the file. Deleted.
* fix(web): key the capability probe to the export tab, not the popover shell
The popover shell can open on Share and sit there across a daemon swap;
switching to Export then presented the PPTX entry against the answer from
shell-open time. The export surface's opening is the tab becoming visible,
not the shell opening.
This is the terminating fix for the transition family, and the reason can
be stated as an equivalence rather than a hope: the probe's guard is now
the same predicate the consumers render under — the PPTX row mounts inside
`unifiedActionTab === 'export'` within the open popover, the modal confirm
under `pptxExportModalOpen` — and the effect's dependencies are exactly the
free variables of that predicate. A transition into visibility is by
definition a change of one of those inputs, so it necessarily re-runs the
probe. "A transition we forgot" is no longer a findable category: the
guard cannot be coarser than the render condition, because it is the
render condition.
Regression: false from the first daemon, switch to Share, swap, switch
back to Export with the new answer held open — the entry must show
(unknown fails open) and a second probe must have fired. Reverting the
guard to the popover shell turns exactly that spec red.
* fix(web): retry a failed probe within the opening; stop at a fieldless one
A probe that fails (daemon still starting, transient network) left the
whole opening at unknown, and unknown fails open — so a headless runtime
kept the entry clickable for the lifetime of that opening. Retried now with
a short bounded backoff that is cancelled by any surface transition:
nothing crosses surfaces, so the contract's first clause still holds for
the retry itself.
The distinction that keeps this from over-polling: a FAILED probe is worth
asking again, but a probe that SUCCEEDS without the capability field is an
old daemon — asking again cannot teach us more — so it stops at unknown on
the first valid answer. The two cases were previously collapsed into one
`null`, which is exactly why the earlier background-copy design kept
tripping over them; here they are separated at the only point that knows
the difference (did `fetchAppVersionInfo` return a payload or not).
Regression: first probe answers 500, the entry stays visible (unknown
fails open), the in-opening retry fires, and its held `false` then hides
the entry. Reverting to a single-shot probe turns exactly that spec red.
---------
Co-authored-by: Jason <>
|
||
|
|
d517a18ec7 |
fix(analytics): restore migrated pricing funnel events (#7299)
* docs: design pricing plan exposure restoration * fix(analytics): restore migrated pricing funnel events * docs: design authenticated pricing analytics bridge * docs: plan authenticated pricing analytics bridge * feat(analytics): add authenticated pricing bridge contract * fix(analytics): harden pricing bridge validation * fix(analytics): restore complete personal pricing semantics * fix(analytics): require explicit personal audience * fix(analytics): relay authenticated pricing funnel events * test(analytics): cover localized pricing handoff * docs(analytics): correct pricing source handoff * fix(analytics): align migrated pricing compatibility contract --------- Co-authored-by: 张辉华 <zhanghuihua@refly.ai> |
||
|
|
1877bf607b |
fix(web): hide empty Skills source filters (#7298)
* fix(web): hide empty Skills source filters (#1374) Hide source filter options that have no matching skills and reset a stale source selection after refresh. Treat missing source metadata as built-in for consistent counts, filtering, and edit protection. Generated-By: looper 0.11.8 (runner=worker, agent=codex) * fix(web): scope Skills source reset to catalog Keep source selection stable when active filters temporarily hide a source, while resetting only after the catalog loses its last skill. Add regressions for filtered empty states and delete refreshes.\n\nGenerated-By: looper 0.11.8 (runner=fixer, agent=codex) |
||
|
|
993f2e1a9e |
feat(ci): add workload convergence shadow (#7311)
* feat(ci): add workload convergence shadow * fix(ci): identify convergence public reads * refactor(ci): move convergence policy into scripts * fix(ci): fail open on malformed convergence reads Generated-By: looper 0.11.2 (runner=fixer, agent=codex) * fix(ci): preserve safe convergence observations Generated-By: looper 0.11.2 (runner=fixer, agent=codex) --------- Co-authored-by: Looper <looper@noreply.github.com> |
||
|
|
e13a4c385f |
fix(daemon): hide ACP metadata updates from transcript (#7309)
Co-authored-by: helson <helson@helsondeMac-mini.local> |
||
|
|
87e75dce84 |
feat(landing): update pricing interactions and copy (#7240)
* feat(landing): add pricing interaction review * fix(ci): allow generated pricing preview bundle * fix(landing): route pricing to reviewed page * fix(landing): narrow pricing PR to reviewed changes --------- Co-authored-by: hazel-ye <80680199+hazel-ye@users.noreply.github.com> |
||
|
|
7a4175c86f |
feat(analytics): report why an installed agent CLI cannot be used (#7285)
* feat(analytics): report why an installed agent CLI cannot be used A user installed DeepSeek Harness correctly and OpenDesign showed nothing. We only learned about it because they said so and sent a diagnostics zip: detection is the one stage that knows an installed CLI is unusable, and it told nobody. There is no way today to ask how many installs are in that state, or which CLI, or why. Report each diagnostic detection produces, once per (agent, reason, version) per page session. Detection runs on cold start and on every rescan, so counting passes would turn one broken install into dozens of events; deduped, the number means "installs affected", which is the denominator a decision here actually needs. Warnings count too — an `untested-version` spike across many installs is how we would learn our supported-version list had fallen behind our own installer, which is precisely what went unnoticed. The resolved path is deliberately absent: an agent binary path contains the OS username. `has_path` carries the only part that matters, since the picker hides a row that has none. Half the registry was invisible to this before it started. `TrackingCliProviderId` covered 14 ids while the daemon ships 28, so DeepSeek Harness and a dozen others collapsed into `other` — a breakdown that cannot name the failing CLI answers nothing. Every shipped agent now has an id, and a test fails when a new runtime def arrives without one. * test(runtimes): assert the shipped agents, not the machine's The tracking-id guard walked AGENT_DEFS, which appends whatever local profiles the developer declared in their own agents.local.json. Those ids are required not to collide with a shipped one, so they correctly collapse to `other` — and turned anyone's personal config into a failure reading `expected [ 'my-custom-agent' ] to deeply equal []`, indistinguishable from the missing-mapper regression the guard exists to catch. Export the shipped list under a name that says which question it answers, and assert on that. The unknown-id case stays: a locally declared agent is not ours to name, and PostHog should never learn an id out of someone's config file. * test(runtimes): pin the local-profile case in the repo, not in my shell The isolation fix was verified by pointing OD_AGENT_PROFILES_CONFIG at a throwaway file by hand, which proves it once and protects nothing. Build the profile inside the test instead, so the machine-dependent failure the guard used to produce cannot come back unnoticed. Registry lists are built at module load, so the profile is written before a fresh dynamic import. The case asserts the profile actually reached AGENT_DEFS before asserting it stayed out of SHIPPED_AGENT_DEFS — without that, it would pass just as happily if the profile never loaded at all. |
||
|
|
35a38abf34 |
fix(daemon): admit one design-system enrichment run per conversation (#7271)
A double-triggered "AI Optimize" affordance created two concurrent design-system enrichment runs 383 ms apart in one conversation and both were billed (2026-07-28 incident). Reject the newcomer on the daemon with HTTP 409 DESIGN_SYSTEM_ENRICHMENT_IN_PROGRESS (details name the owning run), keep ordinary chat turns ungated, and make the web trigger a synchronous single-flight call so the duplicate never leaves the client. |
||
|
|
82c607d70a |
fix(packaged): report the daemon log tail when startup failure is unparseable (#7291)
The largest startup-failure bucket in production (macOS daemon-start, 968 events across 293 people over the 14 days to 2026-08-22) exits code=1 with the native module present and error_code / missing_module / daemon_error all null. The reason was printed in a log tail reportStartupFailure had already read and then discarded, because parseDaemonLogTail only names shapes we already know (ERR_*, a missing module, an XError: headline). When that parse yields nothing, send the tail itself: last 40 lines, scrubbed of POSIX and Windows home dirs before truncation, hard-capped at 4000 chars keeping the end (the tail is chronological, so the fatal line sits nearest the exit). Narrow by design — when the cause is already named the raw tail is bytes and privacy surface for no new information, so a present value also means "this log defeated the parser". |
||
|
|
114e73de16 |
chore: retire Feishu group entries, route every community CTA to Discord (#7295)
Replace every user-facing Feishu group entry (README, landing header and hero community CTA for zh/zh-tw, launch-week page buttons, DeepSeek Harness guide hero CTA in all locales, client nav-rail social slot for zh-CN/zh-TW) with the Discord invite, and drop the now-unused Feishu QR/mark assets, icon, i18n keys, CSS and analytics element value. Claude-Session: https://claude.ai/code/session_017gVe9HtkaMhcPCvm3aX7Eg Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com> |
||
|
|
bbf433b4cd |
docs(readme): fix broken star history chart (#7129)
The star history chart in the README and its localized translations currently fails to render because the upstream provider no longer serves it. This switches the chart image and its wrapping link to a working provider and keeps the existing dark and light variants, restoring the chart across all languages. Co-authored-by: OctoBored <212877535+OctoBored@users.noreply.github.com> |
||
|
|
91c0f701ef | docs: sync zh-CN README with English (Claude Desktop, roadmap, license) (#7149) | ||
|
|
a588cefd2b |
test(daemon): add regression tests for headerless /raw/ reads of workspace-bound projects (#7206)
* test(daemon): add regression tests for headerless raw reads of workspace-bound projects Closes #7072. The /raw/ and /preview/ routes call authorizeProjectRequest with { mode: 'read', allowNavigationQuery: true }. Before the enforceLocalProjectDataPlaneRequest refactor, these routes delegated to enforceVerifiedWorkspaceResourceRead, which required an explicit workspace context for any workspace-bound project and returned WORKSPACE_CONTEXT_REQUIRED (400) when headers were absent — breaking local/desktop preview iframes and MCP preview URLs for projects with a personal workspace_projects binding. The two new test cases pin the corrected behaviour: a headerless request to authorizeProjectRequest with allowNavigationQuery: true must succeed (return true, not call any remote verifier) for both personal and team workspace-bound projects, matching exactly what an iframe src or a bare loopback fetch sends. * chore: retrigger CI after flaky workspace-restoration P0 * chore: retrigger CI after repeated workspace-restoration flake |
||
|
|
1151f4aff8 |
fix(docs): align Manual Edit protocol summaries with live contract (#7207)
The v2.3 paste-image summary still said {id, file} and the v2.4 apply-dom
op list omitted insert-at-index and apply-content, contradicting the
detailed prose and the implemented bridge/host messages (#6141).
|