* 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
On a fresh Windows install the launcher has no versions/0.16.1 tree yet,
so resolvePayloadExecutable returns null and
prepareLegacyPayloadDesktopHandoff immediately returned invalid-payload.
That ran before the eligibility check (active == lastSuccessful,
generation 0), masking the real state as a payload error. The daemon
then logged [packaged desktop handoff] skipped { reason:
'invalid-payload' } and left trustedWebOriginPort null, so /api fell
through to the Next.js HTML shell (already fixed web-side by #7399 for
the no-daemon-origin path).
Reorder the check: report launcher-state-not-eligible first and only
return invalid-payload when the launcher state is actually eligible for
a handoff but the payload file is missing. Payload resolution stays
manifest-driven, so a missing or mismatched manifest keeps failing
closed instead of accepting a guessed executable that the updater never
promoted.
The regression now covers both halves of #6074: the fresh-install
classification, and the packaged startup that actually delivers the web
port the daemon trusts for /api.
Fixes#6074
* add Novita AI provider preset
Adds Novita AI to KNOWN_PROVIDERS so it appears in the BYOK provider
picker alongside the other OpenAI-compatible presets.
Follows the PPIO entry directly above it: same openai protocol, a
/v3/openai base URL, and a preferredModels list. The apiKeyConsoleLink
follows the Atlas Cloud entry so Settings can deep-link to the key page.
Model ids were taken from the live /v3/openai/models catalogue.
No new dependencies, no changes to any shared type or function.
* fix(providers): correct Novita base URL to current documented endpoint
The KNOWN_PROVIDERS Novita AI entry used https://api.novita.ai/v3/openai,
copied from this loop's own evidence store (browser-use's own
examples/models/novita.py, an older third-party example). Novita's
current documented OpenAI-compatible endpoint (novita.ai/docs/guides/llm-api)
is https://api.novita.ai/openai. Both paths return identical, working
results today (verified live against /models and /chat/completions), so
this is a stale citation, not a functional break. The separate PPIO entry
(a different vendor, api.ppinfra.com) is untouched.
* fix(providers): update Novita preferred models per QA catalog check
Replace superseded deepseek/deepseek-v3.2 and qwen/qwen3.5-397b-a17b
with current live-catalog flagships minimax/minimax-m3 and
qwen/qwen3.8-max.
* feat(providers): add Novita BYOK preset
---------
Co-authored-by: jax-novita <jax-novita@users.noreply.github.com>
Co-authored-by: jax-novita <jax@novita.ai>
Co-authored-by: Ray Xi <2667192167@qq.com>
* fix(chat): bust live artifact iframe cache on file updates
* fix(chat): scope artifact cache refresh to live HTML previews
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(chat): keep question form controls in the UI language
* test(web): locate questionnaire controls by visible semantics
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(chat): preserve manual file ownership across run recovery (OPEND-2928)
Record successful workspace document writes and exclude their unchanged
receipts from assistant produced/trace files while retaining proven Agent
writes and existing unknown-runtime output handling.
Carry receipts through reconnects of the same scoped physical run, including
transport failure ordering and manual reconnect after a missing status.
Keep cleanup on real terminal exits, failed run creation and scope changes.
* fix(chat): preserve file ownership during inline artifact recovery
Use the resolved project authority for artifact writes and carry scoped
manual-write receipts through terminal error handling into artifact recovery.
Read the transport text when deciding whether artifact recovery is pending,
and filter existing produced/trace projections without dropping Agent output.
---------
Co-authored-by: lefarcen <935902669@qq.com>
durableRunState never serialized appliedPluginSnapshotId, so a daemon restart
restored the source Run without the field while the strategy task kept its
locked snapshot; the clarification continuation guard then rejected the task
with STRATEGY_TASK_SOURCE_RUN_INVALID. Persist the field on the durable run
state, and recover a missing id from the applied_plugin_snapshots run_id link,
which survives restarts. Runs whose snapshot id is set but differs, or whose
snapshot row is not linked to the source Run, keep failing the guard.
After the user answers the one clarification round, the agent's next turn
carries a complete Full Plan plus a Runtime State. On 0.22.1-prerelease.9
(OPEND-2954) that state said `inputStage: "request"` — the value every
Runtime State example in the protocol reference shows, and the one field the
clarification continuation never named — and the coordinator refused the turn
on that field alone. The task settled terminal-`blocked`, a plan that passed
every other gate never reached production, and no repair path could anchor on
it because the block was schema-valid and the mismatch is not a parser issue.
The stage is host-owned truth: the daemon issued it, wrote it into the
continuation wrapper, and holds it on the task, so a disagreeing declaration
carries no authority — the same footing as the execution mode a clarification
turn predicts, which the parser already discards. `adoptHostInputStage` now
gives the state the host's stage before validation whenever the corrected
state is a valid declaration for that stage, and records the decision as a
normalization rather than a reason code so an agent-declared `blocked` keeps
its own attribution. Where the field does carry meaning — a second
`clarification_required`, a `plan_ready` at production — the corrected state
fails the contract, the agent's value stands, and the turn is refused exactly
as before. The serialization-repair anchor gets the same treatment so a fenced
plan on a clarification turn can still enter its one repair.
The clarification continuation now also says which stage it runs at, mirroring
the production continuation's `inputStage production`, so the agent has one
sentence to follow instead of three examples that all say `request`.
Specs: a clarification turn declaring `request` / `plan_ready` with a valid
plan reaches `plan_ready` (red on main: blocked with
`od_next_protocol_stage_mismatch`); an agent-declared block with the wrong
stage keeps `od_next_agent_declared_block`; a second question round is still
refused; a fenced plan with the wrong stage still anchors a repair; the
continuation text names `inputStage clarification`.
Co-authored-by: Cheems <216108319+hihicheems@users.noreply.github.com>
* fix(daemon): keep proxy Basic credentials off the media wait tool-token lane
The media task wait route selected the run-scoped tool-token lane on the
mere presence of an Authorization header. Tool tokens are only ever
parsed out of a Bearer header (bearerTokenFromRequest), so a reverse
proxy that authenticates browsers itself and forwards its own
`Authorization: Basic ...` could never satisfy that lane: every proxied
poll of POST /api/media/tasks/:id/wait failed 401 TOOL_TOKEN_MISSING,
so image and video generation never reported completion.
Gate the lane on the Bearer scheme. The existing intent is preserved —
once a Bearer caller does choose the lane, invalid, expired or
under-scoped credentials still must not downgrade to project
authorization; this only changes what counts as choosing it.
The other two authorizeToolRequest callers in this file,
/api/tools/media/scaffold and /api/tools/media/generate, are agent-only
endpoints that unconditionally require a tool token and are unchanged.
Same root cause as #7819, which covers the export and archive routes in
apps/daemon/src/import-export-routes.ts.
* fix(daemon): fail closed on a malformed Bearer credential
Review catch: `authorizationHeader.trim()` reduces `Authorization: Bearer `
to `Bearer`, which the `/^Bearer\s+/i` predicate rejected. That sent a
malformed Bearer credential to `authorizeProjectRequest` instead of
`authorizeToolRequest`, so a caller reaching for the tool-token lane could
reach the browser project-authority lane — before this branch, any
Authorization header produced TOOL_TOKEN_MISSING.
Classify the scheme independently of whether a token follows it, so both
`Bearer` and `Bearer <invalid>` stay on the tool-token lane and fail closed,
while Basic credentials still reach project authorization.
Regression coverage added for `Bearer ` and a bare `Bearer`; both return a
full task snapshot (HTTP 200) with the previous predicate and 401 with this
one.
* fix(daemon): keep proxy Basic credentials off the export tool-token lane
authorizeExportRead treated any Authorization header that was not the
daemon API token as a run-scoped agent tool-token call. Tool tokens are
only ever parsed out of a Bearer header (bearerTokenFromRequest), so a
reverse proxy that authenticates browsers itself and forwards its own
`Authorization: Basic ...` (Coolify native basic auth, Traefik basicAuth
without header stripping) could never satisfy that lane: every browser
export and archive request failed 401 TOOL_TOKEN_MISSING.
Gate the tool lane on the Bearer scheme so foreign Basic credentials fall
through to project authorization like a header-free browser request.
Foreign Bearer tokens still fail closed in the registry.
Covers export/{html,pptx,pdf-image,image}, the generic export route,
archive, archive/batch and the inline-HTML route, which all share the
helper.
Root-cause analysis and the Bearer-gate approach are @airano-ir's, from
the #7819 report.
Fixes#7819
Co-Authored-By: airano <244096521+airano-ir@users.noreply.github.com>
* fix(daemon): fail closed on a malformed Bearer credential
Carries the review finding from #8006 across to the export lane, which had
the same predicate. `authorizationHeader.trim()` reduces `Authorization:
Bearer ` to `Bearer`, which `/^Bearer\s+/i` rejected, downgrading a
malformed Bearer credential to browser project authority instead of failing
closed with TOOL_TOKEN_MISSING.
Classify the scheme independently of whether a token follows it. Regression
coverage added for `Bearer ` and a bare `Bearer`.
---------
Co-authored-by: airano <244096521+airano-ir@users.noreply.github.com>
* test(web): reproduce question title wrapping
* fix(web): keep question titles on one line
* fix(chat): stabilize question metadata and verify the rendered header
* fix(daemon): fail blocked OD Next runs before finalization
* test: align run fixtures with OD Next terminal status
Keep generic session and smoke fixtures on ordinary runs, and assert the blocked verdict in the strategy timing fixture.
Generated-By: looper 0.13.0 (runner=fixer, agent=codex)
* test(daemon): isolate Codex resume and retry fixtures from OD Next
Select ordinary-run mode for plain-reply session and recovery fixtures, restoring the rollout override after each test.
Generated-By: looper 0.13.0 (runner=fixer, agent=codex)
* fix(daemon,web): stop a refused turn reporting a delivered deck as failed
One predicate answered two different questions, and the strict answer is the
wrong answer to the loose one.
`validateRunDeliverable` asks "did THIS run write the canonical entry". That is
the right gate for accepting an Agent's completion claim — a turn must not pass
by pointing at a file an earlier turn wrote. It is also `false` for the most
ordinary shape of an OD Next refusal: the user types 「继续」, the agent
re-checks work an earlier turn already finished, correctly rewrites nothing,
and the turn is refused over its machine block.
Both safety nets read that one answer, so both went down together. The
coordinator's completion inference declined, and `deliveredDespiteBlock` in the
web provider — the carve-out that exists to keep a delivered turn out of the
failure branch — could never fire. A field report shows the result: a red
「回复已收到,但没能记录下来」card under a finished 16-page deck the user could
see rendered in the preview pane beside it. The physical run had exited 0 and
the 2.27 MB file was on disk.
`validateProjectDeliverable` is the second question: does the user have it,
right now. Same entry resolution, same kind contract, same readability check,
minus the run-scoped gates. `GET /api/runs/:id` resolves it only for a terminal
run whose strategy task settled blocked — the one case a client has to
distinguish — and logs the answer, because withholding a failure the user
cannot act on is right and withholding it invisibly is not.
The verdict does not move: the task still blocks, the strategy contract still
refuses to complete on an unproven claim, and a turn that delivered nothing
still fails with its card. What changes is that a bookkeeping failure no longer
tells a user they lost work that is sitting in their project.
This is also what `docs/design/run-errors/error-ux-design.md` §3 already ruled
for the case: 「文件生成了但进程报错:算成功」, with the stated reason that doing
otherwise "把一堆无害报错变成失败潮".
Red spec: apps/web/tests/providers/strategy-blocked-verdict.test.ts
「does not become a failure when the project holds the deliverable」 — red on
main, green here.
* fix(daemon,web): name the gate that refused a turn, not the parser that noticed
The protocol parser raises `od_next_protocol_runtime_state_missing` before
`validateAcceptedTurn` — the function that owns the real gates — is ever
reached. So every block-less turn was filed under the parser's own code, and
`reasonCodes[0]` is what the failure card, the diagnostics export and the
analytics bucket all read.
The effect on a user who got nothing this round: they were told a marker was
missing. True of the reply, useless to them, and not the reason the turn could
not succeed — the identical failure with a DECLARED block reported
`od_next_canonical_deliverable_invalid`, the host fact they can act on. Two
names for one failure, and the less useful one was the one that shipped.
`reattributeUndeclaredTurn` already existed for exactly this, scoped to one
cell (a clarification turn that asked again). Extend it: when a block-less turn
was SHAPED like a completion the host may infer and the inference still
declined, the evidence is what refused — both helpers gate on the shape, which
just held, and the evidence. Lead with it. The parser's code stays behind it,
because `blocked_reason_codes_json` is the durable record of what the turn
actually wrote, and a triage that cannot tell "declared badly" from "declared
nothing" has lost the one distinction that decides whether a serialization
repair was ever possible.
ATTRIBUTION ONLY — no verdict moves. The two existing specs that pinned the old
name still assert the verdict they own and now assert the new attribution; the
turns they describe block exactly as before.
`od_next_canonical_deliverable_invalid` gets the card it now needs. It is
deliberately not the Runtime State copy: those say "your reply went missing",
this says "nothing came out this round", and only the second is true when the
project is empty. Retry earns its place here — the deliverable is missing
because this turn wrote nothing, and a re-run is the thing that can write it.
⚠️ The copy is engineering's, like the rows around it. `error-ux-design.md` has
no cell for it; S23 (跑完没生成文件) is the nearest and is listed there as
invisible in telemetry. Product should rewrite the wording, not the routing.
Red spec: apps/daemon/tests/strategies/od-next/coordinator.test.ts
「names the evidence that refused an undeclared completion, not the parser」 and
「gives the undeclared and declared shapes of one failure the same name」 — both
red on main, green here.
* feat(daemon): make an OD Next block countable instead of invisible
`run_finished.result` is derived from the physical run status
(`runResultFromStatus`: `succeeded → success`), and a refused OD Next turn
normally exits 0 with a complete reply on screen. So this entire class of
failure reported `result: 'success'` while the user was looking at a red card,
and nothing queryable disagreed — the reason codes lived only in the task
store's `blocked_reason_codes_json` and in a client-side error object.
`error-ux-design.md` §《线上频次与优先级》 already flags the shape of this
problem for S23/S24: 「被算成了成功 —— 看不到是坏事:线上到底有多少,我们不知
道」. OD Next made it worse than invisible by pairing the silent metric with a
visible failure card.
Carrying `reasonCodes[0]` — the same value the card keys on — makes the class
countable without changing what `result` means. Read off the run's own terminal
projection, the object the SSE `end` payload and the failure card were both
built from, so the three cannot drift. Omitted for every run whose strategy
task did not block, which is the common case.
This is the half that makes the suppression in the preceding commits safe to
ship: a failure the user is no longer shown is one we can still count.
* fix(web): do not go silent on a turn that produced no reply either
The preceding fix credits a deliverable an EARLIER turn wrote, which is right
for the case it was written for — the agent re-checks finished work and
correctly rewrites nothing — and wrong for the mirror case in #7564: a blank OD
Next response into a project that already holds a prototype. There the file is
real and the user still got nothing, so withholding the card swaps one wrong
answer for the other.
Require the turn to have actually said something before the looser field can
suppress a failure. The accumulated assistant text is already on hand in the
stream consumer, so this costs nothing and needs no new daemon field.
The stricter field keeps its existing behaviour untouched: an artifact THIS run
produced is delivery whether or not the agent narrated it.
Red spec: apps/web/tests/providers/strategy-blocked-verdict.test.ts
「still fails when the turn produced no reply at all」 — verified red with the
guard removed, green with it.
* refactor(contracts): make the OD Next block dimension a tested pure mapper
Observability that nothing exercises is observability that rots. The derivation
was inline in the run lifecycle, where the only way to reach it is to boot the
whole analytics flow — so it would have failed silently to "field absent",
which is indistinguishable from "no run ever blocked".
Move it next to `harnessAnalyticsFromRolloutDecision`, which answers the
neighbouring question about the same run and already has a suite. The edges
that matter are the ones a wrong answer would quietly mislabel: a task that has
not settled, one that settled on something other than `blocked`, and a blocked
one an older daemon projected without a reason.
---------
Co-authored-by: Cheems <216108319+hihicheems@users.noreply.github.com>
Co-authored-by: lefarcen <935902669@qq.com>
The watcher treated "I once saw this run completed" as permanent and stopped
polling the lane. GitHub re-runs a workflow IN PLACE — run_attempt increments,
status returns to in_progress, conclusion is rewritten, all under the same run
id — so the card froze on the first attempt's verdict for the rest of the watch.
Both dispatched lanes had it. Each now re-reads its run every cycle and refreshes
the paginated job list only when the run's own status/conclusion/run_attempt/
updated_at say it moved, so watching for a re-run costs no call the lane was not
already making.
Claude-Session: https://claude.ai/code/session_01PbTMU3bKHcu6cmJYy42eF3
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three follow-up suggestions under a completed turn are written by the
model, not picked from i18n files, so the only thing that decides their
language is the prompt. That prompt carried one soft rule — "write them in
the language the user is speaking" — printed directly under three English
examples, and nothing about the run's actual locale.
The locale rule lived only in `# UI locale override`, a section of the
cache-stable prompt head that `server.ts` deliberately drops on resume turns
to protect the upstream prompt cache. The keyed follow-up block is re-sent
every turn. So the rule was absent exactly on the turns that carry the
marker, and a zh-CN run closed with English suggestions — one of them a
near-copy of the prompt's own example.
Thread the run locale into `renderChatTurnHostProtocolInstructions` so the
per-turn block states its own language requirement, including the existing
brand/code/technical-identifier exception so `localStorage` and `CTA` still
survive. An English or absent locale renders byte-for-byte what it did
before. Also collapses the third copy of the zh-CN/zh-TW name mapping into
`prompts/ui-locale.ts`.
Fixes OPEND-2765
* fix(chat): render cards inside execution records
Render supported od-card payloads inside execution records with the existing
card components, instance scopes, and browser-assistance callback. Preserve
Markdown code examples and isolate card payloads from later Markdown parsing.
* test(web): await writable scope before deleting a conversation
* fix(web): buffer streamed card opener prefixes
* fix(web): remove undesigned ChatPanel cards (OPEND-2971)
Remove task-brief and rule-proposal presentation and their exclusive UI,
styles, and translations following the explicit product decision.
Preserve quoted card examples, neighboring prose, and the three retained
card kinds, including browser-assist callbacks.
* fix(chat): stop generating undesigned brief and rule cards
* test(web): use supported role query options for removed cards
* fix(chat): hide retired brand browser assistance cards
* fix(web): hide undesigned assistant message panels
* test(web): specify the brand project kind in hidden UI fixture
* style(web): remove trailing blank line after panel styles
* fix(web): hide verification scorecards in ChatPanel
* test(chat): align retired UI expectations with approved hiding
* fix(chat): keep card payloads opaque to question form parsing
* fix(web): preserve card parsing across replay metadata
Keep marker masking aligned with physical run boundaries, and replace repeated
Markdown tail scans with a forward indexed walk. Preserve actual forms and raw
code examples with replay and paragraph-boundary regression coverage.
* fix(web): keep form field syntax inside its payload
Recognize complete forms through the existing parser before scanning their
fields for card or code markers. Preserve sibling form actions and keep outer
card/code ownership with callback-based parsing that avoids an import cycle.
* fix(chat): share question form protocol context with host detection
---------
Co-authored-by: lefarcen <935902669@qq.com>
* fix(chat): wait for pending clarification before continuing todos
* fix(chat): preserve recovery for truncated clarification forms
---------
Co-authored-by: lefarcen <935902669@qq.com>