* feat(od-next): open registered runtimes and CLI BYOK Use the effective runtime registry for OD Next admission and remove retired capability qualification. Keep native observation and existing resume/cold recovery, with the evidence graph owned by observability contracts. Add run-scoped BYOK connection flags with caller-selected environment key input and reuse the existing provider bridge. Accept equivalent frozen Skill JSON and Base64 representations while preserving content and ownership boundaries. Validated with focused contracts, runtime, public Run and CLI regressions; eight real native agents and a real BYOK request/production pair; guard and workspace typecheck. Refs #8701. * fix(web): describe BYOK project file tools Explain that API-provider project runs use OpenCode file tools and require OpenCode to be available. Update the existing notice in all 19 locales and its component/browser regressions. Validated with red/green notice checks, real settings and preview follow-ups, guard and workspace typecheck. Refs #8701. * fix(od-next): preserve hidden prototype form feedback Keep HTML hidden semantics within the shared layout layer without overriding authored product CSS or hidden-until-found behavior. This fixes name errors remaining visible after a successful registration. Validated with a red/green Chromium regression, three resource-boundary checks, a real Claude follow-up with desktop/mobile form interaction, guard and workspace typecheck. Refs #8701. * test(od-next): make Run fixtures select their execution path Explicitly select ordinary Runs for retry persistence, plain stdout replay and the compatibility single-run telemetry case. Keep task hierarchy and terminal fallback telemetry cases active, and assert the selected path. Restore owner write permission to the leftover-agent fixture's frozen directories only during teardown so recursive deletion works on POSIX. Skip symlinks and preserve cleanup errors and every original behavior check. Reproduced all four CI failures before the fix. Focused daemon and E2E checks passed (12 tests), along with guard, workspace typecheck and focused LSP diagnostics. Refs #8701. * fix(daemon): avoid passing Pi resource directories as prompt files Pi reads existing --append-system-prompt paths as files. Stop passing skills, design systems and task input directories there; their resource paths already travel in the composed RPC prompt. Validated with 104 focused runtime/RPC tests, a real Pi 1.1.0 offline startup red/green probe, workspace typecheck and guard. * fix(web): coalesce streamed calls in generation progress Fold in-flight snapshots and terminal inputs through the existing tool-event reducer before rendering the Design Files progress feed. Keep the latest input, stable call identity and the limit of twelve distinct calls. Reproduced the duplicate React key warning before the fix. Runtime and progress-feed regressions now pass alongside existing in-flight tool-row tests. * fix(web): preserve forms and browser storage in prototype previews Permit client-side form submission in opaque interactive previews. Route Web Storage artifacts through the existing isolated powered preview, including daemon hints for large files, and retain guards when isolation is unavailable. Verified URL and srcDoc form interaction, persisted values after reload, and host isolation with focused Chromium regressions. Focused viewer/runtime and daemon tests, workspace typecheck and guard passed. * test(web): keep srcDoc fixtures on the opaque preview path Use an existing non-storage sandbox-shim signal in transport lifecycle fixtures now that Web Storage artifacts select the isolated preview origin. Preserve every reload, activation, recovery and manual-save assertion. Validated all 338 cases in the three affected FileViewer test files, workspace guard and typecheck, and focused diagnostics. * fix(byok): accept keyless IPv6 loopback providers Recognize the bracketed IPv6 hostname returned by WHATWG URL parsing in both settings validation and the OpenCode BYOK runtime. Keep remote hosts key-required. Validated with focused daemon and Web tests, settings browser checks, workspace typecheck and guard. * fix(daemon): restore Pi sessions and preserve completed RPC turns Load prior Pi history with switch_session and capture the authoritative sessionFile through nonblocking get_state. Treat a host-requested SIGTERM after a clean completed turn as shutdown, including Pi builds that report exit 143, while preserving real errors and cancellation. Validated with focused protocol and public Run lifecycle tests, a real Pi generation and native resumed edit, workspace typecheck and guard. * fix(preview): isolate project storage and preserve hidden controls Give powered previews stable per-project loopback origins and reserve those hosts for matching powered GETs before API or static exceptions. Advertise the capability so newer Web clients retain the opaque fallback with older daemons. Reuse the active powered transport in Present. Share a low-specificity hidden-control guard across URL and srcDoc previews without overriding explicit author rules or until-found; preserve doctype mode when synthesizing a missing head. Validated with actual two-project storage, Workers, reload, Present and API isolation browser witnesses, form/hidden regressions, focused Web and daemon checks, workspace typecheck and guard. * test(e2e): align public canaries with best-effort OD Next Read the remaining recipe identity marker without the retired serialized package hash. Make the fake OpenCode help probe finite. Exercise the current public question-form action, completed question turns/new answer tasks, and no-artifact normal endings while retaining cancellation, artifact and deck assertions. Validated with six fake-runtime cases and focused active/off public browser canaries with explicit rollout flags; no production completion policy or timeout was relaxed. * fix(od-next): guide focus restoration after list updates Prefer native modal lifecycle and restore focus to a current control after list rerenders, falling back to the primary action when a filtered item disappears. Real OpenCode and Pi prototypes exposed detached-trigger focus loss. Keep this as best-effort build guidance, without output contracts or automatic repair rounds. Validated the actual artifacts through normal public edit tasks and browser interaction checks; profile resource tests passed. * fix(od-next): retain focus through inline task updates Extend the existing live-control guidance to inline list actions. Real Copilot and Qoder boards also lost keyboard focus when status updates rebuilt their cards; hidden filtered items need a visible fallback. * fix: align Discovery integration tests and presentation storage * test(daemon): tolerate transient memory origin cleanup races --------- Co-authored-by: zhouzheng <zhouzheng@refly.ai>
14 KiB
Prompt composition — the variant axes
Read this before changing any prompt text, wherever it lives.
The one thing to know first
A generation run is composed by ONE of two independent implementations, and
apps/daemon/src/prompts/ is only one of them. The fork is an early return at
the top of composeSystemPrompt:
// apps/daemon/src/prompts/system.ts:905
}: ComposeInput): string {
if (odNextStrategyRecipe) {
return composeOdNextStrategyRequestPromptV2(odNextStrategyRecipe, {...});
}
// ↓ everything below — the entire legacy stack — is skipped on the OD Next path
The API/BYOK mirror at packages/contracts/src/prompts/system.ts:318 forks the
same way. The two sides share no composition floor: a rule added to one holds
only for the runs that take that side.
Which runs take which path
OD Next is on by default and gated. evaluateOdNextRollout
(apps/daemon/src/strategies/od-next/rollout.ts:151) is an AND over the policy
and one run's facts, and some of its inputs vary run to run on one machine —
which is why a divergence between the two sides surfaces as an intermittent
bug rather than a reproducible one:
- Scenario provenance (
rollout.ts:134,odNextTaskTypeForProjectScenarioBinding) requiresprovenance === 'automatic_default'. A project where the user explicitly picked a scenario resolvestaskTypeto null and takes the legacy path. - The default agent scope comes from the effective runtime registry, including
shipped definitions and local profiles.
OD_NEXT_STRATEGY_AGENTScan explicitly constrain it.sourceKindmust bebundled; capability fixtures, native child evidence and session resume are not admission requirements.
Nothing outside one run's own inputs takes part. There used to be a stop latch — a row a failing run wrote that disabled OD Next for the whole daemon instance, outranked the saved mode, and survived restart while the Labs switch still read "on". It is gone, so a divergence you are chasing cannot be a leftover from an earlier run on the same machine.
The Labs page remains, but its OD Next switch is retired. Historical personal
odNextStrategyMode preferences do not disable the default active route.
OD_NEXT_STRATEGY_ROLLOUT remains an engineering process override.
The variant axes
Changing "the prompt" can mean up to four edits. Check each one:
| Axis | Switch | A side | B side |
|---|---|---|---|
| Strategy | odNextStrategyRecipe (apps/daemon/src/prompts/system.ts:905) |
apps/daemon/src/prompts/ |
plugins/_official/scenarios/od-next-strategy/assets/** plus TypeScript in packages/contracts/src/prompts/od-next-strategy.ts |
| Legacy core | OD_PROMPT_CORE (apps/daemon/src/server.ts:10049) — default is slim, classic is the opt-out |
apps/daemon/src/prompts/core-slim.ts |
official-system.ts + discovery.ts |
| Execution mode | none; hand-maintained mirror | apps/daemon/src/prompts/*.ts |
packages/contracts/src/prompts/*.ts (API/BYOK) |
| OD Next internals | none; the same list is declared twice | OD_NEXT_PROMPT_STAGE_CONTRACT_V2 (packages/contracts/src/prompts/od-next-strategy.ts:214) |
od.pipeline.stages in the plugin's open-design.json |
On the third axis, five pairs are still hand-maintained and have already drifted, so diffing a pair is not a reliable way to find what a change is missing:
| File | daemon | contracts |
|---|---|---|
system.ts |
2048 | 1141 |
discovery.ts |
317 | 286 |
directions.ts |
321 | 284 |
media-contract.ts |
594 | 154 |
official-system.ts |
202 | 163 |
core-slim.ts |
429 | absent |
core-slim.ts having no contracts counterpart means the API/BYOK path cannot
receive the slim charter at all — packages/contracts/src/prompts/system.ts
has no promptCoreVariant input. Whether that is intended has not been
established here; treat it as an observed asymmetry, not a known bug.
deck-framework.ts is the exception and the model: see below.
Host runtime contracts
These are not style preferences. Each is consumed by product code, and a generated artifact that omits one is not controllable by the host. They belong to the host, not to either strategy — if a strategy is retired, these stay.
| Contract | Host consumer | Legacy | OD Next |
|---|---|---|---|
data-od-deck-protocol="1" on <html> |
apps/web/src/runtime/srcdoc.ts:3136 — how the host recognizes a v1-native deck |
✅ | ✅ |
od:deck-ready announce (protocolVersion, capabilities) |
srcdoc.ts ready listener — a message without protocolVersion === 1 is ignored |
✅ | ✅ |
od:slide-state {active, count} posts |
unified slide counter and toolbar state | ✅ | ✅ |
od:slide navigate listener |
host-driven paging (next/prev/first/last/go) |
✅ | ✅ |
id="deck-stage" |
srcdoc.ts:3145 isFrameworkDeck → stage style fix, disables click-nav |
✅ | ✅ |
@media print block |
Share → PDF multi-page stitching | ✅ | ✅ |
<question-form> |
AssistantMessage.tsx → QuestionFormView; runAskedUserQuestion analytics |
✅ discovery.ts |
✅ od-next-strategy.ts:434 |
data-od-comment-id preservation during HTML editing |
strict comment identity readers | ✅ shared comment-identity.ts (classic/slim/API-BYOK) |
✅ shared host directive |
| Motion video export | Actual source/MP4 linkage, local audio and encoded-output checks run inside the exporter; no model-plan gate | ✅ selected motion skill | ✅ motion task profile |
| Shared motion source player | Composition-only preview controls and persisted playback; suppressed during frame capture | ✅ motion source | ✅ motion source |
Motion data-poster-time |
Agent-selected seconds; daemon validates the linked MP4 and freezes its frame with the composition dimensions | ✅ selected motion skill | ✅ motion task profile |
.od-frames/ device shells |
prototype device frames | ❌ | ✅ OD Next only |
Two things to read off this table.
The deck rows are the model for how a host contract should be carried. One
source — renderDeckFrameworkDirective /
renderLegacyDeckCompatibilityDirective in
packages/contracts/src/prompts/deck-framework.ts:573, whose skeleton pins
data-od-deck-protocol="1" at line 48 — feeds all three prompt paths. The
daemon file is now a ten-line re-export whose own docblock states the reason:
"the canonical deck scaffold lives in contracts so classic, BYOK, and OD Next
prompt paths cannot drift." OD Next reaches it through
resolveOdNextDeckFrameworkMode (od-next-strategy.ts:187), which picks
canonical for a blank deck and legacy_compatible when a seed or existing
deck HTML is present, and the directive is emitted as an
<od-next-context kind="instruction" name="deck-framework"> block
(od-next-strategy.ts:548). The wiring that supplies those inputs lives at
apps/daemon/src/server.ts:10125. Copy this shape for the next contract.
The divergence still runs both ways. OD Next is not a superset of legacy;
it owns .od-frames/ device shells that legacy has no equivalent for. And OD
Next content has two possible homes — the plugin's markdown assets and the
TypeScript in od-next-strategy.ts. The deck contract lives in the TypeScript,
not in ppt.md. Check both before concluding a contract is absent on that
side.
Worked example: #7568, then #7651
These two PRs are the same author, three days apart, and together they are the clearest statement of what this document is for.
#7568 (fix(deck): make legacy thumbnail navigation instant, merged
2026-08-29, 973e868ce) introduced deck protocol v1 across 18 files,
+510/-44. Its stated purpose, in its own PR body, was to give newly generated
decks "one canonical, versioned navigation protocol so future agents do not
create more navigation dialects."
It handled the daemon ↔ contracts axis completely: a shared constant so the two prompt copies could not drift on the protocol, both copies changed, a guard added on each side, and validation that ran the full web (7,139 tests) and contracts (500) suites plus two packaged DMG end-to-end runs. By every convention in this repository it was a thorough PR.
It touched nothing under plugins/_official/scenarios/ and nothing in
od-next-strategy.ts. Every deck generated on the OD Next path still shipped
no protocol: the PR whose goal was to stop new navigation dialects left one
live.
Three things made the miss invisible. All three are worth checking against your own change:
- The guards it added could not reach the other side. Both new tests called
composeSystemPrompt({ skillMode: 'deck' })with noodNextStrategyRecipe, so they never passed the fork. The daemon one was named'ships new Agent decks with OD Deck Protocol v1'— a claim about Agent decks that was false for every OD Next run, asserted by a test that was green. A test name is not coverage; check what its inputs can actually reach. - The snapshot moved and stayed green.
apps/daemon/tests/prompts/__snapshots__/system-prompt-matrix.test.ts.snapupdated cleanly, because every scenario in that matrix takes the legacy branch. That is still true today. - Sharing a constant fixed the machine-readable half only. The directive prose around the shared protocol constant had already drifted between the two copies before #7568, and #7568 inserted the same new clause into both — into two sentences that already disagreed.
#7651 (fix(deck): pin deck protocol in OD Next prompts, merged
2026-08-31, f65d245de) repaid it: 16 files, +855/-622, including a new
od-next-prompt-recipe.test.ts, a new plugins-strategy-recipe.test.ts, and
new e2e/ui/real-daemon-run.test.ts coverage. The fix is the right shape — it
collapsed the deck scaffold to a single source in contracts and reached OD Next
through the recipe rather than copying the skeleton into markdown — and it is
the reason the deck rows above now read ✅ on both sides.
The transferable rule is the cost, not the outcome: closing a one-sided change took a second PR nearly twice the size of the first. Before landing a change to generated-artifact behavior, name every path that composes a prompt, then say for each one whether your change reaches it. "The tests are green" answers a narrower question than that.
Editing rules
- Changing a rule that affects generated artifacts? Apply it to every affected path, or state in the PR body why one is genuinely out of scope. "I'll do OD Next later" leaves the bug live for whichever runs take that path.
- Adding a host contract? Give it one source that all paths consume, the
way
deck-framework.tsnow does, and add its row to the table above in the same PR. - Prompt-facing vs. maintenance text. Everything under
plugins/_official/scenarios/od-next-strategy/assets/is sent to the model verbatim. Never put repository-maintenance notes there — put them in that folder'sAGENTS.md, which is not part of the asset roster. Inapps/daemon/src/prompts/andpackages/contracts/src/prompts/the exported string is the prompt, so/** */docblocks above an export are safe. - A green suite is not evidence that both sides carry your change.
apps/daemon/tests/prompts/system-prompt-matrix.test.tsfreezes which sections each scenario receives, but every one of its scenarios takes the legacy branch, so it cannot see an OD Next regression.
Editing the OD Next plugin assets
assets/core-system-prompt.md, assets/general-orchestration.md, and exactly
one assets/task-profiles/<taskType>.md are decoded and concatenated verbatim
into the prompt bundle
(apps/daemon/src/strategies/od-next/initial-prompt-bundle-service.ts).
apps/daemon/src/plugins/strategy-package.ts:158 builds an explicit roster:
./open-design.json, ./SKILL.md,
assets.core.path, assets.orchestration.path,
<selected task profile>.path, assets.taskProfileMapping.path,
<selected profile>.resources[*].path
Only those files are read, and only those are hashed into the package identity.
- A file the manifest does not declare (that folder's
AGENTS.md, for instance) has zero runtime effect — not read, not hashed. open-design.jsonandSKILL.mdare in the roster. Editing either moves the package identity.- Only the selected profile's
resourcesjoin the roster, so one task type's resources changing does not move another task type's hash. - Changing a task profile's body should come with a
versionbump on its entry inopen-design.json.
Two shapes for adding content a profile needs. Prompt text goes in the
profile's own .md. Non-prompt files (skeletons, shells, stylesheets) get a
resources entry on that profile in open-design.json; prototype does this
with assets/task-profiles/prototype/device-frames/*.html and layout.css,
staged into the project as .od-frames/ and referenced by the profile, never
concatenated into the prompt head. Prefer neither when the content is a host
contract — give it one source in packages/contracts and have every path
consume it, as the deck framework now does.
Known gaps
Verified on 6fb440a86 (2026-08-31).
- Five daemon/contracts prompt pairs remain hand-maintained and already
divergent (table above).
deck-framework.tsshows what unifying one looks like; none of the others has been done. core-slim.tsexists only on the daemon side, so the API/BYOK path cannot select the slim charter.- No machine check covers the OD Next column of the contract table.
packages/contracts/tests/system-prompt.test.tsandapps/daemon/tests/prompts/system.test.tskeep the legacy copies honest, but neither passes an OD Next recipe. #7651 addedpackages/contracts/tests/od-next-prompt-recipe.test.ts, which does exercise the OD Next recipe for the deck contract specifically; nothing generalizes that to the other rows.