Centralize untrusted audio transcript framing across channel plugins and keep Discord voice transcripts out of typed command provenance.
Co-authored-by: xialonglee <li.xialong@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(discord): auto-join occupied voice rooms
Add opt-in voice.autoJoin[].whenOccupied residency so Discord voice bots join for the first human and leave when the room becomes empty while preserving existing always-on, manual, transcript, and follow-user behavior.\n\nCloses #125973
* test(discord): isolate process runtime mocks
Use stable hoisted runtime-env mocks so isolate=false Discord test ordering cannot turn sleepWithAbort back into an unmocked function.
* fix(discord): defer unknown voice occupancy
Treat memberless voice states as unresolved instead of human so bot-only rooms cannot trigger occupancy-managed auto-join. Add cache-to-listener and manager regressions.\n\nCloses #125973
* test: isolate shared module mocks
Replace ineffective non-isolated module spies with stable hoisted mocks and a child-process SQLite connection-reuse probe so gateway and Discord shards are order-independent.
* test(gateway): inline connection reuse probe
Keep the child-process SQLite ownership probe in its owning Vitest file so Knip sees the full test surface without weakening process isolation or the original order regression.
* feat(gateway): proxy channel conversation avatars
* feat(discord): capture conversation avatars
* feat(slack): capture DM sender avatars
* test(discord): bind guild avatar mock
* feat(ui): render channel conversation avatars
* fix(ui): align sidebar owner fixtures
* fix(gateway): version channel-avatar routes by media revision
A stable per-session URL let AuthenticatedAvatarRouteLoader's blob and
sticky-404 caches pin a mounted row to a stale or blank avatar after the
backing media changed. Append an opaque digest of the media reference so
replacement and 404-recovery change the route identity.
* test(ui): align sidebar owner facet
* fix(ui): keep owner chip until channel avatar loads
A session with a channelAvatarUrl suppressed its owner chip even while the
blob was loading, auth was not ready, or the route 404ed, leaving an empty
lead slot. The chip now rides as fallback content inside the avatar element
and yields only to a usable image. Covers 404 and auth-pending states;
avatar rows keep renderedOwnerId unset so an owner-viewer stays visible in
the facepile.
* perf(ui): keep channel avatar fallback within budget
* perf(ui): lazy-load the channel avatar element
The avatar element and its authenticated blob loader rode the startup
bundle through session-leading-indicator, pushing startup JS 51 B over the
CI gzip budget. Channel avatars are not startup-critical: register the
element on the first avatar row; the owner-chip fallback covers the
one-time upgrade window. Startup JS returns ~1 KiB under the ceiling.
* build(ui): raise startup baseline for channel avatars
CI-measured startup JS is 344379 B against a 343289 B baseline (+1090 B).
The avatar element and blob loader are code-split out of startup (previous
commit); the residual is the sidebar lead-slot render branch and row
plumbing, which cannot be deferred. Baseline updated via
check-control-ui-performance --update-baseline with CI bytes per the
script's contract; well inside the 4096 B ratchet step and 358400 B
ceiling.
* fix(tts): normalize streamed speech and report fallbacks
Streamed Discord voice now strips Markdown and raw link destinations like buffered synthesis.
Record the first failed streaming provider and reason code before successful file fallback.
* test(ui): stabilize reconnect retry timing
Drive the retry deterministically by advancing the exact 100 ms fake timer while preserving the ordering and outbox proof. Repairs CI run 32110130614 job 95627654678.
* fix(discord): ignore expected TTS stream skips
Address the ClawSweeper finding by retaining warning metadata only for actual failed attempts. Expected unsupported-for-streaming skips stay silent while real streaming failures still warn after successful file fallback.
* test(ui): settle interaction state before assertions
Repairs CI run 32117859632 by invoking the markdown table handler directly with a targeted event and waiting for durable IndexedDB composer state before navigation or page close.
* test: remove incidental UI and process identities
CI run 32120793258 exposed two owner-specific test assumptions.
Preserve the child-session hover visibility and pointer assertions, then focus and activate its menu with Enter so an unrelated delayed hovercard cannot intercept the pointer.
Assert the exact owned descendant PID is dead when QA cleanup completes instead of checking a globally reusable PGID.
* test(ui): stabilize connected send retry timing
CI run 32125741781 exposed the wall-clock race in the connected retry test.
Reuse the canonical realtime audio duration and OpenAI-compatible wire-format helpers while removing redundant state transitions and truncate payload construction. The production diff is net-negative.
Provider interruption policy and lifecycle ownership stay local to each provider; this does not introduce shared mutable assistant tracking or change behavior.
The finished-turn digest ('🧠 N thoughts · 💬 N notes · 🛠️ N tool calls ·
⏱️ Ns') was synthesized onto three surfaces: Slack's Block Kit card footer
(removed in #123851), Discord's final-answer subtext, and a duplicate Telegram
implementation added to match Discord.
Nothing gated it — no config key for the receipt has ever existed on any
channel — so PR #122976, which deleted it from Slack's native progress surface,
could only ever be a partial removal. Finish it: live progress surfaces keep
their in-flight indicators, and a finished turn leaves the answer with nothing
synthesized appended.
Telegram's window collapsed *into* the digest, so it now retires like every
other surface; Discord already discarded its draft after the final landed. The
core tracker keeps only the live counters Slack's working footer reads.
The bound-thread webhook persona path swallowed every pre-dispatch
failure (deleted webhook, revoked token, rate limit) and silently fell
back to the plain bot send. The fallback is intended — persona delivery
is best-effort — but the failure was invisible, so a broken webhook
binding degraded every reply in the thread with no operator signal.
Warn with the error before falling back. Post-dispatch failures still
rethrow (webhookSelected) to avoid duplicate sends.
Generated titles came back in Title Case because none of the title prompts stated a capitalization style; asked for a "title" with no case rule, models default to Title Case and nothing downstream corrects it. The tool-call purpose titles already ask for sentence case, so session titles, Discord auto-thread titles, and Telegram DM topic labels were the outliers.
Sets sentence case in each title prompt rather than post-processing: the titles follow the user's language, and sentence-case rules are language-specific, so a mechanical normalizer would mangle German nouns and proper nouns like OpenClaw or GitHub.
Route error-bearing fan-in abandonment through the existing failure policy so deterministic pre-adoption failures cannot exceed retry limits and starve same-lane messages.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(slack): keep commentary progress on draft lane
* fix(channels): route commentary through one progress owner
Keep non-verbose commentary in Slack and Discord draft lanes while preserving one durable commentary payload when verbose progress makes those drafts yield. Freeze that owner decision for the turn so session changes apply on the next turn.
Co-authored-by: Dallin Romney <dallinromney@gmail.com>
Punchcard-Session: amber-workshop-workshop-36
* fix(channels): refresh queued commentary owner
Recompute the frozen draft-versus-durable commentary owner for every queued follow-up turn and carry that decision into final payload projection.
Punchcard-Session: amber-workshop-workshop-36
* fix(channels): clarify commentary owner opt-in
* fix(channels): preserve queued draft preambles
* fix(channels): require explicit queued progress ownership
* refactor(channels): record commentary progress owner
* fix(telegram): tolerate unscoped queued updates
* test(discord): use canonical draft fixture after rebase
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>