* fix(ui): actionable error and doctor warning for missing widget sandbox origin
* fix(ui): phrase sandbox-origin diagnostics as conditional guidance
* test(ui): satisfy deadcode and mock-factory gates for sandbox diagnostics
Final sweep of the media-placeholder program: with all eleven producers
migrated, the consumer regexes are dead input and are hard-deleted —
media-understanding token un-mixing (bodies are pure user text now),
the persisted-user-turn placeholder drop, the session-memory strip, and
the inbound-event mediaPlaceholder field with its replacement branch
(an intentional removal of shipped SDK surface per the program's
hard-delete decision: plugins still minting placeholder bodies render
them as literal text until updated). formatMediaPlaceholderText remains
the single owner of the <media:kind> vocabulary, used only for
text-only carriers. Legacy tokens in pre-migration history remain
ordinary text.
The .sidebar-view-archived svg rule set stroke: currentColor but never
fill: none, so the archive icon's lid <rect> rendered with SVG's default
black fill — a solid black bar instead of the outlined Lucide archive
box. Align the rule with sibling sidebar icon styles (fill: none,
1.7px stroke, round caps/joins).
Final producer batch of the media-placeholder program: Telegram primary
bodies are caption-only with one aligned structured fact per native
media; the message-cache kind-parsing regex is deleted (native kind
stored directly); reply-chain, debounce/forward, group-history, and
ambient transcript lines render structured facts via the shared
formatter (removing the plugin-local duplicate of the caption-less
literal); audio-transcript and sticker-description replacements gate on
structured facts instead of exact placeholder strings. Also restores
unconditional failed-retryable recording when media resolution is
aborted for live updates, so shutdown cannot silently settle an
undispatched update.
* feat(channels): batch 3 producers drop media placeholder bodies
Media-placeholder program batch 3: iMessage, Signal, and WhatsApp stop
minting <media:kind> placeholder bodies. Primary bodies carry captions
only with one aligned structured fact per native attachment (type-only
for missing/rejected/oversized). Echo, quote, and pending-history
records gain structured media facts so dedupe and previews work without
rendered text. Signal media-only outbound sends drop the placeholder
body entirely (signal-cli accepts attachment-only sends; recipients now
see just the media). WhatsApp audio preflight de-stringed; qa-lab
sticker recognizer moved to structured facts; WhatsApp gif maps to
video (presentation mode, no behavioral consumer).
* test(whatsapp): keep media classifier private
Route automatic dashboard titles through the effective session model and auth profile while preserving explicit cross-provider utility ownership and the existing generic title fallback.
* fix(clickclack): split API base and refresh discussion provider
* chore: regenerate bundled channel config metadata and docs map for the apiBaseUrl key
* feat(system-agent): agentic cached caretaker greeting with quick actions
* test(gateway): pin caretaker greeting in session-ownership tests
* fix(system-agent): offer agent handoff chip only when a model is available
* fix(ci): typecheck narrowing, lint, and knip gates for greeting module
* fix(system-agent): reject structured greeting output on every retained line
* test(gateway): split caretaker welcome tests under the max-lines gate
* feat(anthropic): import Claude Desktop custom groups
* fix(anthropic): resolve Claude Desktop groups from LevelDB entries
Group discovery regexed whole decompressed blocks, so matches were not
attributable to a Local Storage value and byte order decided the winner.
On a real 5.4MB store that mislabelled 59 of 159 sessions, surfacing a
mojibake label spliced out of Snappy copy-record bytes.
Parse SSTable entries properly instead: prefix-delta keys bounded by the
restart array, newest internal sequence per user key (honoring deletions),
and record scanning confined to a single value. Values are normalized so
Chromium's UTF-16 form scans like Latin-1, and unflushed WAL writes keep
precedence over SSTables.
Prod LOC grows ~77; it buys structural correctness in place of ordering
luck, and folds the old index-only walk into one shared entry decoder.
Verified against the live store: 159 assignments, 0 mislabelled versus an
independent entry-level ground truth, 0 control-character labels.
* fix(ui): sort custom session groups ahead of project groups
Custom groups were pushed into the section list as encountered, so their
position depended on roster order rather than the documented behavior;
the existing test only passed because its fixture happened to be ordered
that way. Collect custom and project groups separately and concatenate,
and assert the guarantee with a reversed-input case.
* fix(anthropic): widen LevelDB fixture key type for test typecheck
The prefix-delta helper assigned a Buffer into a Buffer-typed accumulator
whose generic argument differed, which tsgo rejects in the test lane.
* chore(anthropic): drop release-owned changelog edit from the PR
CHANGELOG.md is generated at release time, so a normal PR must not carry
an entry. The release-note context lives in the feature commit message and
the PR body instead.
* fix(ui): make stale-chunk Reload survive the gateway restart
The 'A new version is available' banner appears precisely because the
gateway was just updated — so when the user clicks Reload, that gateway
is usually still restarting. The handler probed once and, on failure,
silently fell back to revalidating the same replaced chunk, so the
button looked dead and a manual hard reload was the only way out.
Poll the document probe until the gateway answers (bounded, so an
unreachable gateway still degrades to the recoverable panel error
rather than a fatal navigation), and show a disabled 'Reloading…' state
while waiting.
* fix(ui): enforce the reload wait deadline locally
The bound was only checked after each probe settled, so it relied on the
probe timing itself out. The default probe does self-abort, but a
caller-supplied one need not, and a probe that never settles would leave
the Reload button disabled forever. Race each probe against the
remaining deadline instead.
* fix(ui): satisfy lint and deadcode gates for the reload retry
CI caught two things local oxlint does not: the wait default returned
the timer id from a Promise executor (eslint no-promise-executor-return),
and retryStaleChunkReload became an unused export once the lazy-route
button moved to the polling variant.
Wrap the executor body, and collapse the two near-identical retry paths
into one: the polling helper with timeoutMs: 0 is exactly the old
single-shot behavior, which the stylesheet-recovery banner keeps.
* fix(ui): keep the reload wait bounded across interval waits
The deadline was checked before the interval wait, so a wait that
carried past it left the next iteration taking the unbounded
single-shot branch — a probe that never settles would then strand the
disabled Reload button past its own bound. Only the first attempt may
probe from outside the window.
* build(protocol): make @openclaw/gateway-protocol publishable to npm
* chore(mxc): align package version with release train
* fix(release): support gateway-only core packages
* fix(clickclack): accept omitted nullable managed-channel fields and auto-open the discussion panel
* fix(ui): open the discussion when write access arrives after it resolved
* refactor(ui): extract the steered-message lifecycle into a typed state machine
The steer flow was an implicit state machine spread across five chat modules,
with chip states encoded in loosely coupled optional fields and duplicated
guard predicates. This extracts it behind structural types:
- steered-chip.ts: leaf chip model — SteeredChip union (in-flight requires
sendState "steering"; acknowledged requires pendingRunId and forbids
sendState), constructors, and narrowing guards; impossible states are
unrepresentable and every call site uses the guards
- steer-lifecycle.ts: owns the steer send flow, single terminal retirement
pipeline (remember -> materialize acked chips -> clear), and history-proven
retirement; absorbs queued-user-turn.ts
- terminal handling in chat-gateway/chat-state and the stored-outbox history
verification in chat-send consolidate onto the shared helpers; chat-send.ts
shrinks by ~300 lines; net prod LOC is flat
- behavior fixes riding the pipeline: acknowledged chips retire once
authoritative history contains their user turn (no more brief chip/history
double display) and lingering chips keyed to a filtered run id now retire on
the next history load
- isTerminalFailureChatSendAck moves to chat-send-contract.ts so ack
narrowing stays type-safe at every formatter call site
* fix(ui): satisfy type, lint, and dead-export gates for the steer-lifecycle split
- restore the snapshotChatAttachments helper (oxc no-map-spread fires on the
inlined form)
- select the stored-outbox drain head with an explicit loop; the find-predicate
form made TS narrow away the failed state and flagged the guard (TS2367)
- drop the unused chip re-exports from steer-lifecycle and the consumer-less
isSteeredChip guard; chip types stay module-local, guard consumers import
the steered-chip leaf directly
* style(ui): oxfmt line joins in chat-send and chat-state
* feat(ui): accept drag-and-drop attachments in new-session composer
The new-thread composer only accepted attachments via paste and the +
menu; dragging a file onto it was silently ignored. Wire the shared
chat attachment drop handling onto the composer shell with the same
balanced drag affordance as the chat pane, and gate both composers'
drop/dragover cancellation on file drags so text/URL drops keep the
textarea's native behavior.
* fix(ui): cancel non-file drops outside editable composer targets
A URL dropped on the transcript, header, or composer chrome would hit
the browser default and navigate the app away, discarding drafts. Keep
native text/URL drops only when the drop target is an editable control;
cancel them everywhere else in both composers.
* fix(ui): treat disabled and readonly inputs as non-editable drop targets
A URL dropped precisely on a disabled composer textarea would still hit
the browser default and could navigate away. Check actual editability
(disabled/readOnly/isContentEditable) instead of selector shape.
* fix(ui): limit native drops to text-entry inputs
Enabled non-text inputs (checkbox, range) counted as editable drop
targets, so a URL dropped on one skipped cancellation and could
navigate the browser away. Restrict the native-drop exception to
text-capable input types, textareas, and contenteditable content.
* feat(ui): show waiting approval run status
* fix(ui): hydrate waiting approval status
* fix(ui): correlate approval status by run
* fix(ui): harden approval status reconciliation
* fix(ui): keep steered composer messages visible until the transcript owns them
Typing while the agent was busy showed a brief steering state and then the
message vanished until the next full history reload. The steer path removed
the queued row at ack time and only restored the steered chip when the tab
had adopted the active run id, which never happens for runs started by
automations, other clients, or tabs opened mid-run.
- steer sends reuse the durable row's sendRunId as the wire idempotencyKey so
delivered-turn and history-proof reconciliation can correlate steered rows
- the steered chip is restored after every accepted ack, keyed to the active
run when the tab still tracks it, else to the steer's own gateway lifecycle
- terminal events materialize acknowledged steered chips into the transcript
before clearing them, and stale history reloads keep idempotency-marked
local turns until authoritative history catches up
- in-flight (unacknowledged) steers are never materialized, so a rejected
send cannot leave a phantom user turn behind
Fix authored with Codex; reviewed and hardened via structured review.
* fix(ui): use bracket access for __openclaw test marker (no-underscore-dangle)
* fix(ui): resolve steered attachment payloads through the store before materializing
Queue rows carry attachment metadata only; composer upload bytes live in the
payload store. Resolving through getChatAttachmentDataUrl keeps attachment-only
steers visible when their chip retires (Codex connector review finding).
* fix(ui): only user-role entries satisfy queued-turn presence checks
An assistant entry can carry the same run's idempotency key; matching it let
the chip re-add and terminal materialization skip the user's copy
(ClawSweeper rank-up move).
* fix(ui): show attributed sender identity properly in chat
Three defects around multi-user sender attribution:
- Sender labels rendered as 'name (<profile uuid>)': resolveSenderLabel
appended the id part even when it is an opaque UUID. Suppress UUID
suffixes server-side and strip already-baked ones in the UI normalizer
so historical transcripts read clean too.
- Attributed messages showed initials instead of the author's avatar:
chat sender metadata carries only the profile id, so resolveAvatar now
derives the canonical /api/users/<id>/avatar route (upload -> gateway
Gravatar proxy -> initials) from UUID-shaped sender ids.
- Author identity was hover-only: identity-resolving gateways keep the
always-visible avatar gutter even in 1:1-shaped sessions, and the
gutter renders the message author's avatar (with identity-colored
initials fallback) instead of the local viewer's.
* fix(ui): preserve legacy author identity and 404-safe sender avatars
Review findings: stripping the baked UUID suffix discarded the only
author key on legacy rows (the gutter then showed the local viewer's
face under another person's label) — the UUID now survives as
structured non-display sender identity. And the derived avatar route
can 404 (no upload, no Gravatar), so the gutter image swaps to
identity-colored initials on error instead of a broken image.
* fix(ui): attribute bare-UUID legacy sender labels to their profile
A nameless legacy sender labels as the bare profile UUID; recognize that
whole-label shape as the sender id (keeping it as last-resort display)
so the row resolves the author's avatar instead of misattributing to the
local viewer.
* perf(state): cap the per-agent SQLite handle cache with LRU eviction
Multi-tenant hosts open one WAL database per agent (~3 file descriptors
each); the process-local handle cache was unbounded, so large fleets
exhausted descriptors. Cache hits now refresh LRU recency, and cache-miss
opens evict the oldest non-transactional handle before constructing the
new one, capped at 64 open handles. Eviction closes the process-local
handle only; registry rows and durable data are untouched and evicted
databases reopen transparently on next access.
* fix(state): fleet-wide introspection reads stop opening writable agent databases
Live testing an 81-agent gateway surfaced three full-fleet sweeps that
opened every agent's SQLite database writable (schema ensure + registry
write transaction per open): the 60s health snapshot's per-agent session
summaries, usage-cost cache reads behind the gateway usage endpoints, and
the zalouser doctor detector that ran from every CLI startup. Each sweep
churned the new bounded handle cache and saturated the event loop.
Session listing gains a readonly non-registering variant (SDK exposes it
as an additive readOnly flag on listSessionEntries); health and usage
reads use it, with transient-lock tolerance owned by the health caller.
The zalouser detector gates on the channel/credentials actually existing,
collects once per pass, and detection reads go readonly while migration
apply stays writable. The usage refresh queue's flat 50ms busy retry
becomes exponential backoff capped at 5s, and refreshing summaries retain
their cache timestamp so the 30s TTL gates fleet rescans.
* perf(state): make per-agent handle eviction and reopen cheap
Eviction only works if reopen is cheap. Reopens after eviction previously
repeated full first-open work; ensureOpenClawAgentSchema takes BEGIN
IMMEDIATE and the registry upsert writes the shared state DB, so reopens
blocked in synchronous busy waits (observed 14-22s event-loop stalls with
reconcile workers holding write transactions). Owner/schema validation and
registration now run once per path per process with invalidation on
quarantine and disposal; the read-only integrity guard still runs on every
physical open. Eviction closes with a PASSIVE WAL checkpoint instead of
TRUNCATE, which waits on readers; orderly dispose keeps TRUNCATE so
sidecars are flushed before unlink.
* ci: regenerate plugin-SDK API baseline, drop prod test-seam exports, mock readonly accessor
The additive readOnly flag on the SDK listSessionEntries changed the
declaration surface, so the generated API baseline is regenerated via the
sanctioned script. Knip forbids production exports consumed only by tests:
the usage-cache testApi export is removed outright and the refresh-queue
seam moves behind an env-gated global exposed by a test-support module.
health.plugins.test's full session-accessor mock gains the newly imported
readonly listing.