* wip: wire deleted-file stop retry and cover owner-scoped HTTP behavior
* wip: persist published comment backfill state
* wip: connect backfill delivery receipts and preserve completed batches
* wip: expose exact-file backfill state and retry through HTTP and CLI
* wip: wire scoped comment sync pause into preview panel
* test: cover backfill terminal rejection and stale publication receipts
* wip: separate published content from unavailable viewer origin
* feat: distinguish publication success from unavailable viewer links
* fix(web): gate export share guide on authoritative project history
* test: restore publish backfill HTTP proof with current publisher pipeline
* fix(web): show authoritative stopped-share guidance
* fix(web): restore publish help before creating a share
* fix(web): show unread comments on collapsed rail
* fix: share annotated selectors across URL and srcdoc bridges
* test: verify saved comment selector reanchors in Chromium
* fix(web): warn about active shares before project deletion
* fix: authorize and bootstrap public share project bindings
* wip: prepare dispatched live share-chain probe (not run)
* fix(web): deliver per-file share residuals after deletion
* fix(web): prevent stopping shares in read-only viewers
* fix(e2e): retain complete share probe HTTP evidence
* fix(web): name shared projects in delete confirmation titles
* docs(e2e): clarify live share acceptance and human login boundaries
* fix(e2e): preserve share query context in diagnostic evidence
* fix(web): restore share-panel copy and comment-pin visuals per board8
chain1-publish G4: delete the stale "仅支持分享单个文件,内嵌图片等本地
资源暂不支持" sentence from fileViewer.publishSingleFileDescription (19
locales) — it contradicts the current multi-resource share and G4 required
its removal.
chain1-publish S1: add the missing "链接访问" title row + its static
description ("接收者可查看预览和已有评论,不包含对话和代码。") to
ShareTab's public-share section, as new fileViewer.linkAccessTitle /
linkAccessDescription keys (19 locales). No switch added — that control
overlaps the existing "停止分享" button and is left as a 需确认 item.
chain4-owner 4.2b: canvas comment pins go from 20x20/weight 600/2px white
ring/fixed #282828 to 28x28/weight 500/no ring, and now derive their
background/foreground from the same per-author color used by that author's
sidebar avatar (commentAuthorAvatarColor), both pure local CSS/TSX with no
packages/ or protocol change.
chain4-owner O7: formatCommentTime now buckets by calendar day (adds
"昨天", drops the "N 天前/N 周前" buckets, switches the hour bucket to
same-day instead of <24h elapsed, and formats older dates via
toLocaleDateString with year only when it differs from the current year),
and comment-side-time gets a title attribute with the full absolute
timestamp on hover.
Verification: pnpm --filter @open-design/web typecheck passes. Not run
through a real renderer — no pixel-level claim.
* fix(web): share entry icon, delete-confirm title, OP2 read-only hint
chain1-publish E0: swap the share toolbar entry's icon from RemixIcon
share-forward-line to the board's own upload-arrow glyph (13px path
"M12 4v11M7 9l5-5 5 5M5 14v5h14v-5") — same size/shape as the board's
.shareBtn icon, glyph only, no behavior change.
chain6-dialogs D-04: drop the extra "确定" from designs.deleteConfirm's
zh-CN/zh-TW title ("确定删除「{name}」?" → "删除「{name}」?") to match
the board and every other locale, which already read "Delete "{name}"?"
without an "are you sure" prefix.
chain4-owner OP2: add the missing "分享页评论不可编辑" explanation line
under a comment's textarea specifically when the viewer can't edit it
AND the comment is a share-page (external) one — team-member-reads-
external-comment (OP3) is unaffected, matching the board's OP2-only
scope. New comment.sharePageCommentReadOnly key, 19 locales.
Verification: pnpm --filter @open-design/web typecheck passes. Not run
through a real renderer — no pixel-level claim.
* test(web): update FileViewer specs for the G4/O7/4.2b restorations
Three specs encoded the exact pre-fix behavior the previous two commits
intentionally changed, so they went red:
- "reads the publish help without publishing" located the help bubble by
the now-deleted stale sentence ("Only a single file can be shared for
now..."); relocates it by the sentence that remains.
- "keeps relative comment-time boundaries stable across clock boundaries"
asserted the old <24h-elapsed hour bucket; the O7 rewrite buckets by
calendar day, so the fixture's 23h-ago comment (which crosses this
fake clock's midnight) now correctly reads "昨天" instead of "N 小时前".
- "keeps D1-D4 marker and deck-page presentation on the rendered
selectors" asserted the old 20px/weight 600/white-ring pin CSS; updated
to the board's 28px/weight 500/no-ring geometry.
Verification: pnpm --filter @open-design/web typecheck passes;
tests/components/FileViewer.test.tsx: 334/334 passed (via
node_modules/vitest/vitest.mjs --pool=forks, run directly against the
node 24.18.0 binary — this shell's `npx`/`node` wrap nvm in a way that
blows bash's FUNCNEST under vitest's fork pool).
* feat(web): draw the export guide's countdown, title and description
The 10s countdown already existed as state - remainingMs, checkedAt and
paused were computed and consumed - but nothing rendered it, so the guide
showed no sign that it was about to dismiss itself.
The bar is one CSS width transition per run rather than a per-frame timer:
the remaining time is already known when the run starts, so animating to
zero over exactly that duration needs no re-render, and a pause just freezes
the transition.
Board copy added across all 19 locales.
* test(web): update AfterExportShareGuide specs for the P1 card restoration
Three assertions encoded the pre-restoration visual/DOM shape that
d2c72f1624 intentionally changed to match Board8 Chain-6-dialogs.dc.html's
P1 ("导出完成 · 分享引导条", `.s-m1eexportprompt .prompt[role=status]`)
mockup:
- role query changed region -> status: P1's `.prompt` div in the board
markup carries `role="status"`, not the implicit `role="region"` that
came from the old bare `<section aria-label>`.
- button count 3 -> 2, and the close-button-then-never-show sequence was
replaced: the board's P1 card has no `.pclose` element (unlike the
discarded EX1 alternative, which does have one) - its only controls are
the `.pfoot` pair (`.pghost` "不再提示" / never-show, `.paction` "去分享"
/ open-share). The guide is still dismissible via never-show, open-share,
or the unchanged 10s auto-collapse in after-export-share-guide.ts.
- dropped the dead `awaitingDesign` fixture key/assertion now that the P1
design reference is in hand and the placeholder key has been removed.
Scoped run: 3 files / 40 tests passed via direct-node vitest (avoids the
nvm-wrapped node FUNCNEST issue under the fork pool noted in
od-side.md).
* feat(web): render the backfill, align, and shareStopped comment sync banners
CommentSyncBanner.tsx previously only rendered K8 (sessionMissing). The
backfill (K2), align, and shareStopped fields already existed on
CommentSyncState with i18n copy standing by, but no render branch consumed
them — this was flagged in .tmp/share-p0/visual-static/chain1-publish.md (K2)
and chain4-owner.md ("补传横幅/评论对齐横幅/分享已停用横幅").
Adds three branches, single-slot priority sessionMissing > shareStopped >
backfill(failed) > align(diverged), respecting the hard semantics documented
on packages/contracts/src/api/share.ts's CommentSyncState:
- backfill absent/pending/succeeded never renders a warning (absence is
"never attempted", not success); the server already filters stale-revision
backfill records, so any backfill this component sees is for the file's
current publication.
- align absent or 'unknown' never renders as aligned or as a problem; only
'diverged' is actionable.
- shareStopped: null (could not be read) never renders as stopped nor as
healthy; only `true` renders.
Retry buttons call the existing retry endpoints (POST comment-sync-state for
backfill, POST comments/align for align) and re-fetch via a new
`refreshToken` option on useCommentSyncState. The backfill retry button is
only shown when `retryable === false`, per the field's own doc comment:
retryable:true means the server is already retrying on its own.
useCommentSyncState also gained an optional `filePath` param, threaded from
HtmlViewer's `file` prop through CommentSideDock -> CommentSidePanel, because
the backfill answer is per-file and the endpoint requires filePath to compute
it (readPublishedCommentBackfill returns undefined without one) — without
this wiring the backfill branch would be permanently unreachable regardless
of the new rendering code. This is plumbing an existing parameter to an
existing endpoint, not new triggering behavior.
Copy is the parent-approved text verbatim, added as three new i18n keys
across all 19 locales (fileViewer.commentSync.backfillFailedTitle/Body,
alignFailedBody); the retry button reuses the existing preview.retry key.
Tests: new tests/components/share/CommentSyncBanner.test.tsx (12 cases)
covering all silence/render rules above. Full apps/web typecheck, guard, and
tests/components/FileViewer.test.tsx (334/334) all pass.
Not done in this commit: the ShareTab panel's own shareStopped-driven hint
(a separate, pre-existing `publicationStatus === 'stopped'` render in
ShareTab.tsx using project-share-history data, not CommentSyncState) is
untouched — it is a different signal for a different surface.
* feat(web): add the board's "link access" switch to ShareTab
chain1-publish.md's S1/S4 rows show a `role=switch` "链接访问" toggle as
part of the panel's fixed structure that the current implementation never
had (flagged under chain1-publish.md's "需要人确认" #1 and chain6-dialogs.md
D-00). The user explicitly authorized adding it and asked me to resolve its
semantic overlap with the existing "Stop sharing" text button.
Resolution: the switch reads and drives the exact same `filePublished` state
through the exact same handlers already used by the primary button/ghost
button (publishCurrentFilePublic / unpublishCurrentFilePublic) — on -> off
calls unpublish, off -> on calls publish. Because both controls act through
the same two functions on the same state, they can never show or produce
inconsistent results; this is presentation only, not a new trigger path.
Did NOT remove the existing "Stop sharing" ghost button, even though the
board's own S4 mockup has no separate one — chain1-publish.md's own
"需要人确认" #2 explicitly warns not to delete it off this board alone,
since it may belong to a different board's scope (chain2's handoff notes
mention Chain-5). Keeping both is redundant but not contradictory, per the
task's own escape hatch ("你判断不该同时存在,那就说明理由并只做一个" —
here the judgment is the opposite: keep both, and make sure they can't
diverge).
Geometry/colors from chain6-dialogs.md D-00: 32x19 track, r999, off #D6D6D6,
on #282828, 15x15 white thumb. No new i18n keys — the switch's accessible
name reuses the existing `fileViewer.linkAccessTitle` key.
Also updates the pre-existing frozen DOM-baseline fixture
tests/fixtures/share-tab/share-panel.team.html. Confirmed via `git stash`
bisection that this fixture was ALREADY stale/failing before this commit —
the earlier "链接访问" title+description restoration (commit 721cb976cf,
prior round) changed the panel's markup without updating this baseline, and
nothing in CI caught it because this file wasn't in that round's targeted
test run. This commit's switch adds one more, unrelated divergence on top of
that pre-existing one, so the fixture is regenerated from the component's
actual current output rather than hand-edited, and captures both changes at
once. The `personal` fixture is untouched: canPublishPublic is false in that
scenario (no workspace context at all), so the entire link-access section
(switch included) never renders there, exactly as before.
Verification: pnpm --filter @open-design/web typecheck and pnpm guard both
pass. tests/components/FileViewer.share-tab-extraction.test.tsx (22/22),
ShareTab.copy-button/first-publish/publish-error/scope-keyboard.test.tsx,
tests/styles/share-scope-design.test.ts, and FileViewer.test.tsx
(394 tests total) all pass. Not rendered in a real browser — see the
evidence file for the "未经真实渲染验证" caveat.
* fix: scope deletion stops to the selected published file
* feat(contracts): define share bridge v2 geometry boundary
Define authoritative OD v2 viewport geometry, fallback pin bounds, display tokens and geometry-update gates. Add 16 passing boundary tests; root guard and typecheck passed.
Known pre-existing failure: share-contract.test.ts expects SHARE_SNAPSHOT_DISCOVERY_IN_P0=false while baseline bd39802907 exports true. The identical assertion was reproduced against unchanged HEAD source; this unrelated assertion is not modified. Vela runtime and browser acceptance remain separate.
* fix(comments): create anchors for personal public shares
* fix(comments): create anchors for personal public shares
* fix(web): prefer readable element names in comment titles
Use screen labels then element-text snapshots before localized type fallbacks. Keep synthetic pin/page titles and hide internal IDs. Export a pure resolver for share-page parity without new locale strings.
Verified 10 contract tests, all 334 FileViewer tests, all 19 existing locale key sets, root guard and root typecheck. Vela adoption and browser visual acceptance remain separate.
* fix: stop published files before file deletion
* fix: explicitly resume stopped publication generations
* Restore main comment UI styling while retaining share flows
* test: verify guarded resume through real Vela CLI
* fix(web): match main comment composer surface
* fix: register personal project owner before share history lookup
* fix(web): align comment avatar colors with shared page
* fix(share): revoke deleted sources through atomic resource removal
* fix(comments): seed client avatar colors only from authoritative author key
* test(share): expect the additive publish failure detail after merging main
#8402 adds an optional closed-token `failure` to failed publish responses.
The share-origin HTTP test compared the 502 body exactly, so it now also
expects { stage: 'push', reason: 'unknown' } for a synthetic upload error.
* test(contracts): assert deployed snapshot discovery in P0
* fix(share): revoke folder publications before deleting descendants
* fix(web): normalize share controls to supported font weights
* test(web): assert manual copy fallback after clipboard rejection
* test(share): align route fixtures with original owner authority
* feat(comments): expose explicit visitor comment pull to UI and CLI
* feat(cli): expose explicit project share resume command
* fix(cli): emit JSON for files delete
* refactor(share): name the unpublish response contract
* fix(comments): preserve Vela author key in inbound avatar identity
* test(share): use valid sharedAt timestamps in owner fixtures
* fix(share): use promise stat with production folder route fs
* fix(comments): use defined theme red for unread indicators
* fix(cli): preserve workspace context error codes
* fix(files): hide internal paths in deletion failures
* fix(comments): persist local author name and reconcile cloud author key
* fix(share): align owner sharing flows with final UI audit
* test(share): verify unboxed comment sync across owner states
* fix(share): apply second-round final design review to owner share UI
- Share panel: remove every yellow warning; show the public link row above
the copy button in published states; oversized shares stay blocked and
explain why in the red error row.
- Global toast for update success/failure and residual stop failure, with
final design icons and copy; share panel buttons match the design.
- Comment sync: K1 turns the primary action into the sync progress bar, K4
shows a busy "正在同步评论…" button, K5 retry is red.
- Deployment menu layers above the share popover; export-complete guide
rebuilt to the final design.
- Share bridge pin label accepts exactly one letter so a CJK author's
initial can label their pin.
- Update ui-audit capture specs for the intended copy/structure changes.
* fix(share): apply third-round design review to owner share UI
- Share toast rebuilt as ShareFeedbackToast to the design: solid red/green
badge, plain underlined 重试, close; used for update success/failure and
the deleted-project stop failure.
- The ··· deploy menu drops down under the trigger, right-aligned to the
header tools, over the share panel.
- Signed-out share button reads 「登录后分享」; update notice loses its box;
更新链接 / 登录后更新 / K5 retry use the design's blue and retry icon.
- Signed-out observed share reuses the published link row and copy button
(chain icon, green check 已复制) and no longer overflows its card.
- Capture specs follow the intended copy and placement changes.
* fix(share): keep the signed-out share panel instead of re-homing on sign-out
Signing out on a shared file page intermittently landed on Home instead of
the signed-out share panel (Owner-S13), depending on which re-read returned
first:
1. The workspace-context refresh re-adopted the route's bootstrap witness
from the previous account, re-keying ProjectView on the same file.
2. FileWorkspace treated that remount as a file switch and cleared the
observed public link.
3. With no link left, the tab scope reset fell back to Home.
- useProjectRouteWorkspaceContext: a bootstrap witness issued before an
identity refresh is retired and never adopted after it (the hook's
documented rule for resolved authority now covers the witness too).
- isObservedShareFileSwitch: mounting or remounting a file view is not a
file switch; only a change to a different file clears the observed link.
Red specs: hook re-adoption (red on previous source) and FileWorkspace
remount (red on previous source). S13 capture: 8/8 passes, was 2/7.
* fix(share): align remaining owner share copy and K1 sync count with final design
- S0: signed-out description uses the design copy via its own key; the
publish-failure message keeps its original text.
- S5: update notice reads 「文件有新改动,访问者看到的还是上一版。」.
- K2/K5: the failed-sync notice drops the 「分享已发布」 title and uses the
design copy; terminal variants keep their own wording.
- K1: first-publication sync shows 「同步已有评论 · N 条」. The backfill
state carries optional total/synced from the daemon's backfill members
table, driving a determinate bar when progress is known; older daemons
fall back to the previous copy and the indeterminate sweep.
- Refresh the PR screenshot of the Share popup comment-sync state.
* chore(web): restore generated next-env.d.ts accidentally included in 30361fd670
* refactor(share): give each owner share UI element one owning component
Behavior-preserving consolidation of the owner share surfaces, verified by
pixel diff of all 17 Owner capture specs against the pre-refactor baseline
(every state within the known 0-0.25% noise from timestamps, placeholder
animation and the dev badge; the share panel itself is identical).
- Shared components: LinkAccessRow, ShareNoticeRow, ShareProgressButton
(S2 upload + K1 sync), ShareErrorRow, ShareButton (primary / soft /
soft-error / dark-sm on @open-design/components Button), ShareSignInButton
over a new useCloudSignIn hook shared with CloudSignInTip.
- The popover skin moves from a :has(.panel) override to an explicit
.chrome-unified-popover--share class; SignedOutObservedShare reuses
SharePanelHeader and the extracted rows.
- Icons: every inline share SVG moves into components/Icon.tsx with
byte-identical path data.
- Tokens: styles/share-tokens.css holds the share palette and sizes as fixed
light values (not app theme tokens, so dark mode renders as before); share
CSS modules reference them. Single-use decorative values stay local.
- Delete the unreachable React-component share popover (canShareCurrentFile
is false by design, Owner-G3) and its dead state and handlers.
- Toasts: share-adjacent feedback uses ShareFeedbackToast; top toasts share
one stacking order instead of overlapping at top:64px.
- Static CSS-contract tests read share styles through readShareCss, which
resolves share tokens to the design values they assert.
Allowed visual changes (existing defects):
- The sign-in entry is a real button; signing/error messages render below
it instead of inside a nested button (UI-040).
- The comment side panel's retry-failed message is red (UI-042).
- Simultaneous top toasts stack instead of overlapping (UI-039).
- 「正在开启…」 goes through i18n (UI-041).
* feat(daemon): resolve share links only from the Viewer origin
Public share links come from OD_SHARE_VIEWER_URL(S) (HTTPS root origin)
as https://<viewer>/artifact/{projectId}/{slug}; the console origin is
no longer a fallback. GET publish-public derives the URL from the
resolver instead of the stored row, so pre-upgrade console URLs are
never echoed, and reports link unavailable plus the slug when no Viewer
origin is configured. Remotely stopped rows are not live. Manual-revoke
recovery data carries projectId and a nullable url. od project share
prints the slug in the unavailable state. Test fixtures move from
OD_VELA_WEB_URL to OD_SHARE_VIEWER_URL.
* feat(web): show a link-unavailable state for URL-less publications
A publication is published when it has a slug, not a URL. Without a
Viewer origin the share panels show a translated link-unavailable
status with Stop sharing, never the Publish CTA, so the file is not
uploaded twice. registry normalizes the publish and read responses to
{slug, url|null}; auto-copy is skipped without a URL.
* feat(tools-dev): inject a default share Viewer origin for local daemons
The default stays empty until the test Viewer domain is decided
(decision 67 #7), so local shares report link unavailable. An operator
OD_SHARE_VIEWER_URL(S) always overrides it.
* feat(daemon): parse C3-LITE member comment pages in both collab transports
Add a single-page member pull to the Vela CLI and HTTP collab clients
that returns either a typed page or a rebuild-required signal.
- The page envelope is validated strictly; malformed comments are
skipped per item and reported, so one bad stored payload cannot
block the cursor.
- Minimal tombstones ({id, seq, deleted}) are accepted and get the
requested projectId.
- INVALID_CURSOR / CURSOR_STALE / SCOPE_CHANGED map to
rebuild-required; INVALID_PAGE_REQUEST stays an error.
- Paged requests send no If-None-Match.
* feat(daemon): drain member comment pages into SQLite atomically
Team projects on the Vela CLI transport now receive member comments
through bounded C3-LITE pages instead of the unbounded legacy pull.
- Each page's merges, tombstone deletes and cursor advance commit in
one immediate transaction; the persisted cursor is the cloud's
stateless page/resume token.
- The drain continues through empty hasMore pages, hands a finished
snapshot off to an incremental round, and stops at the end of an
incremental round.
- A remote tombstone deletes a row that has a pin_seq; unknown ids
are a no-op.
- A wake during a pull triggers exactly one trailing rerun.
- Merge notifications fire only for committed, changed rows.
- Share-page (user) comments stay on the legacy pull until public
paging exists. The legacy pull also keeps carrying member comments
whenever paging did not complete, and a cursor that needs a
rebuild is latched until the rebuild path lands.
* feat(daemon): rebuild member comment cursors and isolate sync scopes
An unusable member cursor is no longer latched. It is reset and rebuilt
from a fresh snapshot in the same drain.
- A per-scope ledger records the ids the member stream delivered. The
terminal snapshot page, in the same transaction as the handoff cursor,
removes cloud-origin rows the snapshot no longer contains. Rows the
stream never delivered (never-synced local comments, share-page
comments, rows another account learned) are never pruned, and
nothing is deleted before the snapshot completes.
- Writes committed during the snapshot arrive through the handoff
incremental round and are not pruned.
- Scope is (workspace, member, team, project). A scope must prove it is
current before taking a project over. Every page is re-checked after
its await and before its commit (active scope, team-share binding,
bound context), so an old account's or unshared project's in-flight
page is discarded.
- Account switch and stop sharing drop that scope's cursor and ledger
but keep the local comment rows as local copies.
- INVALID_PAGE_REQUEST and repeated transport failures back off per
scope (bounded exponential, capped at 5 minutes). A refused rebuild
snapshot backs off instead of looping.
* feat(daemon): announce comment merges once per pull round
A member page drain used to fire `onMerged` after every committed page,
and the legacy pull fired once more, so one 3-page drain of 250 comments
pushed three or four `comment-changed` events and cost the open web view
as many full list reads.
- Every row a round commits (member pages, prune, tombstones, legacy
share-page rows) is summed into one `onMerged` call when the round
settles, including rounds that pause, lose their scope or throw after
some pages committed. A trailing rerun is its own round.
- A throwing listener is reported through `onError` and neither masks
the round's own error nor turns a successful round into an unredeemed
wake.
* feat(web): single-flight the comment list refresh and keep unchanged comments
Every comment trigger (project SSE, fallback poll, visibility, stream
reconnect, status patch) started its own full list read; the generation
guard only discarded stale answers. A burst of 10 `comment-changed`
events cost 10 reads, and a refresh that found nothing new still
published a new list and re-rendered.
- `refreshPreviewComments` goes through a single-flight runner keyed on
project and conversation: a trigger during a read buys exactly one
trailing read with the latest closure; a different conversation starts
its own read and the old loop stops rerunning.
- Fresh reads are reconciled into the current state: unchanged comments
keep their objects, an unchanged list keeps its array (no re-render),
and attached comments follow the same rule. The initial conversation
read reconciles too.
- The remote half of an explicit pull has a 15s deadline and falls back
to the local list, so a stalled daemon pull cannot hold every later
refresh behind it.
Measured in ProjectView tests (10 SSE events during one read): 10 -> 2
list reads; an equal-data refresh: 1 -> 0 new lists/renders.
* fix(daemon): make a published private team project team-visible before its comment backfill
Publishing a public link for a private project in a team workspace registers
it in the team catalog, but only the later background reconcile marked the
local row team-visible. Until then the row was personal in a team workspace,
which is neither Team- nor personal-relayable: the publish backfill was
cancelled (BACKFILL_DELIVERY_DISCARDED) and comments written right after
publishing were never queued.
- publish-public marks the creator's private row team-visible right after
catalog registration, before the publication transaction enqueues the
backfill (decision 67 #11); the response carries madeTeamVisible.
- The relay re-queues a personal record as Team relay once its creator's
project turned team-visible, and defers (bounded) a personal record in a
team workspace whose publication is still current instead of cancelling it.
- A cloud seq is never adopted as pin_seq when another confirmed row of the
file already holds it, which produced two pins numbered 1.
* feat(share): tell the owner a public link makes a private team project team-visible
The share panel (and the React component viewer's share menu) shows
"After publishing, team members will be able to see this project" before
the first publish of a private project in a team workspace, in all 19
locales, and updates the scope control and team project views once the
daemon reports madeTeamVisible. `od project share publish` prints the same
notice to stderr and --json keeps the madeTeamVisible field.
* feat(packaged): hand the daemon its channel's share Viewer origin
Packaged desktop and headless daemons get the share Viewer origin for
their release channel: stable sends OD_SHARE_VIEWER_URLS with the prod
profile mapped to https://open-design.app; other channels send nothing
until the test Viewer domain is decided (decision 67 #10), so shares
report link unavailable. An explicit OD_SHARE_VIEWER_URL(S) at launch is
forwarded unchanged instead. The daemon resolver stays the only
validator. The legacy payload-desktop handoff strips both keys so a
channel default never becomes a sticky override.
* fix(daemon): report madeTeamVisible when a catalog reconcile marked the project first
Registering a private team-workspace project in the team catalog lets a
concurrent catalog reconcile (hub team-projects-changed, a project list
read) rebind the local row to team before publish-public marks it. The
mark then changed nothing and the response omitted madeTeamVisible, so the
share panel kept showing "only me" and the CLI printed no notice. Seen in
the local integration rerun on the first UI publish.
publish-public now reads whether the row is the creator's private team row
before registration, and reports madeTeamVisible when that row is the
creator's team row afterwards, whichever writer flipped it. A project that
was already team-visible still reports nothing.
* fix(daemon): order synced comment revisions by cloud seq so replays never undo newer state
A share-page comment resolved at cloud seq 10 reverted to open after a daemon
restart: the legacy pull cursor is in memory, so the restarted daemon replays
from seq 0, and the merge stamped Date.now() on the share-page create (which
carries no updatedAt), making the replayed create look newest.
Each preview_comments row now records the highest cloud revision seq it
reflects (cloud_revision_seq) and the seq space it came from
(cloud_revision_stream: relay team + member stream epoch). An incoming
revision at or below that seq in the same stream is a replay and never
overwrites the row. Unseen revisions keep updatedAt last-writer-wins when they
carry an updatedAt; one without updatedAt applies in seq order and is stored
with its createdAt, never the local clock. pin_seq is untouched.
* feat(share): take the canonical share link from AMR
The daemon no longer builds the public share address from a hardcoded or
per-channel Viewer origin. AMR reports it: `vela share publish|bind|resume|
project-status --json` carry `url` (https://<share origin>/artifact/{projectId}/{slug})
for an active binding, or `link: {status: "unavailable", code}`.
Link rule, one place (resolvePublicShareLink): an explicit
OD_SHARE_VIEWER_URL(S) override wins (a present invalid override is
unavailable, never a fallback); otherwise the AMR address, accepted only when
it is byte-for-byte the canonical HTTPS root-origin address of this project
and slug; otherwise unavailable, passing AMR's code
(PUBLIC_SHARE_WEB_URL_UNAVAILABLE / PUBLIC_SHARE_IDENTITY_INVALID) through.
A rejected address is logged without echoing it.
The verified AMR address is persisted with the publication and refreshed on
publish, foreground bind/resume, the background binding retry and every
publish-public status read, without rotating the publication witness. When
the status read fails, GET falls back to the persisted link marked
`stale: true` (not for a queued stop); a stored legacy console URL fails
re-verification and stays hidden until the next online read replaces it.
Packaged daemons no longer get a per-channel Viewer origin and tools-dev has
no default; an explicit launch override is still forwarded. `od project share`
prints the same link, a stale notice on stderr, and says when an alias has no
Viewer address.
* fix(share): keep the link access switch thumb at the left when off
* fix(share): settle the share status after a stop or resume in the open panel
* fix(comments): keep a comment that was handed to the agent
Sending a saved comment to the agent deleted it as soon as the send was
queued, so a run that failed or was stopped left nothing to retry and
pushed a tombstone to the share page.
The hand-off is now carried by status alone. The three send paths no
longer delete after queue acceptance; they only clear the composer and
the selection for the accepted ids. A run that fails, is reported
canceled, or is stopped by the user releases its comments back to open
through releaseCommentsAfterUnsuccessfulRun. A successful run still ends
in needs_review.
* fix(comments): keep an unsent comment draft when another element is picked
Picking another element, lassoing, opening a saved pin, or selecting a
comment in the side panel retargeted the composer at once and dropped
the draft, queued notes, and context without a word.
All four pick paths now retarget through requestComposerRetarget. While
the composer holds unsent work the first pick changes nothing and shows
a notice inside the composer; the next pick confirms the discard and
retargets. Editing the draft disarms the pending discard. Closing the
composer, Escape, and switching files behave as before.
* fix(daemon): walk nested HTML in share/deploy plans and refuse root index.html conflicts
Referenced .html/.htm files (iframes, frames) are now walked like the entry:
their references resolve against their own directory at any depth, so their
stylesheets, images and child pages ship and missing/invalid ones are
reported. Share mode rewrites their root-absolute references relative to
their own directory and maps references to the entry to the published
index.html; standalone deploy keeps their bytes.
Parsing is deduped on each file's real path, which bounds the walk even with
in-project symlink loops (x -> .) and also fixes the existing unbounded CSS
@import recursion through such links.
A reference to an existing root index.html while sharing another page no
longer overwrites the entry: the plan records the conflict (referrer file,
attribute, line, suggested free name) and deploy refuses it with 409
ENTRY_INDEX_CONFLICT carrying a copy-to-agent fix prompt.
* fix(daemon): refuse share entry/index.html conflicts before reserving
The share plan summary carries entry-index-conflict blockers (with the agent
prompt), and publish-public now builds the plan, and refuses on size or
blockers, before reserving an alias or preparing Vela. A conflict returns
409 SHARE_ENTRY_INDEX_CONFLICT with the details in error.data. Freshness is
not compared against a blocked plan.
* fix(daemon): print share blockers and agent prompt in the od CLI
od project share preflight prints each blocker with its copy-to-agent
instruction and exits 1 when blockers exist (also with --json). A publish
refused with SHARE_ENTRY_INDEX_CONFLICT prints the instruction readably in
text mode before the structured error envelope.
* fix(web): explain share index.html conflicts with a copy-to-agent action
The share panel shows the entry-index-conflict blocker from the share plan or
a 409 publish refusal, disables publishing while it stands, and copies the
daemon's agent prompt (falling back to showing it for manual copy). New
strings are translated in all 19 locales.
* fix(comments): release a comment whose queued send never becomes a run
A saved comment is written 'applying' when its send is queued, before any run exists. The release added for unsuccessful runs only fired on run end, cancel and stop, so a send held by the pre-run balance gate, stopped or failed before its run was created, or taken out of the queue left the comment 'applying' and out of the comment list for good.
Queue drains now go through startQueuedChatSend, which releases the comments of a send that ended without a run. A send the gate holds no longer reserves its comments, and removing or editing a queued send releases what it carried. A held send that starts later reserves them again when its run begins.
* fix(share): keep a reopened share busy until its link has been read
A resume answers without a link, so the panel re-reads the share state. The busy state ended with the resume response, and for the seconds the re-read took the panel said the link was temporarily unavailable before swapping to the link.
settlePublicShareAfterMutation now writes the active state only once the re-read has settled, and the publish flow stays busy on it. A re-read that fails or carries no link still ends on link unavailable, without an error.
* fix(web): size the copy-for-agent button to its longest label
The soft share button is a fixed 88px slot sized for short labels, so
"Copy fix for agent" overflowed in English and longer locales. Let the
button grow to fit, and stack both labels in one grid cell so the width
stays put while "Copied" shows.
* test(e2e): reopen a stopped share through the switch in the share audit specs
The share audit specs stopped a share inside the panel and then clicked
the publish button to bring it back, which is the behaviour the panel no
longer has: a stopped share reads as stopped and reopens through the link
access switch with a resume request.
The mocked publish responses also lacked the receipt the client parses,
so they now return the real response shape.
* test(share): align CI cases with current share UI
* test(share): align CI cases with current share UI
* test(web): pin comment time boundary to local noon
* fix(share): allow reopening saved comment after save
* fix(daemon): wallet 401/403 no longer expires the AMR session
Billing is display-only (OPEND-3553). Vela maps internal billing
failures to 403, so a wallet rejection now only reports the balance as
unavailable instead of marking the credential expired, which dropped
workspace context and redirected to sign-in.
* fix(daemon): billing summary failure no longer expires the AMR session
A failed 'vela billing summary' (even an auth-shaped 401) only drops the
optional plan/balance from /status. Credential expiry stays owned by the
non-billing workspace directory path (OPEND-3553).
* fix(daemon): billing projection 403 no longer flushes workspace authority
A rejected workspace billing snapshot only marks the billing runtime
entry. It no longer invalidates the workspace directory, exact authority,
or exact context caches with auth_reject; Vela also maps internal billing
failures to 403, and membership revocation stays owned by the directory
and the hub workspace-access-revoked event (OPEND-3553).
* fix(daemon): carry share content fingerprint across stop and resume
Resume reopens Vela's saved version of the same alias but writes a new
local publication revision, so the fingerprint row (keyed to the old
revision) made freshness permanently 'unknown' and hid the update entry.
Rebind the stored fingerprint to the resumed revision of the same slug so
later edits read as 'outdated' (OPEND-3517).
* fix(daemon): redact identity argv from vela command failures
Node's exec error repeats the full command line in message, cmd and
stack, and presence/member/catalog callers logged it verbatim, leaking
--display-name and activity payloads. The vela command runner now
rewrites the values of --display-name, --name, --email, --activity-json
and --metadata-json before rejecting (stderr and the upstream status
signal are untouched), and the presence failure log records only the
classification and upstream status (OPEND-3520).
* fix(daemon): refuse plain publish of a stopped share before uploading
When Vela reports the file's share as stopped, a plain publish-public POST
now answers 409 SHARE_STOPPED_RESUME_REQUIRED before pushing bytes, since
Vela rejects it with 409 share_binding_stopped anyway; reopening the link
is the explicit { mode: 'resume' } request. The share publish wrapper also
keeps the upstream 'API request failed with status NNN: code' line so the
failure classifies as upstream_http instead of reason=unknown (OPEND-3510).
* test(daemon): satisfy test typecheck for batch-2 share fixes
* fix(daemon): keep vela argv redaction from crashing on non-string args
Some media callers pass optional argv slots through as undefined. The
redaction must never replace the real command failure, so it skips
non-string entries (caught by vela-media-safety-process tests).
* fix(daemon): increase comment id entropy
* fix(share): keep deployment menu alive through pointer press (OPEND-3537)
* fix(share): unify owner Share entry glyphs (OPEND-3535)
* fix(share): show pending link-stop feedback (OPEND-3513)
* fix(share): retain scoped preflight across panel reopen (OPEND-3536)
* fix(comments): wait for preview targets before lost writeback (OPEND-3516)
* fix(share): restore Owner toolbar hierarchy (OPEND-3580)
* test(share): align toolbar style contract with C0 (OPEND-3580)
* fix(web): preserve owner scroll after comment location
* fix(cli): distinguish share size refusals from connection failures
* fix(web): separate share eligibility and preserve comment identity UX
* test(cli): preserve daemon connection failure exit code
* fix(share): expose owner-scoped persisted stop tasks
* fix(contracts): bound share anchors at original selector lengths
* fix(web): recover pending deleted-share stop notices
* fix(contracts): define atomic share pin snapshot metadata
* test(web): provide retry preflight fixture and await completion
* fix(web): keep billing upgrade outside account identity boundary
* fix(share): protect staging and restore client continuations (#8675)
* fix share staging and client continuations
* docs: clarify share batch verification baseline
* fix(web): notify when sending to a deleted project or conversation
* fix(share): dismiss card share popover on conversation scroll (OPEND-3784) (#8689)
* fix(share): keep anchored popover geometry current (OPEND-3783) (#8694)
* fix(share): expose freshness checks and parallelize state reads (OPEND-3791) (#8708)
* fix(share): preserve safe publish errors and actionable messages (OPEND-3796) (#8709)
* fix(share): preserve safe publish failure details and actionable messages
* test(share): align failure copy expectations and cover classified messages
* fix(share): retain Button binding required by CI merge
* fix(share): add bounded state-read and resume diagnostics (#8710)
* Revert "fix(share): expose freshness checks and parallelize state reads (OPEND-3791)" (#8711)
This reverts commit 8f6c09db39.
The reported latency bottleneck remains unproven. Revert the full PR at the user request and defer presentation optimization. Preserve the subsequent OPEND-3796 changes.
* fix(share): preserve entry binding checks when opening Share (OPEND-3594) (#8712)
* fix(share): report preflight failures and allow explicit retry (OPEND-3771) (#8716)
* fix(share): use green for update-link action (OPEND-3790) (#8717)
* fix(share): retain confirmed PDF export guide during history refresh (#8719)
* fix(analytics): add passive share and comment observations (#8723)
* fix(analytics): define passive share observations and contain measurement failures
* fix(analytics): observe share receipts and selected comments without changing actions
* fix(web): isolate after-export guide observation from actions
* fix(analytics): connect passive guide observations and document minimized fields
* fix(analytics): observe saved comment creation and run handoff passively
* fix(analytics): exclude edited composer drafts from saved-comment attribution
* fix(e2e): align Team run fixtures with authoritative workspace catalog
* fix(analytics): wait for comment acceptance readiness in regressions
* fix(share): 修复超限评论恢复与资源读取失败,补验四项验收问题 (#8721)
* fix(share): recover oversized comments and reject unreadable dependencies
* fix(share): close real export completion and cloud failure gaps
* test(share): align export and delete relay regression assertions
* fix(export): handle interrupted ZIP saves in preview actions
---------
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: a1chzt <chizblank@gmail.com>
* test(share): align export failure assertions with current copy
* fix(share): guard user-scoped export guide dismissal (OPEND-3579) (#8738)
* fix(share): guard user-scoped export guide dismissal (OPEND-3579)
* test(web): align export guide failure assertions with product copy
* test(web): settle restored share state before publish assertions
* fix(chat): keep artifact Share actions static (OPEND-3854) (#8747)
Keep ChatPanel artifact Share actions static and remove card-only publication state tracking. Preserve the existing share panel and readonly/generation gates.
* Merge pull request #8750 from nexu-io/fix/opend-3888-comment-age
fix(web): preserve comment creation age (OPEND-3888)
* fix(comments): persist new static comment identities (OPEND-3516) (#8768)
* fix(comments): preserve authored identities through manual and AI edits
* fix(comment): prepare durable static source identities (OPEND-3516)
* fix(comments): integrate durable identity lifecycle (OPEND-3516)
* fix(comment): validate authored HTML identity tokens (OPEND-3516)
* fix(comment): mask raw text before authored tokenization (OPEND-3516)
* fix(comments): enforce prepared document and publication barriers (OPEND-3516)
* fix(comments): revalidate pending documents and guard updates (OPEND-3516)
* fix(comments): require app-owned source preparation consent (OPEND-3516)
* fix(comments): contain preparation consent focus (OPEND-3516)
* fix(sharing): diagnose project bootstrap failures safely (OPEND-3516)
* fix(comments): retire stale publication guard alerts on explicit retry (OPEND-3516)
* fix(comments): locate Personal identities in the live preview (OPEND-3516)
* fix(comments): surface lost Owner identity anchors (OPEND-3516)
* fix(comments): unblock publication after strict target loss
* fix sharing to keep file publication independent of team visibility (OPEND-3855) (#8759)
* fix sharing to keep file publication independent of team visibility (OPEND-3855)
* refactor sharing to one explicit policy path (OPEND-3855)
* test: isolate metadata snapshots from the trailing identity directive
* fix: align collaboration consumers with explicit project visibility
Carry trusted catalog roots through content receivers and suppress stale Team rows after explicit privacy changes. Read shared design-system brand assets from their resource snapshots. Align stopped-publication ordering and protocol/browser fixtures with the existing contracts.
* test: await explicit comment preparation and independent UI states
Complete the existing source-version consent and real frame-readiness flow before opening the comment sidebar. Bind the response waiter to the UI actions so failures retain their real cause. Await the independently loaded account callout and assert exact catalog-bound materialization wiring.
* test: expect private policy when unsharing a team project
Await the explicit visibility-private command instead of deleting the retained root registration. Assert that remove was not invoked and preserve all member quarantine, directory and file-read privacy checks.
* fix(comments): synchronize file lifetimes across member and public shares (#8764)
* fix sharing to keep file publication independent of team visibility (OPEND-3855)
* fix(comments): synchronize current file lifetimes across members and shares
OPEND-3870 / OPEND-3871. Stack on published OPEND-3855 dependency; preserve reviewed source tree exactly. Local validation does not establish PostgreSQL or native acceptance.
* refactor sharing to one explicit policy path (OPEND-3855)
* fix(collab): adapt comment sync to converged project lifecycle
* test: isolate metadata snapshots from the trailing identity directive
* fix(collab): preserve nested publication and pulled file authority
* fix: align collaboration consumers with explicit project visibility
Carry trusted catalog roots through content receivers and suppress stale Team rows after explicit privacy changes. Read shared design-system brand assets from their resource snapshots. Align stopped-publication ordering and protocol/browser fixtures with the existing contracts.
* fix(collab): preserve relay identity and retire captured file batches atomically
* test: await explicit comment preparation and independent UI states
Complete the existing source-version consent and real frame-readiness flow before opening the comment sidebar. Bind the response waiter to the UI actions so failures retain their real cause. Await the independently loaded account callout and assert exact catalog-bound materialization wiring.
* test: expect private policy when unsharing a team project
Await the explicit visibility-private command instead of deleting the retained root registration. Assert that remove was not invoked and preserve all member quarantine, directory and file-read privacy checks.
* fix(collab): retain the actual native root during mirror materialization
* fix(collab): fail closed on unbound active share identity
* fix(collab): retire comment lifetimes after local mutation commits
* fix(collab): commit local artifact invalidation with working-root changes
* fix(collab): reconcile committed root retirement without stopping new links
* test(collab): align CI fixtures with admitted file identities and receipts
* test(collab): model receipt-backed root file admission in the shared hub
* test(collab): distinguish bound and unbound publication recovery
* chore(pack): pin compatible test CLI and exclude dependency dotenv files
* test(share): verify paired CLI policy and immutable share contracts
* fix(share): remove comment preparation interruptions
* fix(web): offer after-export guide when sharing history fails
* fix(comments): preserve unknown ages and repair synced creation times
* style(web): restore comment reference visuals without changing behavior
* test(web): verify colored avatars and document deployed parity
* fix(share): collect literal JavaScript image dependencies
* fix(web): match canvas comment pins to author avatar colors
* test(comments): align existing UI and E2E color expectations
* test(share): align collab flow and snapshot with direct comments UI
* test(campaign): await verification before advancing impression frames
* test(cms): await verification and mount before impression frames
* fix(web): match canvas comment initials to sidebar avatars
---------
Co-authored-by: PerishCode <perishcode@gmail.com>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: alchemist <alchemist@alchemistdeMacBook-Pro.local>
* fix(od-next): continue plans with keyed markers and honor explicit deliverables
* fix(od-next): retire output contracts and hand off continued tasks
* fix(od-next): record round settlement and recover production cold
* fix(od-next): resume marker sessions with context deltas
* fix(od-next): separate completion facts and honor plan-only intent
* fix(od-next): preserve physical failures and legacy chat compatibility
* test(web): align run recovery fixtures and await campaign mount
* fix(od-next): tolerate terminal production marker formatting
* fix(od-next): unblock follow-ups after a stop or unreadable history (#8423)
Treat a previous task this build cannot read as absent so the follow-up starts a new task, and stop counting a Run with a pending cancel as holding its conversation. Add end-to-end coverage for the marker continuation flow and restore the remote Workspace directory gate cases.
* docs: clarify focused local validation
Keep local daemon and web validation scoped to affected files and integration boundaries. Reserve full suites for PR CI unless the user explicitly requests them.
* fix(web): scope folded task completion to its final run
Keep the final physical Run key and conclusion together when OD Next history is folded or accumulated live. Reuse the existing predecessor offsets while retaining the full display history and remaining Todo snapshot.
Add regression coverage for completed and unfinished production, live continuation, multiple Runs, legacy history, wrong and quoted markers, question handoff, and failed or canceled Runs.
Validated the parent regression before the fix, 165 focused Web tests, workspace typecheck, and guard.
* fix(od-next): compact historical context without restricting clarification
Reuse confirmed artifact summaries for old assistant history, preserve cross-agent plans and original requests, and represent earlier questionnaires as neutral historical information without inferring answers.
Keep user replies, skips, changed answers, unpersisted bodies, examples, and unfamiliar content intact. Do not add question repetition checks, output gates, repair rounds, or limits on clarification.
Validated red regressions, shared context and Web provider tests, focused handoff/resume/cold-production coverage, real server and fake CLI input, workspace typecheck, and guard. The repeated-question server fixture remains successful.
---------
Co-authored-by: bone3deep1962-collab <bone3deep1962@gmail.com>
Co-authored-by: Cheems <hihicheems@gmail.com>
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
A daemon that dies of a V8 OOM cannot report itself, and today nothing
records heap usage, SQLite growth or why the previous daemon ended. Add a
persist-then-report health checkpoint:
- sample heap spaces, RSS and event-loop delay (60s and after major GC),
flag pressure levels and jumps, and keep a synchronous in-flight marker
for large reads so an OOM inside one stays attributable
- opt the daemon into Node's fatal-error report once listening; the next
boot reduces it to controlled fields, deletes the raw report, and
reports the previous session as daemon_health_summary or
daemon_unclean_exit (carried forward across crash loops)
- add storage_* fields to the emitted run_finished (and its startup
replay) measured with SQLite octet_length, plus a constant-cost page
snapshot (daemon_storage_snapshot)
- bundle the checkpoint in the diagnostics export
Observation is additive: existing return values, rows, HTTP responses,
event properties, recovery snapshots, logs and exit statuses are
unchanged, and every hook isolates its own failures.
Refs OPEND-3402
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(daemon): capture per-request request_id + token usage for claude_code runs
The claude-stream-json parser read every assistant message's `message.id`
only to dedup streamed text/thinking and then dropped it, and never read
`message.usage` at all — so a run with N model requests collapsed into a
single run-level `result.usage` record with no request id. That left
request-level cost/percentile analysis stuck at run-level (~0.9% coverage).
Surface the per-request granularity the wire already carries:
- claude-stream.ts emits a new `request_usage` event per assistant message,
keyed by `message.id` (the provider `msg_…` request id), carrying the
per-message input/output/cache tokens via a `perRequestUsageFrom` helper.
- packages/contracts adds `request_usage` to both the live SSE
`DaemonAgentPayload` and the `PersistedAgentEvent` union; server.ts maps
the live event to its persisted form.
- mocks/lib/format-claude.mjs now emits per-message `usage` summing to the
run-level `result.usage`, so the capture path is verifiable by local
replay without burning provider budget.
The per-request token sum reconciles against the run-level `result.usage`
aggregate — the correctness anchor. New daemon test plus regenerated claude
golden cover both the synthetic stream and the mock-replay path. No change
to run execution; only telemetry/contract surface is added. claude-stream-
json only for now; other runtime families ship per-request shapes in
follow-ups.
Closes#4610
* feat(daemon): thread per-request usage through Langfuse + PostHog run telemetry
Addresses review on #4620: the parser emitted the per-request records but they
stopped at SSE/message persistence and never reached the telemetry sinks named
in #4610's acceptance criteria, so PostHog/Langfuse still had no per-request
request_id + token coverage.
Wire the records through the existing run telemetry choke point:
- run-analytics-observability.ts adds `scanRunEventsForPerRequestUsageAnalytics`,
a pure scanner that collects the per-request records keyed by request_id and
checks the reconciliation invariant (per-request token sum == run-level
`result.usage`). Both sinks read this one source.
- langfuse-bridge.ts `collectAgentEvents` exports one `agent-request-usage`
Langfuse event per model request, keyed by the provider request id.
- routes/runs.ts adds `run_finished` coverage properties
(`request_usage_count`, per-request token sums, and
`request_usage_reconciles_aggregate`) so the PostHog request-level surface
lifts off the ~0.9% floor; contract fields added to RunFinishedProps.
Test extended: a multi-request claude run produces telemetry records keyed by
request_id whose token sum reconciles with `result.usage`, plus a
non-reconciling guard. langfuse-bridge + run-analytics suites stay green.
* fix(daemon): fold cache tokens into per-request usage reconciliation
Addresses review on #4620: request_usage_reconciles_aggregate compared only
input/output tokens, so a run whose prompt/completion tokens matched but whose
cache creation/read tokens drifted still reported `true` — letting the new
PostHog/Langfuse telemetry certify the #4610 invariant when part of
`result.usage` had not reconciled.
scanRunEventsForPerRequestUsageAnalytics now reads the aggregate
`cache_creation_input_tokens` / `cache_read_input_tokens` and folds every field
`result.usage` reported into the boolean (each compared only when the aggregate
carries it, mirroring the input/output leniency). Added a fixture where cache
tokens mismatch while input/output still match, so the flag stays false and
this cannot regress.
* feat(daemon): map request_usage to durable PersistedAgentEvent
* feat(daemon): wire per-request usage rollup into run_finished analytics lifecycle
* ci: trigger fresh validation run for approved changes
* feat(daemon): preserve per-request usage in truncation-proof run side ledger
* fix(daemon): preserve terminal delivery lifecycle evidence
* fix(daemon): preserve per-attempt terminal evidence
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(daemon): preserve terminal attempt and persistence boundaries
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(daemon): settle terminal claims before capture
Defer the one-shot run_finished envelope by one check phase so adjacent
duplicate and late terminal claims are included in telemetry, and bind the
daemon lifecycle type directly to the shared API contract.
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(daemon): preserve replay and teardown evidence
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(analytics): separate admission phase from failure attribution
* fix(analytics): recognize ACP replay boundary
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(analytics): retain admission evidence across truncation
Preserve terminal-attempt evidence before the run event ring buffer truncates, and recognize guarded stdout plus persisted artifact activity.
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(analytics): flush Antigravity output before close errors
Emit guarded non-OAuth stdout before terminal close errors so admission evidence survives in both the live ledger and durable event log.
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(analytics): require admission attempt boundary
Keep fallback admission phase unknown when no start event proves that an attempt began.
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(analytics): honor confirmed ACP model selection
Use the final pre-error ACP model status so session/new metadata does not invalidate a subsequently confirmed requested model.
Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
* fix(daemon,web): report analytics for daemon-started Runs and mid-write Labs toggles
An OD Next task runs as several physical Runs — the user's request, then the
repair and production stages the daemon starts itself. The Run analytics
lifecycle was installed inline in POST /api/runs, so only the stage a client
asked for ever reported: volume, success, failure, cancellation, duration and
artifact counts were all computed on the request stage alone.
Walking every Run-creation site rather than only the one the report named found
three more callers in the same position — scheduled Automations, live-artifact
refreshes, and POST /api/chat.
Extract the lifecycle into routes/run-analytics-lifecycle.ts and install it from
internalRunCreation.start, which is now the only way a physical Run starts. Its
analytics argument is required, so a new caller has to state its identity —
including stating that it has none — instead of silently dropping the Run, and
a guard check keeps the bypass from coming back.
A daemon-created Run inherits its caller's identity and its task lineage from
the Run that caused it, resolved through the same deterministic fallback the
lifecycle itself applies. Reading the source Run's recovery record instead would
race: the lifecycle re-reads host facts before capturing, so a short Run hands
off before its own record exists.
Also split the Labs mounted guard. It sat in front of the toggle event and the
dialog's save pill as well as this component's state, so leaving the section
while the write was in flight lost the event and left the pill on "Saving" for a
preference the machine already held.
* refactor(daemon): move the run analytics lifecycle to its owning domains
Review follow-up. `createRunAnalyticsLifecycle` takes no Express request or
response, so by `apps/daemon/AGENTS.md` it is a reusable daemon service and
belongs under `src/services/`, not `src/routes/`. The run/project record shapes
it shares with the run routes describe a Run, not an HTTP exchange, so they move
to the runs domain.
The seam also carried a second copy of the analytics facts, with its context,
snapshot and rollout fields widened to `unknown` so the two definitions could
drift. `RunAnalyticsFacts` is now declared once by the lifecycle and reused by
`InternalRunCreationService.start`. Consolidating immediately surfaced what the
weak copy had been hiding: the route was passing a snapshot shape the lifecycle
did not actually accept, and the test was passing an analytics context missing
four required fields. Both are now typed at the choke point.
* fix(web): keep a late Labs save from taking the shared indicator
Review follow-up. Letting the Labs write settle after unmount fixed the stuck
"Saving" pill but handed an older request the ability to relabel a newer one:
hold the Labs PUT, switch section, start another save, release. The old
completion wrote `saved` over the newer save and its timer then forced `idle`,
which is worse than the pill it was fixing.
`SettingsDialog` owns one indicator for every section and already refuses to let
a superseded save win — its own path checks `autosaveLatestRef` and its idle
timer is a compare-and-set. Labs bypassed both, and its timer wrote `idle`
unconditionally.
The dialog now hands out a claim when a writer takes the indicator, and only the
current claim may settle it. Labs captures its claim when the write starts, not
when it finishes. Comparing the displayed value would not do: a newer section
can legitimately be showing the same `saving` the older write left behind.
The analytics event still fires after unmount — that half was never about the
indicator.
* fix(daemon): make the run analytics seam impossible to opt out of by omission
Review follow-up. The choke point documented an invariant it did not enforce:
`analyticsLifecycle` was optional and invoked with `?.`, so a future factory
could build the service, satisfy the new `.runs.start` guard, and still emit
neither `run_created` nor `run_finished` — the exact failure this PR exists to
close. `RunAnalyticsFacts.requestAnalyticsContext` had the same gap: the
contract says a caller with no attributable identity passes `null` explicitly,
while the type let it drop the key instead.
Both are required now. A harness that wants silence injects a no-op lifecycle
and says so. Making the context required immediately caught a service spec that
had been omitting it.
* fix(web): preserve autosave ownership across async saves
* 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 8bc7805e62, but the
client-facing StrategyTaskProjectionV2 dropped it, so the UI could not
tell a blocked task from any other failure. Add the optional
blockedContext field to the projection contract (additive, strict shape
mirroring the task store) and have projectStrategyTask carry it through
every run-status / SSE assembly point, only on a blocked outcome.
* fix(web): terminate question-form interaction when a strategy task is blocked
A blocked OD Next task is a sticky terminal verdict, but the web client
kept the turn's discovery/clarification form fully submittable; the
answer then 409'd with STRATEGY_TASK_STATE_MISMATCH and surfaced only
the generic 'Task failed' card.
- ChatMessage gains additive strategyTaskBlocked / strategyTaskBlockedText
fields; strategyBlockedMessageFields derives them from any terminal
blocked projection (visibleText preferred, trimmed, else null).
- streamViaDaemon/reattachDaemonRun expose onStrategyTaskSettled with the
terminal task projection; ProjectView stamps the blocked verdict onto
the assistant message on live settle, on reattach settle, and on the
reattach status probe, and persists it so it survives reloads.
- AssistantMessage disables the inline question form for a blocked turn
(including the auto-continue countdown) and renders the gate's visible
text - or a generic localized notice - under the form.
- resolveRunFailureUi maps STRATEGY_TASK_STATE_MISMATCH to dedicated
halted-task copy instead of chat.runError.title.generic.
- New i18n keys (title, 409 message, form notice) added to types.ts and
all 19 locales.
Blocked-state interaction shape is a minimal safe default pending a
product decision: disable submission and explain, no auto-retry, no
degraded re-run.
* fix(daemon): normalize premature clarification execution mode
The observed 2/2 field failure on vague first prompts: the agent emits a
well-formed Runtime State block for clarification_required but predicts
"executionMode": "simple" alongside it. The mode is daemon-locked only
after clarification resolves, so the prediction carries no authority — yet
strict schema validation classified the whole block as
od_next_protocol_runtime_state_invalid_schema (non-repairable) and
dead-ended the task. The protocol stream now discards exactly that field
before validation, records the normalization for attribution, and the
prompt recipe gains an explicit rule plus a clarification-shape Runtime
State example so agents stop copying the plan_ready example's mode.
* fix(strategy): declare the runnable-entry deliverable convention to agents
The deliverable validator requires a root-level runnable entry (root
index.html for html deliverables), but neither the frozen prompt bundle nor
the production continuation ever stated that convention — the agent was
told only 'editable prototype source with a stable runnable entry', built a
framework scaffold in good faith, and the completed task then blocked with
od_next_canonical_deliverable_invalid. The production continuation and the
prototype Task Skill now state the root-entry convention and forbid
unplanned framework/dependency scaffolds, so the contract the daemon
validates is the contract the agent was given.
* fix(daemon): stop the rollout only for real machine-contract failures
rolloutStopSignalForBlockedContinuation mapped every reason code containing
'protocol' to machine_contract_leak, whose stop mode is a hard instance-wide
off that survives restart. Two vague prompts were therefore enough to
silently disable OD Next for the entire daemon: the agent emitted a
clarification runtime state carrying a premature executionMode, the task
blocked, and every later request in the daemon fell back to the legacy
plugin with no UI signal and no automatic recovery.
The machine block never reaches the user — the protocol stream suppresses
undelimited and oversized bodies — so an agent turn that omits, duplicates,
or mis-populates a block is a per-task defect, already fail-closed for that
task and now attributable from the store. Only the two block-delimiting
failures keep raising the leak signal.
* fix(strategy): state the entry-resolution constraint without inventing a product rule
The previous wording told agents that a prototype's runnable entry must be a
root index.html and that framework scaffolds are forbidden. Neither is in the
product spec, which says only that the user must be able to open and use the
deliverable from a real entry point, and neither matches the validator, which
resolves an entry through root index.html, then a single root-level html file,
then a single kind-matching file. That turned one fallback branch into a
product constraint the agent then had to obey.
Both surfaces now state the resolution rule the daemon actually applies and
why a delivery is rejected without it, and take no position on whether a
prototype may be an application project. Whether the validator should also
resolve framework-project entries is a product decision and stays open.
* fix(strategy): assemble the OD Next prompt bundle as an XML tree
The first-turn payload was an XML envelope around four flat CDATA blobs,
with one of them holding 25.3KB of joined Markdown (89% of the bundle).
Three layers each threw structure away: the contract typed the bundle as
four strings, the daemon joined 15 individually-named contributors with
'\n\n---\n\n', and the recipe re-joined 10 sections while demoting the
stage/atom map to Markdown headings. The result inverted heading levels in
7 of 46 places and put a per-task UUID 1,268 bytes into the payload, which
pushed 23KB of byte-identical strategy content past the point where a
provider prompt cache can share a prefix.
Introduce a canonical XML tree primitive and a v2 bundle schema that nests
one element per spec slot. Because system_prompt is now the shared cache
prefix, it may hold only cross-task-constant content, so the recipe
identity, runtime planning facts, runtime tool environment, client system
prompt and form override all move into context, and user_prompt moves last.
Measured on the real strategy assets: 28,325 -> 26,010 bytes, 4 -> 16 CDATA
leaves, 7 -> 0 heading inversions, and the first byte that differs between
two tasks moves from offset 1,268 to 24,709 (past the end of system_prompt).
Persisted rows keep working: final_text_schema is dispatched per row, so a
v1 bundle still parses at its own version while new tasks mint v2.
* feat(daemon): let the main Agent choose the task route
Product spec 3.1 makes the main Agent the party that decides Direct Edit vs
Full Plan, and 6.1 puts that decision inside MVP scope. The daemon instead
locked the route before the Bundle was ever sent: routes/runs.ts passed
preference 'full_plan' with all five Direct Edit eligibility facts hardcoded
false, and decideStrategyRequestRoute short-circuits on that preference, so
Direct Edit was structurally unreachable. A local edit on an existing
artifact therefore paid a full planning round — measured at 2 Request Turns
and 61s for a colour change that the Agent itself correctly scoped to one
property.
Four of the five eligibility facts require reading the request, which the
daemon cannot do before dispatch. So the route now stays unlocked through
the request turn — a state the task store and the projection contract both
already model — and the daemon adopts the route the Agent declares in its
Runtime State. Every later turn keeps the existing lock: a route may never
change once set.
The Full-Plan-only guards for clarification inference, serialization repair,
and the repair anchor now also accept the unrouted first turn, since each of
those paths is itself a Full Plan declaration and locks the route as it
transitions. blockTask settles an unroutable turn on the spec's documented
fallback route rather than refusing to record it.
No contract field, DB column, or prompt byte changes: the recipe already
tells the Agent to choose the allowed route on the request stage, and
Runtime State has always carried .
* fix(strategy): tell the Agent it owns the request-stage route choice
Unlocking the route in the daemon was necessary but not sufficient. The
recipe still read "use the supplied task facts to choose the allowed route"
and immediately followed with "For a Full Plan route, the request and
clarification stages are planning-only", so the Agent kept assuming a Full
Plan had already been chosen for it. Verified through the real UI: an
unambiguous local edit still came back as full_plan with the Agent stating
'this turn will not modify files'.
The request-stage section now says the choice is the Agent's, that Open
Design leaves the route unset until the first Runtime State declares it,
and that the active orchestration Skill's eligibility conditions decide
between the two. It states the mechanism and defers the criteria to the
Skill rather than restating them.
Verified end-to-end in Chrome against a real codex run: a local colour edit
on an existing artifact now routes direct_edit / simple and completes in
ONE Request Turn (50s), matching spec 3.4. Ambiguous requests still route
full_plan, and a request needing clarification still routes full_plan
because Direct Edit is request-only.
* fix(observability): restore failed-Run process evidence in the task hierarchy
PR #7016 replaced the per-Run Langfuse trace with a task hierarchy
(`strategy-task:<taskExecutionId>` + `strategy-stage:*` child spans) and
durably suppresses the single-Run trace. Failure classification survived
the move, but the terminal process evidence the single-Run trace carried
— stderr/stdout tails, host close diagnostics, exit code and signal —
did not reach Langfuse at all, so a failed Run lost the only facts that
explain *why* it failed.
Carry those facts through the existing safe-projection boundary:
- `SafeRunQualityV1` gains an optional `process` section
(`exitCode`, `signal`, `stderr`, `stdout`, `diagnostics`), with
`SafeObservationStreamTailV1` and a bounded `SafeRunDiagnosticsV1`
record that admits only booleans and identifier-shaped values.
- `buildSafeRunQualityProjectionV1` builds it. Stream tails reuse the
producer's existing 20-line / 4 KiB cap plus `redactSecrets`, then add
the wider Prompt-stack local-path masking before `safeQualityText`.
stderr follows the run error-message rule; stdout is the agent's own
output and needs content consent, reporting its line count with an
explicit limitation when withheld.
- `buildSafeRunQualityProjectionFromDaemon` collects the same summaries
the single-Run bridge collected, and the aggregation projects them
onto the `strategy-stage:*` span metadata for both the legacy and OTLP
exporters.
The smoke test now asserts the task-hierarchy shape (trace id, child
span id/name, failure metadata keyed by runId) and re-asserts the
diagnostics coverage there, including that a local path in stderr is
masked before transport. Each case gets its own daemon and `OD_DATA_DIR`
via `vi.resetModules()`: the OD Next rollout stop latch lives in SQLite
and fires per daemon instance, so a shared data root silently pushed
later cases onto the compatibility single-Run reporter. The one case
that legitimately stays on that reporter (deepseek, outside the OD Next
runtime path) now asserts the same coverage on the single-Run trace.
* test(daemon): make the run-failure smoke deterministic under load
The failed-run smoke could report `expected 'process_exit' to be 'auth'`
under CPU pressure. Two independent sources, both closed here.
1. The fake `claude` was not a valid OD Next Claude runtime. `detectAgents`
parses `claude -p --help` for every flag in the runtime def's
`capabilityFlags`; once a physical Run is mapped to an OD Next strategy
task, `buildArgs` sets `observeNativeChildBehavior` and THROWS when the
probe never saw `--forward-subagent-text`. The fake advertised neither
that flag nor `--agents`, so whenever the task mapping was already
visible to `startChatRun` the Run died on that host-side TypeError
before the agent was ever spawned — reported as `AGENT_EXECUTION_FAILED`
/ `process_exit` instead of the auth, rate-limit or timeout failure the
case scripted. Captured in the wild on the `hang_timeout` case
("Claude native Child behavior observation requires advertised
--forward-subagent-text support.", 9 events, no spawn) and reproduced
deterministically against `buildArgs`: old help text throws, derived
help text builds args containing the flag.
The fake's help text is now derived from `claudeAgentDef.capabilityFlags`,
so it cannot drift from what the daemon probes for and the Run always
reaches the scripted failure regardless of mapping order.
This gate pre-existed the task-hierarchy change — the same fake and the
same daemon check are on `main`. But that commit gave every sub-case its
own daemon and data root, which keeps the OD Next rollout active for all
five instead of only the first, so exposure went from one Run per file
run to five.
2. The events log was sampled at an arbitrary moment. `GET /api/runs/:id`
publishes terminal status before the buffered `events.jsonl` write has
necessarily reached disk, so re-deriving a classification from that file
could see a log whose error record was still in flight.
`readCompletedRunEvents` waits for the log's own completion marker — the
`end` record the daemon writes last, before closing the stream — rather
than for a fixed delay or for the assertion to pass.
Each case now also asserts the daemon's own settled verdict
(`run.failureCategory` / `failureDetail` from the terminal status), which is
what the chat UI, the persisted message and telemetry actually consume. The
one case rejected before the runtime finish path declares
`daemonPublishesVerdict: false` rather than tolerating a null.
Verified under load average ~45-50 on a 10-core box (22 spinners plus a
concurrent full daemon suite): case 1 alone 6/6, file + tests/observability
6/6, file + observability + telemetry-message-finalization +
prompt-telemetry 5/5, every case in isolation 4/4.
* refactor(strategy): reshape the OD Next Prompt Bundle to the PRD layout
The emitted v2 bundle wrapped the cache-stable head in a `system_prompt`
element the PRD never asked for, and named three blocks differently from
the spec. Adopt the PRD-literal naming with the wrapper removed:
open_design_prompt_bundle
open_design_core_system_prompt (was system_prompt/core_system_prompt)
execution_boundary / native_execution
discovery_and_planning_surface / core_strategy
output_contract / echo_guard (moved in: both are output constraints)
session_skills
active_stages
task_metadata (was task_config)
context
user_first_prompt (was user_prompt)
Block order stays load-bearing: the first three blocks are byte-identical
across tasks of the same strategy version, task type, and execution
profile, so the shared provider-cache prefix now runs through the end of
`active_stages` instead of `</system_prompt>`.
Three noise cleanups land with the reshape:
- `frozen_skill_package` and `request_input_facts` are omitted when they
carry no substantive content, generalizing the rule the PRD already
mandates for `attachments`. An empty Skill roster and an all-zero
workspace/comment/MCP state cost ~540 bytes and told the model nothing.
- `recipe_identity` drops `strategy_package_hash`, `task_skill_digest`,
and `stable_prompt_identity`. They are audit-only; observability and the
persisted strategy task record keep them.
- Prompt text naming the old tags is updated: the echo guard constant, the
form-override strings in both call sites, and the map notes.
Schema id `open-design.od-next-prompt-bundle/v2` is unchanged — it has
never shipped — and the v1 parser stays intact for persisted rows.
* test(daemon): make ingestion waits report what actually arrived
A bare "timed out waiting for the task observation owning run <id>" does not
say whether the daemon delivered nothing, delivered the compatibility
single-Run trace instead, or delivered the span under a different Run — so a
CI-only occurrence costs a reproduction cycle to classify. Both ingestion
waits now append the received trace/span/generation ids, names and runIds to
the timeout message.
Diagnostics only; no assertion, budget or behaviour change.
* fix(daemon): gate OD Next admission on what the installed CLI advertises
Making OD Next the default design strategy turned every prototype/ppt/
marketing/hyperframes Run with `claude` into an OD Next Run, and every
admitted Claude strategy Run asks the CLI for native Child behaviour
observation. `claude.buildArgs` refuses that unless the installed build
advertises `--forward-subagent-text`, and the refusal is a thrown
TypeError inside the spawn path, so the Run dies as
AGENT_EXECUTION_FAILED before the agent ever starts.
Admission never established that fact. `runtimeCapabilityVerified` was
derived from a bundled capability fixture keyed by runtime path + agent
id + adapter version, plus an `ensureDetectedRuntimeVersions` probe that
reads only `--version`. The fixture proves what the claude-code runtime
*path* can do; it says nothing about the build on the user's disk. So the
daemon admitted a task it could not launch.
The symptom hid on developer machines. A real Claude Code install had
already populated the process-wide `agentCapabilities` map through full
detection, and that stale entry answered "advertised" for a fake CLI that
advertised nothing — which is why four pre-existing daemon suites and one
e2e suite passed locally and failed on CI hosts with no Claude Code
installed. Ambient developer state was deciding the verdict.
Establish the fact at admission instead. `ensureDetectedRuntimeCapabilities`
runs the same bounded `-p --help` read full detection already uses, keyed
by the resolved launch path so a configured CLAUDE_BIN cannot be answered
from another binary's cached probe. An installed build that does not
advertise what OD Next will demand loses admission and takes the ordinary
route, reported as `od_next_rollout_capability_advertised_capability_missing`
with the missing keys on the `[od-next-rollout]` line.
`customAgents` is required alongside `forwardSubagentText` because a simple
task may escalate to complex after planning, and discovering that gap at
the production stage would strand the task mid-flight.
Red spec: apps/daemon/tests/od-next-advertised-capability-admission.test.ts
* fix(daemon): let a re-entered turn resume its own session
latestCompletedAssistantMessageId excludes the caller's in-flight assistant
placeholder so it cannot count as conversation advancement. Its
resumableMessageId parameter exists to admit the stored session cursor back
through that filter — but the exclusion was evaluated first, so when the
placeholder IS the cursor the row was already gone and the admission never
applied.
That collision happens when a daemon-internal restart re-enters
startChatRun with the same assistant message after that message was already
persisted as the session cursor: post-tool recovery writes the cursor, and
nativeSessionContinuePending is consumed once, so a later safe-retry restart
arrives without it and falls through to the ordinary guard. The live session
was then abandoned and the whole transcript re-seeded. For an OD Next
continuation it is worse than a slow turn — the non-request stage refuses to
cold re-seed and blocks the task instead.
The exclusion now yields to the admission for exactly that row. Advancement
detection is untouched: any later succeeded message still wins on
position DESC and still fails the cursor comparison, which a new test pins
so the fix cannot later be simplified into an unconditional cursor match.
* fix(web): hide internal OD Next rollout controls
* fix(daemon): stop OD Next from discarding delivered work
Seven defects found by running the four OD Next runtimes end to end. Each one
silently threw away a turn that had actually done its job.
- OpenCode wrote deliverables outside the project. It resolves its workspace by
walking up to the nearest enclosing git root rather than using its process
cwd, so a managed project directory — never a repository, and in a
development install nested under the checkout — made it adopt the whole
repository. The project stayed empty, artifact detection saw nothing, and the
Run reported `no_artifact`. The BYOK definition already carried this
workaround; the native one was missed. Sent unconditionally like BYOK does:
gating it would turn a missing flag back into silent data loss, where an
OpenCode build without `--dir` should fail loudly at spawn.
- The `--help` capability probe read only stdout. OpenCode writes its entire
help to stderr, so every one of its advertised flags resolved to `false` and
the capability mechanism was inert for that runtime — `--dir` above, and
`--dangerously-skip-permissions` on builds that support it.
- Optional argv flags were dropped on a cold daemon. `agentCapabilities` was
only warmed by full detection, so a turn that spawned before Settings or
`/api/agents` built argv from an empty map. Resolve the advertised flags on
the run path first.
- One ordinary simple Run could disable OD Next daemon-wide. A child-evidence
coverage gap latched the rollout stop regardless of execution mode, but only
the complex production gate consumes per-child coverage; a simple task has no
Child agents, so `unavailable` there is accurate missingness. Recovery needed
an operator `od strategy rollout reset`.
- A Direct Edit that delivered but never declared it was refused. The agent
writes the canonical deliverable, `validateRunDeliverable` resolves it, and
then the turn carries no machine block at all. No repair could rescue it:
serialization repair needs a recovered Plan Contract to anchor on. Infer the
completion from delivery Open Design verified itself, fail-closed on every
weaker signal.
- A production block named nothing. Every other blocking path persists its
reason codes; this one did not, so the most common production block reached
the client anonymous.
- The web client remapped a blocked verdict onto a Run that had succeeded and
delivered, hiding the artifact behind a generic failure card.
Also: tell the request stage the canonical-deliverable rule that judges a
Direct Edit completion. It shipped only in the production continuation, so
Direct Edit agents were graded on a contract they were never given.
Blocked diagnostics now report which machine blocks the turn actually emitted,
because "malformed block" and "no block at all" have different remedies and the
reason code alone cannot tell them apart.
* feat(daemon,contracts): expose which turn a strategy Run belongs to
A Full Plan task runs as several physical Runs (request -> production) that
the user asked for once, and the daemon-issued continuation carries no user
prompt of its own. Clients had no way to tell that those Runs are one
conversation turn, so the continuation surfaced as an answer nobody asked
for.
The daemon stays the single writer of one message per Run. It now also
reports the logical-task position alongside each message
(strategyTaskExecutionId + strategyTaskRunIndex, resolved in bulk from the
Run mapping) and emits a strategy_task_continuation diagnostic before the
source stream closes, so a live client learns the turn is still open. The
SSE diagnostic channel is contract-typed for the first time — the daemon has
always emitted it, ChatSseEvent just never declared it.
The read-side projection deliberately bypasses the record parser: it is for
display, so it must stay cheap across a conversation and must never let one
unverifiable task record fail the whole message list. Existing conversations
resolve their history without migration.
* fix(web): render a Full Plan task as one conversation turn
Each physical Run of a strategy task arrived as its own assistant message,
so a Full Plan answered one question with two turns: the planning Run
finished mid-answer — author line, "completed", copy/branch/rate actions —
and the production Run then opened a second turn that no user prompt
preceded.
The fold happens purely in the view. Messages sharing a task execution id
are folded into the turn's first message in Run order, appending content,
events and produced files, so nothing is dropped and nothing is repeated.
The turn reports the latest Run's status: an intermediate Run finishing is
an internal step, not the turn ending. Direct Edit (a single Run) and
ordinary runs are returned untouched.
Folding in the view rather than by re-pointing a message at the successor
Run is deliberate — the daemon owns message writes, and a client that also
re-points would have the continuation's output persisted twice.
* fix(daemon): let every runtime finish an OD Next complex task
Complex production was reachable for Codex, native OpenCode and AMR but
finishable only by Claude, and the refusal arrived after a full production Run
had already been spent — then latched OD Next off for the whole daemon.
- Scope Build Package ownership to the runtimes that can answer it. Completion
demanded `attributes.buildPackageId` on every Child, but ownership rides on
Claude's `--agents` / `subagent_type` transport and only that adapter stamps
it. Assertions a runtime cannot answer are now skipped for it while every
lifecycle, graph, parent and ordering assertion still applies to all four.
This also makes those lifecycle checks reachable at last: a failed package
check used to `continue` past the started/terminal verdict for that Child.
- Publish child-evidence coverage for ACP. The AMR branch emitted none, so
every AMR task aggregated as `child_lifecycle_unavailable_not_zero` — unable
to distinguish "no Child agents" from "nobody was observing". The consumer
already accumulated bindings, negotiation state and rejection reasons; it now
summarizes them, and reports `unavailable` rather than `complete` when the
capability was never negotiated. The branch had no child-close handler at all.
- Promote native OpenCode child evidence to L2. The live root stream filters
child sessions out, so it can only yield a terminal-only candidate that fails
the evidence graph on `child_started_missing` and refuses every complex task.
`opencode export --sanitize` is the one surface pairing a child's parentID
with its transcript, and it can only be read once the child has exited. Half
a lifecycle is worse than none — it adds an id that fails the graph while
displacing the L1 candidate carrying the bounded Prompt — so a partial or
unavailable export leaves that Child wholly on L1.
- Recover an undeclared production completion. A production turn is only ever
entered from a locked Full Plan and its schema admits no non-terminal
outcome, so a turn that ran the frozen plan, delivered a canonical entry Open
Design resolved itself, and then answered in prose has exactly one thing it
could have declared. Refusing it discarded a finished multi-page deliverable
already sitting in the project.
Two diagnosis surfaces that hid the above:
- A schema failure reported only Zod's first message, so an absent required
field read as the bare word "Required" with no way to tell which field the
agent omitted. Carry the path.
- Repair-anchor recovery only matched a block that was nothing but a fence, so
an agent narrating either side of its JSON produced no anchor and the one
allowed serialization repair could not engage. Recover the first balanced
object too; it still has to satisfy the full schema and is still only ever an
anchor, so a wrong extraction yields nothing.
* fix(daemon,contracts): judge Build Package ownership by the evidence
Complex completion asked whether the selected agent was Claude, then skipped or
demanded `attributes.buildPackageId` on that answer. Ownership is a best-effort
attribute riding on Claude's `--agents` / structured `subagent_type` transport,
so an agent allowlist gets all three of its edge cases wrong: a runtime that
starts stamping ownership stays unverified and its real evidence is discarded, a
Claude turn whose model slipped is refused outright and latches OD Next off for
the whole daemon, and the list needs maintaining for every future runtime.
Ask the evidence instead. Ownership is asserted when every Child observation
names a package and skipped when none can, so verification follows capability
without anyone maintaining a list. Every lifecycle, graph, parent and ordering
assertion applies either way.
The binding factory keeps its own `claude` check: constructing `--agents`
handles is transport-specific and genuinely belongs to that runtime, which is a
different question from whether recorded evidence can be verified.
Also tell the Agent whether the runtime has the verified structured native Child
lifecycle that complex mode requires. The core strategy makes it a precondition
for locking complex, and the capability snapshot already answers it, but only
its hash reached the prompt — leaving the Agent to judge a capability it cannot
observe.
* fix(contracts): teach the plan contract the shapes only a complex plan emits
The canonical Plan Contract example is a simple plan, and the schema rejects a
simple plan that declares Build Packages, so that example necessarily carries
`buildPackages: []`. The prose beside it spells out the element shape of
`buildRequirements` and `readinessArtifacts` but never of `buildPackages`,
leaving the one array unique to complex mode with neither a template nor a
description. A model asked for complex had to invent seven `.strict()` field
names: codex and opencode independently guessed `dependencies` plus a stray
`boundary`, and both terminated on
`od_next_protocol_plan_contract_invalid_schema`.
The same single-item example hides a second rule. It makes
`canonicalDeliverable.id` equal the sole `requiredDeliverables` id, so the
membership invariant reads as a coincidence rather than a constraint. A complex
plan declares one deliverable per page and picks one as canonical, at which
point codex emitted a canonical id that appeared nowhere in the list.
Describe both in the output contract, next to the element shapes already
documented there. With this, codex and opencode both reach
`executionMode: complex`, and opencode carries a complex plan through to
`outcome: completed` with three native `task` children and
`child_evidence_coverage_v1` reporting complete coverage of all three.
* fix(daemon): name the codex rollout root so a complex Run can see its Children
`collectCodexChildEvidence` reads native Child lifecycle out of
`<CODEX_HOME>/sessions/YYYY/MM/DD/rollout-*.jsonl`, and deliberately refuses a
homedir fallback so it can never attribute one install's sessions to another.
That leaves the caller owing it an explicit root — and no caller ever supplied
one. `spawnEnvForAgent`'s codex branch was a pure pass-through, so the
collector's `CODEX_HOME` guard was false on every default install and the
collector has been unreachable since it landed.
The visible symptom was a complex codex Run that locked `executionMode:
complex`, spawned three native sub-agents, delivered four valid files, and was
then refused certification for child evidence the daemon had never gone
looking for. The rollouts were on disk the whole time, with each child
declaring the parent thread id.
Backfill the root before launch, mirroring the AMR `HOME` and
`OPENCODE_TEST_HOME` backfills next to it. The collector's no-fallback
invariant is preserved: the caller still names the exact root it hands the
runtime.
* fix(daemon): report unobserved Children as missing evidence, not malformed
`resolveDaemonOwnedOdNextComplexRuntimeEvidence` always brackets the Child list
with a running/completed root pair, so the empty-observation branch in the
complex gate is unreachable in production: a Run whose Children were never
observed still arrives with two perfectly valid observations and falls through
to the graph check, which reports `od_next_complex_child_evidence_invalid`.
That sends whoever debugs it hunting a corrupt payload. In the case that
prompted this, nothing was malformed and nothing was observed — the collector
had never run at all. Name the two states apart so the reason code says which
one happened.
* fix(daemon): stop one corrupt task record from voiding a whole boot's terminals
Reshaping the OD Next Prompt Bundle changed the v2 child tag set but kept the
schema id `open-design.od-next-prompt-bundle/v2`, on the reasoning that v2 had
never shipped. That holds for releases and not for a developer's own daemon:
the previous v2 composer was HEAD long enough to persist rows that still claim
today's label over a layout its parser cannot read. Those rows raise a raw
`TypeError` out of `parseOdNextPromptBundleV2`.
Startup reconciliation called `reconcileStrategyTaskRunTerminal` unguarded, so
that TypeError escaped `reconcileDurableRunTerminals` entirely. One unreadable
record therefore skipped the task-fact seeding, the whole PostHog `run_finished`
and Langfuse replay loop, and the task-observation crash-window sweep — for
every other Run on that boot, not just its own. Twelve consecutive boots of a
local daemon lost their terminal telemetry this way, and it only stopped when
the data directory was wiped.
Absorb the failure per Run, the way `strategyTaskTurnsForRunIds` already does
for the message list: a task row nobody can read is that Run's problem alone,
and startup still owes every sibling its terminal obligation.
* test(daemon): expect the pinned workspace in the OpenCode connection argv
Pinning OpenCode's workspace with an unconditional `--dir` also reaches the
connection-test probe, which spawns in a per-probe temp directory. That is the
intended reach — a probe must not adopt the repository root as its worktree
either — but the argv assertion was still written against the pre-`--dir` shape
and had been failing since.
Assert around the minted path instead of removing the check: the fixed prefix,
the flag, an absolute value, and the remaining flags in order. The 1.3
compatibility this test guards is a property of the argument order, so the
order stays asserted byte-for-byte on both sides of the temp path.
* fix(daemon): let an agent probe end even when the CLI swallows SIGTERM
`execFile`'s `timeout` is not a settle guarantee. On expiry it only *sends*
`killSignal`, and the promise settles on `'close'` — which needs the child to
actually exit. An agent CLI that traps SIGTERM and keeps a handle open leaves
the probe pending forever, and no caller's `catch` ever runs, so the
degrade-on-probe-failure path every caller already has is unreachable.
That became a hang on the Run path once `startChatRun` began awaiting
`ensureDetectedRuntimeCapabilities`, so the argv it builds matches a daemon
that has served `GET /api/agents`. The probe's cache key includes the resolved
launch path, so a fresh binary always misses and always probes. Against a
SIGTERM-trapping CLI the Run then parked in `queued` roughly 490 lines short of
`run.status = 'running'`: never spawned, so the inactivity watchdog never
armed, so it never reached a terminal status either.
Kill probes outright instead. They are read-only metadata calls with nothing
to flush, and every caller already treats a rejection as "probe failed, use the
safe baseline". `killSignal` is listed before the option spread so a caller
that needs a gentler signal can still ask for one.
* fix(daemon): stop discarding a codex Child that its parent re-invoked
A Codex sub-agent does not run once. The parent re-invokes it, and every
invocation opens another turn in the Child's own rollout, so `started` followed
by N-1 `interacted` is the ordinary shape of one delegated Build Package. The
collector treated a Child with more than one own turn as ambiguous and dropped
it, which meant a complex Run delegating three packages had all three Children
discarded and reported `knownChildCount: 0` with
`availability: partial` — while three complete Child lifecycles sat on disk,
each declaring the parent thread id.
The guard was defending something real: terminals are read from the parent's
activity list, so handing every turn the whole session's activities would let
one parent record terminate them all and stamp them with a single `endedAtMs`.
Slice the activities by the turn they occurred in instead — the parent's
records align with the child's turn starts within tens of milliseconds — so
each turn reads its own terminal, and a turn the parent never closed falls back
to the terminal its own rollout recorded.
Known gap, left for a follow-up: `knownChildCount` is keyed per (session, turn),
so it now reports one entry per invocation rather than per sub-agent. It feeds
the coverage diagnostic only — the complex gate counts observations — but it
makes codex's figure incomparable with the per-child count OpenCode reports.
* feat(strategy): port spec normative content into OD Next assets (#7141)
* feat(daemon): admit Vela to the OD Next capability registry
Vela drives the same native OpenCode runtime this registry already admits under
`native-opencode`. Its Child mechanism is that runtime's, reached over the ACP
extension instead of the CLI stream, and its fixture passes all seven evidence
paths. Holding the tuple out therefore never withheld an unproven capability —
it refused OD Next to an agent on the separate grounds that the build is not
yet immutable, which left every AMR run on the legacy plugin with
`od_next_rollout_x1_capability_fixture_unverified`.
Admit it. Re-pin `agentCliVersion` here once Vela publishes a build reporting a
stable producer version; that is the condition the old comment was really
waiting on, and it is a version pin, not an admission question.
Verified against a live daemon: AMR now resolves `od_next_rollout_eligible` /
`active`, and a simple full-plan task runs through OD Next to
`outcome: completed` with a valid deliverable. Complex still blocks for a
separate and honest reason — the installed Vela advertises no
`child_agent_lifecycle` producer, so no Child can be observed at all — which is
a Vela-side capability, not a gate.
* fix(daemon): stop one runtime's unverifiable Children from disabling OD Next
The rollout stop latch is `daemon_instance`-scoped and survives restart, so it
must be reserved for failures of the machine contract itself — those are true
whichever agent hit them. Unverifiable Children are not: they are a property of
one runtime. Vela ships no child-lifecycle producer today, so an AMR complex
Run cannot be certified at all, and every one of them took OD Next away from
Codex, Claude and OpenCode until an operator ran `od strategy rollout reset`.
The same latch also fired after a SUCCESSFUL run. `availability` demotes to
`partial` on any limitation, and a Codex collection always carries
`codex_inherited_turn_excluded` — children replay their parent's turns — so a
complex task that completed and delivered still disabled the strategy behind
itself. This was observed on a live daemon: the task reached
`outcome: completed`, and the rollout latched `complex_child_unverified`
seconds later.
Child evidence now raises no daemon-wide signal. That task is already
fail-closed with its reason codes persisted, which is the proportionate
response; route/mode drift and machine-contract leaks still latch.
`complex_child_unverified` stays in the reason-code union so daemons already
holding that latch, and the records explaining them, keep parsing.
* fix(daemon,contracts): count Child agents, not their parent's re-entries
`knownChildCount` had no stated unit, so each adapter picked one. OpenCode
counts distinct Child ids; Codex identifies an observation per (session, turn)
and the count was derived from observation ids at the call site, so a Child its
parent re-entered was counted once per invocation. A run with three sub-agents
reported four, and the two runtimes' figures were silently incomparable.
Say the unit in the contract — distinct Child agents, not invocations — and let
the Codex adapter own its own figure, the way the OpenCode adapter already
does. The per-turn observation identity is unchanged: it is what keeps each
invocation's lifecycle separate, which the complex gate still reads.
Verified on a live codex complex run: three native sub-agents now report
`knownChildCount: 3`.
* fix(daemon): stop inferring a complex completion nobody verified
The production-completion inference recovers a task the agent finished but
failed to declare, resting on Open Design having resolved the evidence itself.
For a simple plan that evidence IS the canonical deliverable, so the inference
is sound. A complex plan additionally owes verified native Child lifecycle —
the property that makes it complex — and no deliverable check substitutes for
it.
Accepting any locked execution mode therefore certified Children nobody
observed. Caught on a live AMR complex run: the installed Vela advertises no
child-lifecycle producer, the collector reported `knownChildCount: 0` with
`availability: unavailable`, the turn declared nothing, and the task landed
`outcome: completed` — walking straight past
`evaluateOdNextComplexChildEvidence`. The same run blocks correctly once the
inference is confined to simple.
* docs(daemon): record that Vela's Child producer is verified end to end
A local Vela built from the unmerged child-evidence branch negotiates
`vela.opencode.child_agent_lifecycle`, and an OD Next complex task on AMR now
reaches `outcome: completed` with `availability: complete`,
`source: vela_opencode_acp`, `knownChildCount: 3` and no limitations — three
native OpenCode sub-agents, one per Build Package, each observed through the
ACP extension. `verifiedRuntimeSupport` is the flag that makes exactly that
claim, so it is no longer false. `published` stays false: the producer is not
on Vela's main, so no user can install a build carrying it.
Also write down why this needs no version gate. Child evidence is reached by
capability negotiation, not by sniffing `agentCliVersion`: a Vela without the
producer never advertises it, so a complex task blocks on honestly missing
evidence, and one that has it works the moment it is installed. Admitting the
tuple therefore cannot make an older Vela claim Children it never reported,
and nothing has to be walked back when the producer ships.
* fix(strategy): default prototype demo content to real referents
Field test on beta.104 (claude, active, completed): the model read the
authentic-imagery clause, then legitimately routed around it by choosing
fictional demo books with CSS-drawn typographic covers — the clause is
conditional on content referencing a real entity, and nothing constrained
that upstream choice. The user-intended behavior (real books with real
fetched covers, for texture and credibility) never triggered.
Close the loophole with an affirmative default: prototype sample data
defaults to real, well-known referents with their real fetched images;
fictional stand-ins are a disclosed exception (user request or licensing),
and de-realizing content to avoid the fetch is named and forbidden. Core
baseline gets the matching one-liner so the default reads as baseline
discipline, not a profile-local whim.
Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.
* fix(od-next): stop offering to continue work the task already delivered
A finished Full Plan turn could read as unfinished, offer to continue itself,
and have that offer end in a second task that could only fail.
A Run carries two accounts of whether its declared work is done. OD Next's own
verdict reaches `completed` only after the coordinator confirmed BOTH a
succeeded process AND a canonical deliverable resolved on disk — the entry file
exists, is readable, was touched by this Run, and matches the project kind. The
other account is the agent's last TodoWrite snapshot, unverified narration that
agents routinely leave with a stale `pending` item after writing the artifact.
The snapshot was winning. QA saw a task settle `completed` with seven artifacts
delivered, stamped `endedWithUnfinishedWork`, offering "continue remaining
tasks"; taking that offer opened a second task with nothing left to write,
which validated as `no_artifact` and settled `blocked`. One project then held a
completed and a blocked task for the same work.
Let the verified verdict outrank the narration, through one predicate shared by
the daemon's terminal classifier and both surfaces that read the snapshot — the
pinned todo card's continue affordance and the per-message footer. A
mid-generation truncation stays an independent term, so a turn cut off by
max_tokens is unfinished whatever verdict was recorded.
The verdict also has to survive a reload. The messages table has no strategy
column, so it lives only in the task store, and the client's projection probe
skips a task that is already terminal — reopening the project brought the bogus
offer straight back. The conversation read path already joins the task store to
place each message in its logical turn; it now carries the verdict along the
same join.
Watching a real Full Plan run in Chrome surfaced a second defect on the way in.
The live stream re-points ONE assistant message at each successor Run, so that
message accumulates every stage's output, while the daemon persists one message
per Run. On the post-run refresh the two views meet and
`mergeServerMessageWithLocal`'s "a longer local body is fresher" rule (#6396)
reads the accumulation as freshness and keeps it, beside the successor's own
row: the production answer rendered twice, live, until a reload cleared it.
Scope that rule to messages with no later sibling in the same task — the final
Run has nothing to have absorbed, so the guard is untouched for it and for
every ordinary turn.
Deliberately unchanged: `artifactCount <= 0 -> no_artifact` still blocks. That
guard is what stops a turn claiming completion it never delivered, and the
recovery case QA hit disappears once the offer stops appearing on finished
work. A turn that legitimately has nothing left to write needs a "no change
required" outcome in the protocol, which is a product decision, not a defect
fix. Note also that a settled task cannot be resumed — the daemon rejects any
continuation of a terminal task with 409 — so a continue affordance has no
choice but to open a new task.
Also drops `onStrategyTaskContinuation`, a client callback with no consumer
whose contract described re-pointing the existing message at the successor Run
— the very shape that prints the answer twice. The daemon still emits the
diagnostic into the Run event log, where it documents the hand-off for anyone
reconstructing a multi-Run turn.
Red first: the end-to-end spec fails on the pre-fix daemon with
`endedWithUnfinishedWork` true, again with the reload verdict `undefined`, and
the merge spec with the production answer counted twice.
* fix(web): stop leaking the strategy package above a user prompt
An OD Next turn opened with two rows of chrome the user never asked for: a
"Design" session-mode chip and a "Using OD Next Strategy V2" disclosure that
named the internal strategy package and its version. Neither is context the
user attached to the message — the daemon binds the strategy package itself,
and the mode chip only restates the default that a strategy-owned design turn
already implies.
Key the suppression on `AppliedPluginSnapshot.strategy`, which the daemon
stamps only for an internal strategy apply (verified against real persisted
snapshots: OD Next rows carry `strategy_json`, ordinary plugin rows store
NULL). A user-picked plugin, skill, design system or workspace reference still
renders exactly as before, and Ask/Plan stay labelled everywhere because those
are per-turn behaviour choices the user made.
An OD Next design turn now opens with the prompt itself.
* fix(strategy): add action-level layout mechanics to the baseline
Field test on beta.105 (first AMR active sample, deepseek-v4-flash,
prototype completed): the artifact shipped two elementary geometry
defects — the city switcher and daily-judgment regions overlapped, and a
stat numeral's percent sign wrapped to a second line inside a fixed-size
ring. The existing rules could not have prevented either: they are
outcome-level ('overflow-free layout', 'no overflow or clipping risk'),
and under ship-on-write the model never sees the rendered outcome it is
asked to guarantee.
Replace the bet on outcomes with rules about the source itself — verifiable
while writing: flow-only layout for sibling content regions (no absolute
positioning / negative margins / transforms to stack them), fixed-size
boxes only for fixed-size content, clamp()-sized display numerals with the
unit pinned to the same line, block-level display declared before sizing
track-style elements, no hidden-overflow masking, no 1–2 character
orphans. Core gains the Layout mechanics baseline bullet; the prototype
quality floor names the two observed failure shapes.
Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.
* fix(strategy): add measure-then-size image geometry to the baseline
Follow-on field sample: a fetched, localized movie poster rendered
cropped inside a container whose fixed ratio the model invented — a
failure class created by the real-first imagery clause itself, since
fetched assets carry intrinsic dimensions the model never authored.
The fix is procedural, not aspirational: read the file's intrinsic
width/height during Build (input probing sits inside the ship-on-write
allowance), give the container the measured ratio (aspect-ratio or
natural flow), reserve cover-cropping for decorative backdrops, and
render content-bearing images — posters, covers, artwork, product
shots — at their full frame with no double-locked axes. Core gains the
Image geometry baseline bullet; the prototype quality floor names the
observed failure shape.
Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.
* fix(strategy): anchor overlays on the anchoring axes only
Address PerishCode's review: 'explicit inset on every side' would, taken
literally, set all four offsets on an auto-sized absolutely positioned
badge and stretch it to fill its containing block — the exact malformed
overlay geometry this baseline exists to prevent. Anchor with offsets
only on the anchoring axes (top plus right for a corner badge), size
constrained independently, and name the stretch trap explicitly.
* revert(strategy): split image geometry out of this bugfix
Per review: the measure-then-size image-geometry policy addresses a
separate field failure (localized poster cropped by an invented fixed
ratio) and belongs in its own focused PR with that reproduction. This
branch returns to its stated objective: region overlap, stat-unit
wrapping, and track sizing.
* fix(strategy): add measure-then-size image geometry to the baseline
Field reproduction (beta.105, od-next active, prototype route): the
real-first imagery clause now makes runs fetch and localize real assets
— and the first such sample shipped a localized movie poster rendered
cropped, because the model invented a fixed container ratio for an
image whose intrinsic dimensions it never authored, then let cover-crop
hide the mismatch. The page showed a partial poster frame: wrong ratio,
content cut off. This failure class is created by fetched assets
carrying dimensions the model has not seen, so it cannot be prevented
by the existing outcome-level rules.
The fix is procedural and write-time-verifiable: read the localized
file's intrinsic width/height during Build (input probing sits inside
the ship-on-write allowance), give the container the measured ratio
(aspect-ratio or natural width:100%/height:auto flow), reserve
cover-cropping for decorative backdrops, and render content-bearing
images — posters, covers, artwork, product shots — at their full frame
with no double-locked axes. Core gains the Image geometry baseline
bullet; the prototype quality floor names the observed failure shape.
Split out of #7169 per review scope discipline.
Guard sweep zero hits; plugin-runtime 71/71, contracts 397/397.
* fix(strategy): allow intentional fixed and sticky chrome
Address PerishCode's review: 'normal flow only' contradicted the
prototype profile's own environment rules, which expect fixed headers,
bottom navigation, and floating controls that reserve matching padding —
a model following the core rule literally could strip valid mobile
bottom navigation. Narrow the prohibition to its actual target
(accidental stacking of sibling content regions) and state the chrome
allowance with its padding obligation in both the core bullet and the
prototype quality-floor line.
* fix(web,daemon): keep internal turn chrome and control markers out of the chat
Two acceptance findings, both the same shape: daemon-internal plumbing reaching
the reader.
The session-mode chip flashed "Design" above a freshly sent prompt and dropped
it a beat later. The optimistic user message renders before POST /api/runs
answers, so the strategy binding the previous fix keyed on simply is not there
yet. Key the chip on the mode alone instead: it labels a turn the user steered
away from the default, so Ask and Plan stay labelled and Design — the mode every
design project already runs in — carries no chip. That is decided the moment the
message renders, with no snapshot to wait for.
`<od-title>…</od-title>` rendered as message body. The stripper was armed only
for Runs that requested a title, and the web only requests one on the first turn
of a conversation, so from turn two onward nothing consumed the marker while the
directive was still sitting in the agent's own resumed session history. Arm the
stripper on every Run and swap only the announce callback, then strip again in
the chat: the OD Next machine protocol blocks join the title marker there, and
messages already persisted with a leaked marker render clean, which no
daemon-side fix can reach. Renderable artifacts (question-form, od-card,
artifact) are deliberately excluded.
* fix(web,daemon,strategy): stop a stray question-form marker from stalling a turn
An OD Next turn that needed no clarification narrated its decision straight
into the marker — `<question-form> 无需提出——…`, an unclosed tag whose body is
prose. Four surfaces independently read "an open marker is present" as "a form
is being generated", at three different strictness levels, and none of them had
a state for markup that can never render:
- the web parser raised a loading skeleton and truncated every character after
the marker, so the turn's real output was invisible;
- `hasPendingQuestionForm` held the turn in a clarification handshake;
- `resolveDesignDeliveryOutcome` returned `awaiting_input` before looking at
what the run had delivered;
- `listProjectsAwaitingInput` matched the marker with a raw SQL `LIKE`, latching
the project on `Needs input` until the user typed something.
The daemon's own strict detector meanwhile scored the stray marker as zero
forms, so the coordinator accepted the turn with an empty reason-code list. The
one component that could have reported the contract break was blind to it by
construction.
Give every one of those decisions the same precondition — an open marker only
means a form is in flight while the text after it could still parse as the form
body — and make the malformed case reportable instead of silent. The two new
coordinator codes are attribution, never a gate: every code `validateAcceptedTurn`
returns blocks the turn, so a stray marker on an otherwise valid planning turn
still reaches production.
The awaiting-input queries needed care. They take the newest form-bearing
message per partition and test only that one for an answer, so the strict check
has to run before the newest-per-partition pick. Post-filtering the winning rows
would let a stray marker win its partition, fail the check, and drop a project
that is genuinely waiting on a real unanswered form. Both directions of that
ordering are pinned by specs, and the scan streams without retaining message
content because it runs on every `GET /api/projects`.
The strategy prompt now also forbids restating the marker to announce that no
form is needed. Every clarification rule was phrased as "when to emit a form" /
"skip the form"; nothing forbade writing the bare marker as a label, which is
what produced the turn.
* fix(strategy): define the session-skill conflict order (#7188)
The Instruction order named the three session-skill layers but gave no
tie-break when they conflict inside their shared applicable scope, so a
user-selected skill and a bundled profile could pull the model in
opposite directions with no principled resolution. Make the order
explicit: user-selected skills first, then the general orchestration
Skill, then the task-type profile — and state that the tie-break never
unlocks what higher rules forbid (ship-on-write actions, the V2 machine
contract, the user's explicit requirements). Spec source of truth
updated with matching wording in both languages.
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
* feat(od-next): restore anti-cliché visual rules across the strategy package
The od-next rewrite dropped the legacy anti-slop blacklist (core-slim /
discovery ❌ lists), leaving the package with no background-tone rules at
all — prototype.md even referenced 'the anti-cliché rules above' that did
not exist. Models revert to their warm-cream 'AI look' prior on every
task type.
- core-system-prompt.md: new cross-task 'Anti-cliché defaults' block in
the Design baseline (warm beige/cream grounds, purple-gradient wash,
stock UI display faces, left-border accent cards, icon-per-heading,
gray hover, hand-drawn SVG people, invented metrics)
- prototype.md: resolve the dangling reference with a labeled rule
cluster; add the designer/demo-controls ban
- ppt.md: new 'Avoid deck clichés' section (gradient-wash covers,
icon-per-bullet, center-everything, corner blobs)
- marketing.md: cream/stock-gradient ground ban + never invent awards,
badges, press quotes, or testimonials
- hyperframes.md: cream color-wash grade + particle/glow filler ban
Asset versions unchanged: package identity tracks content via sha256
digests; tests pin '2.0.0' literally.
* feat(web,daemon,contracts): carry an official example card into OD Next
Clicking an official example card under a task type pinned that example as an
executable plugin. The pin stamped explicit_user authority, which took the task
off the automatic route entirely — no strategy task, no plan, no clarification
round — while free text under the very same chip entered OD Next normally.
An example card is a more specific choice inside a task type, not a claim on
the strategy. So send its identity instead of a plugin pin: `exampleReference`,
which the daemon re-resolves through the same local catalogue lookup
`/apply-local` performs, reading the example's bytes from the resolved path and
never from the request body. Its SKILL.md and the side files its prose names
then travel where user-chosen material belongs — `session_skills/
user_selected_skills`, a bundle region that already existed in exactly this
shape and had no producer. No bundle format change, no schema bump.
The discriminator has to be the call site, not the record: the same
`InstalledPluginRecord` is simultaneously an example card, a chip's default
scenario plugin, and a Community entry, and nothing in its data tells the three
apart. `examplePick` is therefore set only by the example-rail handler, and a
genuine plugin pick from the Community page keeps its explicit authority. An
example card under a chip with no OD Next route — wireframe, mobile, image,
video — behaves exactly as before.
Drop the planning/Build-only guard from user-selected Skill capture. Measured
against real content it rejected most of the corpus it was applied to while
protecting almost nothing: it is 15 English regexes, so the same sentence
passes or fails on language alone — "After the build, run through the checklist
and fix any defects" is refused and its Chinese equivalent is admitted. It also
false-positives on prose that merely names the word, and it never inspected
side files at all, so a `references/checklist.md` always walked past it. 47 of
183 shipped examples tripped it, including all three default cards. Conflicting
instructions are resolved by the Instruction order the core system prompt
already defines; first-party strategy assets keep the guard, where the corpus
is controlled and the rule works.
Two latent bugs in that rail surfaced once it had a caller, both of the same
shape — one bad side file deleting an entire Skill. A prose link to a file that
was since flattened away (`example-hyperframes`) and a bundled screenshot past
the per-file byte cap (`example-open-design-landing`) each sank the whole
capture. Neither says anything about whether the Skill is safe to carry, so
both are skipped; a symlink, a symlinked parent, or a file that changes under
the read still fails closed. The byte cap keeps its purpose — oversized bytes
are never read — and `readBoundedNoFollow` still enforces it on what is read.
Answer the brief's template from the card's own input defaults. `od.useCase.
query` is a template that the ordinary route fills from the applied snapshot's
inputs; an example card deliberately creates no snapshot, so without this the
Agent received `{{artifactKind}}` and `{{audience}}` raw — a broken template
rather than an assignment. A field with no declared default keeps its token
instead of inventing an answer the card never gave.
Verified against the shipped corpus: 182 of 183 example folders capture and
serialize into a `user_selected_skills` region, the one exception shipping no
SKILL.md and belonging to no OD Next route. 102 cards across Prototype, Deck
and HyperFrames now enter the automatic route; the other 81 are untouched.
* fix(web,contracts): keep a nested Prototype scene on the OD Next route
Picking a second-level scene under Prototype — Mobile app or Wireframe — took
the task out of OD Next entirely, for free text and example cards alike, while
the first-level Prototype chip worked. Two independent layers excluded them and
cross-check each other fail-closed, so neither could be relaxed alone: the
composer swapped the route id to the scene's own catalog action id, which the
route table does not carry, and the metadata re-derivation separately rejected
`fidelity: 'wireframe'` and mobile platform targets.
Neither exclusion was protecting anything. The two scenes are the Prototype
route plus metadata, as their own chip definitions say — both bind
`example-web-prototype` with `kind: 'prototype'` and `automaticDefault: true`,
differing only by `platformTargets` or `fidelity`. And the Prototype task
profile already branches on exactly those two: it downgrades content and
interaction-state requirements when fidelity is wireframe or lo-fi, and it
carries mobile guidance down to touch reach and the 375/768/1024/1440
breakpoints. The route was being denied to the cases the profile was written
for.
A second-level scene refines WHAT to build, never WHETHER the parent task
type's route applies. Make that structural rather than enumerated: routing
resolves the first-level task type through `taskTypeChipIdForChipId` instead of
reading whichever catalog action the scene happens to bind, so a scene has no
route id of its own to route by and a scene added later cannot strand its
parent. Adding table rows would have fixed today's two and left every future
scene a silent-regression candidate.
On the contracts side `intent` is now the only field that can move a project
off a route — it is the only one naming a different pipeline. `fidelity` and
`platformTargets` stay in the signature to record that the decision has seen
them and chosen not to gate on them.
Deck and HyperFrames were never affected and now have witnesses that say so:
Deck's second-level rail is a card filter that never leaves the hero, and
HyperFrames has no rail at all. A catalog sweep pins the routed chip set to
exactly deck, hyperframes, mobile, prototype and wireframe, so Image, Video,
Audio, Document, Website clone, WebGL, Figma and the rest cannot join it
silently.
* refactor(web): make a nested Prototype scene a second-level type
#6914 moved Mobile app and Wireframe out of Home's first-level rail and into
Prototype's second-level scene rail — but only in the UI. Underneath, both
stayed full first-level chips with their own `apply-scenario` action, merely
hidden from the create rail, and the scene pointed back at them through
`actionChipId`. Its own comment recorded the compromise: keep their catalog
action ids so the nested scene preserves the metadata those actions stamp.
A thing that renders as a refinement of Prototype while still being a task type
in its own right is what dropped these two scenes out of OD Next: the composer
resolved the route from whichever action the scene bound, and Mobile and
Wireframe are not routes. `f360f46e0` stopped the routing site from asking.
This removes the ambiguity it was working around.
A scene now carries its own refinement — Mobile stamps the handheld platform
targets, Wireframe stamps the lo-fi fidelity — and binds its parent. The two
chips leave the catalog, because they were never task types. The refinement
type cannot restate its parent's `kind`, so a scene is structurally incapable
of becoming a type again.
Nothing the user sees changes: same eight scenes, same labels, same metadata
stamped byte for byte, same Prototype route, same example-card behaviour. The
integration assertion was tightened from `objectContaining` to an exact match
to say so, and the unit specs compare the serialized metadata rather than its
fields, since project metadata is persisted and compared as JSON.
Legacy ids still arrive from outside the catalog, so normalization moved to the
two boundaries where that actually happens — a persisted composer draft and a
queued one-shot chip intent — instead of living at the routing site. The queued
intent had no coverage and relied on `pickChip` normalizing before `findChip`;
it has a spec now. `taskTypeChipIdForChipId` is gone: with the scenes out of
the catalog, `active.chipId` is a task type by construction, and every producer
was traced to confirm none can yield a scene.
The placeholder carousel had the same latent shape — three scenario lines bound
a scene as if it were a chip — so they bind Prototype plus a scene id, and the
carousel narrows by scene slug. A one-click create from a scene's carousel card
now has a spec proving it keeps both the refinement and the route.
* fix(web): stop a restored composer pick from costing its task type
Dismissing the pill of an example card returned the Home composer to nothing —
no task type, no OD Next route, and the user's typed brief wiped with Send left
disabled — but only after the composer had been remounted. Opening a project,
visiting Settings, any EntryView swap: the composer comes back looking
pixel-identical, same type selected, same scene, same pill, same brief, and the
dismiss now destroys instead of stepping back.
The persisted chip draft records what was picked and never how. A restored
example card therefore comes back as an ordinary chip binding, while still
rendering its own pill and dismiss control — that check asks whether the record
differs from the chip's default plugin, which is true for every example card.
The dismiss handler keyed its step-back branch on the pick being explicit, so a
restored pick missed it and fell through to the reset that clears everything.
Persist how the pick was made and restore it, so a pick is the same object
after a remount as before one. Then key the step-back on the pick naming a task
type rather than on it being explicit: whatever a pick was, if it named a type
the user chose, dismissing it returns there. Only a pick that never named one
resets the composer, and the typed draft is left exactly as typed either way.
Drafts written before this carry no flags and restore exactly as they do today.
The same lost provenance had a second victim: an example card that was never
dismissed stopped sending its `exampleReference` after a remount, so the card's
own material silently vanished from the run while the route survived — on all
three OD Next routes.
The reproduction drives the real rails and the real bundled catalog through ten
first-level and second-level combinations, each on a fresh mount and across a
remount. The differentiator was never the type or the scene: every combination
loses its route once the pick's provenance has been through storage.
* feat(od-next): ship handheld device shells with the prototype task profile
Blind evals against the live slim-sp strategy showed OD Next losing every
phone-app prototype on one detail: the live route hands the agent a real
handset seed (design-templates/mobile-app), OD Next only described one in
prose and the agent drew a rounded card by hand.
- Declare task-profile `resources` in the bundled strategy contract; the
prototype profile ships iPhone / Android / platform-neutral shells that
enter the package identity with the profile (rule-card and shell edits
move the hash alike) and are re-verified at consumption.
- Stage the shells into `<cwd>/.od-frames/` on every strategy run and
add a "Handheld device shell" section to the prototype rule card with
the brief→shell table and the shell invariants.
- Resolve the platform from project metadata and the user's own words
(ios / android / mobile-neutral; responsive and desktop briefs veto the
platform-less class), latch it per conversation, and quote the selected
shell into the stable request context as `device-frame` +
`device-frame-shell` facts.
- Observe at run finish whether the delivered entry kept the shell and
expose it on run_finished (od_next_device_platform / _source /
_shell_present) so the rollout can measure adoption.
- Shells collapse to a full-bleed phone page below 480px and scale on short
viewports so the artifact still meets the 375px rule.
* docs(od-next): describe task-profile resources in the strategy package SKILL.md
* fix(od-next): stage device shells non-destructively and cover the collab exclusion
Review: `.od-frames/` is a name this PR introduces inside a user-owned
project directory, so wiping it on every run could delete pre-existing
content. Staging now keeps an ownership manifest
(`.od-frames/.od-next-device-frames.json`): only files it recorded earlier
are replaced or retired (and retired only while their bytes are still
ours); a pre-existing file under a managed name is left untouched and
reported as skipped; unrelated files are never read, written, or deleted.
Tests prove an unrelated file survives, a foreign `iphone.html` survives
repeated staging, and a user-edited staged file is not retired.
CI: `.od-frames` joins the expected member-mirror exclusion list in
`vela-cli-resource-adapter.test.ts`, which pins that list exactly.
* fix(od-next): prove device shell bytes are ours before replacing them
Review: listing a name in the ownership manifest was not enough to claim
it. After the first staging a user could edit `.od-frames/iphone.html`
and the next run's unconditional `writeFile` destroyed that edit, and a
pre-existing `.od-next-device-frames.json` under an unrelated schema was
read as an empty manifest and then overwritten.
Ownership is now provisional on both sides. A managed name is written or
retired only when the manifest claims it AND the bytes on disk still hash
to the recorded digest — the rule the retirement path already followed,
now applied to replacement too. A digest that moved makes the file the
user's: it is reported as skipped and drops out of the manifest, so later
stagings keep their hands off it. The control file gets the same
treatment: `readOwnership` classifies the name as absent / ours / foreign
instead of collapsing unreadable, malformed, and unrelated content into
an empty manifest, and a foreign control file makes the whole directory
off limits — nothing written, nothing deleted, every managed name
reported as skipped. In each case the quoted `device-frame-shell` fact
still carries the shell source, so the run itself is unaffected.
Two fixture cases, both red on the previous head: a shell edited after
staging survives repeated staging byte-for-byte, and a pre-existing
invalid manifest plus the user's own `iphone.html` survive untouched with
no shells created.
* fix(od-next): bound the device shell manifest to the names it can stage
Review: the manifest lives inside a directory the project controls, so
its key list was letting the project widen this feature's deletion set.
The parser accepted any slash-free basename with a SHA-256 digest, and
the retirement loop then deleted any accepted entry missing from the
current package while its bytes still matched. A project holding a
same-schema `.od-next-device-frames.json` that claims `leftover.html`
with that file's real digest therefore lost `leftover.html` on the next
strategy run — verified, not hypothetical: the new fixture fails on the
previous head with `ENOENT` on that file.
The bound is now this feature's own filenames rather than whatever the
manifest names. `MANAGED_SHELL_FILES` is derived from
`OD_NEXT_DEVICE_FRAME_FILES`, and a manifest is `ours` only when every
key is in that set and every value is a 64-hex digest — that is exactly
what this materializer writes, so anything else came from somewhere else
and the whole record is refused as foreign, leaving the directory alone.
Refusing beats salvaging the well-formed entries here: salvaging is what
would let a crafted entry keep nominating an unrelated file. The
retirement loop re-checks the allowlist as well, so the deletion set
stays bounded even if the parser is later loosened.
Manifests written by the previous head hold only shell names and valid
digests, so they keep reading as ours; no migration.
* fix(od-next): let an @-mentioned Skill ride the automatic route
Mentioning a Skill in the composer took the turn off OD Next entirely. The
project fell back to its task type's legacy plugin, so the chat header
announced "正在使用 Web Prototype" and the run reached the agent as ordinary
prose with no Prompt Bundle at all — no Runtime State, no Plan Contract, none
of the strategy the task type was supposed to own.
Three layers each dropped the Skill or the route, and every one of them had to
give for the turn to survive intact:
- The admission gate counted a named Skill as explicit user authority, next to
a pinned plugin and a pinned snapshot. A Skill is not an executable surface
and cannot run a strategy; it is material for one. It now stays out of that
read, and a context plugin remains the only non-plugin signal that still
claims the route.
- Home treated the task type and the Skill as mutually exclusive: picking
either one wiped the other, and submit dropped whichever survived. That rule
came from #2972, which asked for a defined answer when the prompt's intent
and the picked card disagree. "The Skill wins, drop the card" is defined, but
it routed a user who picked 幻灯片 into a prototype project, which is exactly
the silent misrouting #2972 objected to. The task type now owns the route and
the Skill is material carried inside it, so the conflict resolves without
discarding either.
- The create handoff nulled `skillId` the moment a task profile was claimed,
which would have swallowed the Skill one layer after Home stopped dropping it.
The catalogue capture entry point for exactly this already existed and had
never had a production caller — the gate above made it unreachable. It is now
wired through a single session capture that converges both channels a session
can select material through, because one task freezes ONE package: an
@-mentioned Skill and an official example card land in the same
`session_skills/user_selected_skills` slot, the mention first, which is the
authority order the strategy's own conflict rule already assumes.
Capture is fail-soft on every leg, matching the example-card rail: a Skill
deleted between picking and running, a catalogue that will not list, a
selection past the package bounds — each drops that material and runs the task
without it. The ordinary route drops an unresolvable Skill silently too, so
this is the same contract rather than a new leniency, and an unfreezable
selection can never cost the project the example card it was created from.
Skill-catalogue scoping moved to one helper shared with the system-prompt
composer. A Skill one surface can resolve is now a Skill the other can freeze;
resolving them from separately-derived partitions would drop a Workspace Skill
on whichever side guessed narrower.
Coverage: the three request shapes that carry a Skill (`ChatRequest.skillId`,
`skillIds`, and the Skill persisted on the project row by Home's create) each
have an HTTP witness asserting the Bundle text; all four routed task types are
covered for a mention arriving alongside their default card. Two server tests
that encoded the old "Skills route ordinary" contract, and the two Home specs
that encoded the mutual exclusion, were rewritten rather than deleted — each
now states the rule that replaced it.
* fix(od-next): accept a HyperFrames composition as its canonical deliverable
A HyperFrames run that wrote an editable root `index.html` was blocked with
`od_next_canonical_deliverable_invalid`, surfacing as "任务执行失败" even though
the artifact was sitting in the project and working. Every HyperFrames
production turn in the reported diagnostics bundle hit it.
`validateRunDeliverable` gated the entry's file kind on `metadata.kind`, and
`kind: 'video'` accepted only `video` files. But `kind` records which Home
surface a project was created from, not the shape of what it delivers. For
every other kind those coincide; HyperFrames breaks the identity — it is a
local HTML-to-video renderer, so the HTML composition is the authored artifact
and the MP4 is a render of it, produced on demand and not necessarily present.
OD Next already said so on the way in: `DAEMON_OWNED_OUTPUT_KINDS.hyperframes`
admits `html` and `source` as supported deliverable kinds at plan time, so the
same daemon approved a plan the completion check then rejected.
Resolve the accepted kinds from the project's metadata instead of its bare
`kind`, via one named invariant used by both entry inference and the kind gate.
Generative video (fal / Veo / Sora / Volcengine) keeps the strict video-only
contract: there the provider returns a video file, and an HTML preview page in
its place is a genuine delivery failure.
The `hyperframes-html` discriminator moves next to `ProjectMetadata` so
delivery validation and analytics `project_kind` read one constant.
Red spec: `apps/daemon/tests/run-deliverable-validation.test.ts` reproduced the
production `type_mismatch` before the fix, and covers both guards after it.
---------
Co-authored-by: Cheems <94773058+itscheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
Co-authored-by: Cheems <cheems1229282@proton.me>
Co-authored-by: 黄桃 <huangtao@huangtaodeMacBook-Pro.local>
Co-authored-by: open-design-crew[bot] <299007234+open-design-crew[bot]@users.noreply.github.com>
Co-authored-by: NJUHua <113895241+NJUHua@users.noreply.github.com>
Co-authored-by: hihijack111 <216108319+hihijack111@users.noreply.github.com>