* fix(release): terminate Windows package command trees
* fix(release): type package runner test output
* fix(release): scope package runner normalization to Windows
* test(release): run package timeout proof on Windows CI
---------
Co-authored-by: TheAngryPit <16145902+TheAngryPit@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(gateway): retain root-work admission for streamed HTTP runs
Streaming requests to /v1/chat/completions and /v1/responses dispatch the
agent run from a detached `void (async () => {...})()` that intentionally
outlives the HTTP handler. The handler itself runs inside
runWithGatewayHttpWorkAdmission, which releases its root-work admission as
soon as it returns.
Because the detached run inherits that same AsyncLocalStorage store, it ends
up holding a *released* lease. isGatewaySubordinateWorkAdmissionClosed()
returns `current.released` for an inherited store, so every subordinate
session/lane admission the run subsequently requests is refused with
GatewayDrainingError - on a gateway that is not draining at all.
Symptoms: streamed turns fail partway with "Gateway is draining; new tasks
are not accepted", surfacing to OpenAI-compatible clients as a generic error
chunk. Non-streaming requests are unaffected, because they await the run
inside the handler's still-live admission.
Fix: retain the admission across the handler boundary with
retainGatewayRootWorkAdmissionContinuation() - the helper that already
exists for exactly this case ("Transfers an admitted request root to work
that intentionally outlives its handler") - and release it in the existing
finally block so drain accounting stays balanced.
* test(gateway): cover streamed HTTP admission lifetime
* test(gateway): satisfy deferred timer lint
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(channels): remove flat streaming compat
maintainer-approved early removal of v2026.7.2-gated compat
* refactor(channels): remove group intro hint adapter
maintainer-approved early removal of v2026.7.2-gated compat
Plugin SDK surface baseline update is maintainer-approved for this intentional removal.
* fix(agents): preserve relocated auth and shared stores
* fix(state): avoid control regex in alias probe
* style(state): keep alias probe within line budget
* feat(ui): repair settings titles, scope form-unsafe banner, consolidate advanced tier
- Advanced/Notifications settings headers rendered raw i18n keys
(tabs.advanced/tabs.notifications); configPageTitle now resolves through
the nav registry titleForRoute, deleting the drifting key map.
- Form-unsafe banner is value-aware (only when the user's config has a value
at a form-unsupported path in the active scope), names the paths, offers an
Open Raw editor action, and sits in the content column instead of full-bleed.
- Advanced tier collapses to one synced toggle: hidden advanced fields render
a ghost row that enables the toggle; the Advanced page always reveals and
hides the toggle; per-section details state and the controlled-open dance
are deleted; toggle visibility mirrors the renderer's tier split.
- schema.tags no longer tags facet-less paths as "advanced" (common fields
like update.channel wore a lying chip); the chip is no longer rendered in
form rows while tag:advanced search keeps working.
* fix(config): dev bootstrap writes canonical agents.entries; finish stale agents.list sweep
- openclaw gateway run --dev failed on a fresh state dir: the bootstrap wrote
the retired agents.list array and validation rejected its own config
(Unrecognized key: list). It now writes the keyed agents.entries record;
new dev.test.ts validates the written shape against the zod schema.
- Revive the dead gateway hot-reload rule: prefix agents.list never matched
canonical config diffs, so per-agent heartbeat edits fell through to the
agents:none tail rule; the rule now matches agents.entries.
- config set replacement protection moves from the dead agents.list array
to the agents.entries map (joins plugins.entries/auth.profiles family).
- Sweep remaining stale agents.list config-path strings in hints, fix-it
messages, elevated gates, doctor hints, audit text, and type docs to
agents.entries.*; RPC ids, the internal list projection, and doctor
legacy-migration references intentionally keep the old name.
* test(ui): derive nav i18n audit from route registry; fix stale bootstrap hint assertion
- Knip flagged navigationCopyEntries (test-only export); the audit now walks
ALL_ROUTES through prod titleForRoute/subtitleForRoute and rejects raw
dotted-key output, so no export exists solely for the test.
- commands-context-report expected the retired agents.list[] hint wording.
* fix(config): align rebase resolution with main's roster-aware diagnostic
- tool-policy-diagnostic: keep main's test expectations (dotted
agents.entries.<id> paths from the roster-aware implementation); our
pre-rebase bracket-style assertions no longer match any code.
- Reapply the placeholder sweep the conflict resolution dropped:
generic agents.list[] fallbacks in tool-policy-diagnostic and the audit
sandbox-mode hint now say agents.entries.*.
* fix(ui): meet WCAG AA for Control UI status tokens
Status labels (--ok/--warn/--danger/--info) are rendered as text on their
own 8% -subtle tint. Composited over --card and --bg, several pairs fell
below WCAG AA 4.5:1 -- every light-mode token, and --danger/--info in dark.
Retune the failing bases one Tailwind step and re-derive the matching
-subtle/-muted rgba so each pair clears 4.5:1 on card and bg across all six
theme families. Add the missing --info-subtle and adopt it in
.session-avatar--group, whose --ok/--warn siblings were already tokenised.
Non-text uses (status dots, meters, chart bars) only gain contrast, and the
one solid --danger fill carrying white text improves 4.83:1 -> 6.47:1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(ui): state the bound the status contrast audit actually proves
The audit comments named their card/bg bound but not what falls outside it, so
they read as a general guarantee at the exact site the next contributor will
trust. Status labels also land on hover, muted and input surfaces, and one of
those is reachable with real text: the plugins error row sits on a hovered
background at 4.28:1 under dash-light. It improves on the 3.23:1 it replaced,
but it is not AA and the comment should not imply otherwise.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* refactor(media): read persisted media facts first across all consumers
PR 2 of the media legacy retirement program. Gateway display projection,
Control UI (history filtering, attachment rendering, initial-turn
handoff), embedded and plugin-harness hydration, replay/prune, late-media
reconstruction, hooks, media understanding, and trajectory capture now
read nested __openclaw.media facts first. Legacy top-level Media* fields
survive only in the narrow persisted-message compatibility reader (rows
predating the PR-1 dual-write) and the public SDK projection.
Non-goals pinned by tests: rendered batches, reply-payload dedupe,
durable session/outbound queues, and lowercase ReplyPayload.mediaUrl(s)
are untouched contracts.
Consumer matrix covers {legacy-only, facts-only, both-equal,
both-conflict, sparse, type-only, media-only} inputs across every
consumer family; prompt/cache goldens byte-identical; 131-file legacy
consumer sweep run individually (two failures reproduce unchanged on
clean main); 1,047 broad media tests and the full UI lane (5,553) green.
* refactor(media): drop caller-less staging predicate and localize empty-user-row classification
The consumer flip left hasStagedMediaProjection without production
callers (facts-only staging uses hasStagedMediaFacts); delete it and
retest the staged merge matrix against the facts predicate. Unexport
LEGACY_MEDIA_CONTEXT_KEYS (internal only; the derived type stays public).
Move isEmptyUserTextOnlyMessage beside the other message-shape
classifiers in ui/src/lib/chat/message-extract.ts where chat-history
imports it cross-module.
* chore(ui): drop unused hasTranscriptMediaFacts import after predicate move
* chore(ui): localize hasTranscriptMediaFacts after its last external consumer moved in
* style: format staged-media matrix assertion
* feat(ui): publish host style variables to MCP apps
An embedded MCP app received only the `theme` string, so it knew which
appearance was active but nothing about what that appearance resolves to.
Every app therefore had to ship its own palette, and an app installed through
a plugin looked like itself rather than like the surface hosting it.
Publish the Control UI theme as `hostContext.styles.variables`, the field the
MCP Apps specification defines for exactly this. The key set is closed by the
specification, so the mapping is a table from Control UI custom properties to
specification keys; the canonical meaning of each key lives in the carapace
embed contract.
Only keys Control UI can honestly source are published. The specification
lets a host publish any subset and apps resolve the rest from their own
fallbacks, so omitting is preferable to inventing. The body font is
deliberately omitted: it leads with a webfont, and an embedded app may load
fonts only from resource domains it declares itself, so publishing it would
silently resolve to an arbitrary system face. Apps own their sans stack until
Control UI adopts the carapace embed tokens, which define a sandbox-safe one.
Values are read as computed custom properties so nested references are
substituted before crossing into the app's separate origin, where a Control
UI token name would have nothing to resolve against.
Live updates come free: the existing theme subscription already re-sends host
context, and `theme` continues to be published alongside this.
* fix(ui): align MCP app host theme semantics