* feat(ui): who's-online roster menu on the footer facepile
Clicking the sidebar footer facepile now opens a scrollable roster of
everyone online — avatar, name, and email subtitle per person, with your
own entry pinned first and marked (you). Uses the stock menu-surface +
wa-dropdown idiom; long rosters scroll inside the popup via the dropdown's
menu part. Session-row facepiles stay non-interactive so row clicks keep
navigating.
* fix(ui): roster menu interaction hardening from review
- close explicitly on wa-select (preventDefault also cancels the
dropdown's own select-and-hide)
- clear the open state when a presence update unmounts the footer
facepile, so the menu cannot remount stale when presence returns
- restore focus to the visible facepile button on keyboard dismissal
(the dropdown's own trigger is a hidden throwaway anchor)
* fix(ui): return focus to the facepile trigger on roster row selection
The About page commit row now embeds the commit's committer timestamp
(commitAt, advisory like branch/dirty) and renders it as an always-relative
age with the exact localized date-time on hover. The copy button becomes a
compact borderless icon revealed on hover/focus only where hover exists;
touch keeps it visible.
normalizeMessage / isToolResultMessage / isStandaloneToolMessageForDisplay
cast their unknown input to a record and then read `typeof m.role`. That
still evaluates `m.role`, so an undefined entry (e.g. groupMessages calling
normalizeMessage(item.message) with an absent message on a pending or
malformed transcript row) threw "Cannot read properties of undefined
(reading 'role')" inside the gateway event handler — a caught but recurring
error storm on the live team instance. Coerce non-object input to {} at the
source so every downstream `typeof m.<field>` check works and role degrades
to "unknown". The existing safeNormalizeMessage try/catch wrapper existed
precisely for this hazard; the guard now lives in normalizeMessage itself.
* feat(channels): batch 1 producers drop media placeholder bodies
Media-placeholder program batch 1: Google Chat, Zalo, LINE, and
Mattermost stop minting <media:kind> placeholder bodies. Media-only
messages carry an empty caption plus one structured fact per native
attachment (type-only when a download fails or is rejected, so payload
positions and kind signals stay aligned). The shared
formatMediaPlaceholderText SDK formatter renders text-only carriers
(Mattermost pending-room lines) from structured facts; per-channel
placeholder builders and the expected-count side channel are deleted.
* fix(mattermost): satisfy type, deadcode, and SDK manifest gates
`openclaw gateway stop` resolved the unmanaged fallback port from config and
discovered pids through lsof only. On hosts without lsof, and whenever the
gateway runs on a port other than the configured one, discovery came back empty
and the command reported `Gateway service disabled.` with `ok:true` and exit 0
while the gateway kept serving.
The gateway lock already holds the verified owner pid and port. Restart learned
to read the lock port in #105241 to keep an unmanaged restart honest when the
configured port drifts; stop's fallback never did. Read the lock identity once in
the not-loaded fallback, use its port for discovery, and signal its owner when no
listener is found. Verified listeners still win when lsof is available.
Signalling still goes through `signalVerifiedGatewayPidSync`, which re-reads argv
immediately before SIGTERM, and lock identities are only returned after a
liveness and start-time or argv check, so dead, recycled, port-less and
non-gateway lock owners are refused and the command stays `not-loaded`.
Closes#72948
* feat: add session discussion panel seam
* fix: keep discussion iframe cookie-capable and changelog release-owned
* test: cover cookie-capable discussion iframe sandbox
* fix: stretch discussion panel host so the embed fills the rail
* fix: keep provider failures retryable and probe discussion availability before showing the action
* fix: block same-origin discussion embeds, show action on catalog sessions, close stale panel on reconnect
* fix: scope discussion probes and panel callbacks to the issuing connection
* fix: dedupe in-flight discussion probes per session
* fix: retry superseded discussion probes and key-scope panel results
* fix: regenerate Swift protocol models and extend advertised-method expectations
* style: format chat-pane-header
* fix: regenerate Kotlin protocol models and date discussion methods in the 2026.7 train
* chore: restore release-owned changelog to main state
* chore: keep changelog untouched relative to merge-base
* fix(ai): honor Retry-After when retry-after-ms is unparseable
The ChatGPT Responses retry path returned as soon as `retry-after-ms` was
present, so a malformed or empty value discarded a valid `Retry-After`
header sent alongside it and the client fell back to blind exponential
backoff. The sibling parser in `provider-transport-fetch.ts` treats the
two headers as ordered preferences and falls through on a parse failure;
this aligns the Responses path with that contract.
* test(ai): deduplicate retry header coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat: avatar upload UI, self-service profiles, server-side avatar proxy
* refactor(ui): make resolveAvatar synchronous so avatar updates render immediately
resolveAvatar no longer does async work (gravatar moved server-side), so
the until()-deferred render left the identity chip img stale after
updateSelfUser. Rendering synchronously reuses the img element and
reflects avatarUrl changes on the next render.
* fix(gateway): bound avatar Gravatar lookup to one timeout budget
Linked-email Gravatar lookups ran sequentially, each with its own 5s
timeout, so an upstream outage stalled the held connection for
5s x linked-email-count. Resolve them concurrently and pick the first
hit in email order, keeping primary-email precedence while capping the
request to a single timeout budget.
* fix(gateway): cap per-profile Gravatar fan-out to bound sockets and memory
A profile with many linked emails would fan out one concurrent fetch per
email with no bound, each able to buffer up to MAX_GRAVATAR_BYTES before
the cache budget applies. Cap the lookups to the first 8 primary-ordered
emails so a single avatar request holds a bounded number of sockets and
transient bytes.
* test: reconcile avatar tests with server-side gravatar model after merge
main landed a client-side Gravatar path (browser computes the gravatar.com
URL); this branch resolves avatars through the same-origin gateway route,
which is the only approach that works under the Control UI CSP (img-src
'self'). Update the identity-section, app-sidebar footer chip, and
profile-page e2e expectations to assert the canonical /api/users/<id>/avatar
route (gateway serves the Gravatar fallback behind it) instead of a
CSP-blocked direct gravatar.com image.
* test(gateway): fix fetchImpl mock param type for check-test-types
The Gravatar mock typed its param as (url: string), narrower than the
fetchImpl signature (URL | RequestInfo); function-param contravariance
made it unassignable under tsgo:test. Widen to the fetch input type and
extract the URL via a small helper that avoids no-base-to-string on a
Request.
* fix(gateway): short-circuit Gravatar lookups and version presence avatar URL
Two autoreview findings on the avatar path:
- Privacy: the concurrent Promise.all fan-out queried every linked email's
hash against Gravatar even when the primary already had one, exposing
secondary work/personal addresses. Resolve sequentially with short-circuit
so a later hash is disclosed only after the earlier email is a definite
miss, under one shared request deadline that still bounds total latency.
- Staleness: presence published an unversioned /api/users/<id>/avatar, so a
reconnecting viewer's <img> kept the stale cached image after an upload.
Carry the profile revision as ?v=<updatedAt> so a changed avatar refetches.
* feat(agents): authoritative write diffs end to end
The write tool now reports what actually happened: no-op (changed:false),
created files (created:true with a bounded numbered diff), and overwrites
(created:false with a real old-to-new diff when the old content is
readable text within byte, line, and edit-distance budgets — unknown
removals are never fabricated). The gateway history projection forwards
the changed/created flags, the web and iOS write rows prefer the
authoritative diff and suppress the +N -0 stat when creation is not
proven, and the iOS transcript cache persists one bounded apply_patch
envelope so patch diffs survive cold opens.
* chore(i18n): sync native inventory
Lazy command-group registration must import the real command tree to render complete help. On macOS with NODE_USE_SYSTEM_CA=1, those imports start Node's system CA loader worker; natural shutdown can block in CleanupCachedRootCertificates while joining LoadSystemCACertificates.
Request the existing stream-flushed one-shot exit immediately after successful Commander help parsing, covering both CommanderError and normal-return plugin help without affecting leaf command execution.