* fix(agents): recover xAI/Grok "could not decrypt encrypted_content" 400 instead of tripping the circuit breaker
openclaw already strips a stale reasoning replay and retries the Responses call, but the
recovery is gated on isInvalidEncryptedContentError(), which only recognizes the
`invalid_encrypted_content` / `thinking_signature_invalid` codes/messages. xAI/Grok returns
a prose 400 with no error code — "Could not decrypt the provided encrypted_content. Ensure
the value is the unmodified encrypted_content from a previous response." — so the matcher
returns false, the call fails, and the per-model circuit breaker trips, blocking ALL
grok-4.3 traffic through the gateway until manual intervention.
Match that message (contains `encrypted_content` and a decrypt-failure phrase) so the
existing strip-and-retry path handles it too. Narrow enough to avoid unrelated
"could not decrypt" messages (e.g. the OAuth sidecar warning), which do not mention
`encrypted_content`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(agents): narrow xAI decrypt retry detection
Co-authored-by: rvdlaar <rvdlaar@gmail.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
The Lobsterdex upgrades to a v2 object schema (v1 arrays migrate in
place, memories backfill on the next visit): each palette remembers its
first visitor's name and date, shown in the gallery tooltips ('Goldie -
first visited 7/5/2026'). Familiarity counters (visits, shoos) shape
behavior with no UI: fewer than three lifetime visits is shy (shorter
stays, later arrivals), fifteen or more is a friend (longer stays,
sooner returns, and a hello wave on the first arrival of each load),
and shooing it too often makes it wary, stretching the gaps between
visits. All best-effort localStorage; tuning lives in one exported
table.
* fix(gateway): re-check session runtime model against current agent defaults after hot-reload
Agent model hot-reload silently did not take effect because session entries
cached modelProvider/model from agent defaults during reset, and the resolver
returned these cached values before checking current config.
Fix (three-pronged):
1. Reset-side: only cache modelProvider/model when the resolved model came
from a user override — default-derived values are no longer persisted.
2. Resolver-side: when runtime metadata exists without overrides and an
agentId is available, the values are default-derived and may be stale —
skip them in the persisted-model fallback so current config defaults win.
3. Inheritance: only inherit runtime model metadata from parent when it
carries explicit user overrides (align with reset-side contract).
Reset response includes resolvedModel so API and TUI consumers always get
the effective model identity.
Fixes#102269
* fix(gateway): restore truncateUtf16Safe and emoji-boundary title test per ClawSweeper review
* fix(gateway): only skip stale session runtime model metadata when it actually differs from current defaults
The previous change unconditionally skipped cached modelProvider/model when no
user overrides were present and an agentId was available, assuming it was always
stale. This broke sessions that legitimately had non-default models set through
normal session creation (e.g. custom vision models).
Now the resolver resolves the current agent default first and compares: if the
cached runtime metadata matches the current default it is returned directly
(not stale); only when it differs is it treated as stale and re-resolved.
Also updates tests that set modelProvider/model without overrides to configure
their agent defaults so the expected model matches the resolution result.
* fix(session-model-ref): add stale-metadata detection for config hot-reload
* fix(test): remove strict timeoutMs assertion in provider catalog live-runtime test
The remainingTimeoutMs calculation can be off by 1ms depending on timing
(Date.now() - startedAt = 1ms on fast CI runners), causing a flaky failure.
This assertion is not the test's focus — dedicated timeout behavior is already
covered by 'uses one timeout budget across paginated live catalog discovery'.
* fix: restore AVATAR_MAX_BYTES to 2MB (revert accidental merge contamination)
* fix(gateway): resolve session models from current config
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* improve(ui): mark the default model inline
* chore: leave changelog to release workflow
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Drop the 32pt drag-overlay clearance above the sidebar tab strip to a
12pt inset - just enough to clear the window's 12pt drag overlay so
every tab pixel stays clickable - and shrink the header from 98pt to
78pt. Follow-up to #103438 per maintainer feedback.
* feat(ui): move app chrome into a topbar and dock the session workspace rail
The desktop shell now uses a slim topbar for brand, primary navigation
(Chat + pinned routes + More menu), command-palette search, pairing,
theme, and Settings; the left column slims down to a sessions-only
panel (Cmd+B hides it entirely — the 78px icon rail is gone). Split
view drops the fixed geometry-mirroring toolbar for in-flow per-pane
headers. The session workspace rail can dock right or bottom inside
its pane — drag its header between edges or use the dock button — and
the collapsed strip's file glyph is now a real button (it used to be a
dead, button-looking span) with a changed-file count badge. The Mac
app's injected chrome CSS slims down accordingly; its drag-region
geometry is unchanged and the topbar brand strip stays passive under
it.
* chore(ui): regenerate locale bundles for topbar and workspace dock strings
* fix(ui): keep the native macOS drawer clear of the titlebar overlay
The narrow-width slide-over drawer sits fixed at the window top, over
the AppKit traffic lights and drag regions. The old Mac-app-injected
CSS padded .sidebar-shell for this; that rule moved web-side for the
desktop topbar, so restore drawer clearance here for both the app
drawer and the in-drawer settings sidebar.
* chore(ui): translate topbar and workspace dock strings; regen docs map
Roughly one load in ten, someone else walks across the footer ledge
once and leaves: usually a stranger lobster wearing a palette the
resident pet does not (hover: 'a stranger'), and on rare loads a crab
(hover: 'definitely a lobster') - wide shell, eye stalks, walks
strictly sideways, smug about it. Passers never perch, never react,
and never count for the Lobsterdex. Planned by a pure per-seed
function, gated by the visits setting, skipped under reduced motion,
and their timers reset with the seed and clean up on disconnect.
Route assistant-visible Tlon output through the shared sanitizer and prove the rendered Urbit poke payload for DMs, groups, and media captions.
Co-authored-by: masatohoshino <g515hoshino@gmail.com>
* fix(mcp): redact secrets in /mcp show replies
Prevent owner /mcp show from dumping raw headers/env credentials into
group chats. Mark MCP env values sensitive, restore redaction sentinels
on set so show→set cannot overwrite real secrets.
Fixes#103053
* test(mcp): e2e redaction of /mcp show secrets on live gateway
Spin up an ephemeral gateway process, chat.send /mcp show with live
credential-bearing config, and prove secrets never leave the reply path.
Also prove show→set sentinel restore on disk via chat and CLI.
The pet watches your pointer between acts (facing follows the cursor,
throttled, reduced-motion inert). Press-and-hold now pets it - content
closed eyes and a floating heart - while a quick tap stays a poke.
Run endings are honest: resolveLobsterRunOutcome picks the most recently
ended session (endedAt first, activity stamps as fallback) so successes
cheer, failures earn a sympathetic droop with sagging antennae, and
user-aborted runs just get an acknowledging startle. Runs longer than
ten minutes summon the pet into a settled vigil pose that pauses all
acts until the run ends.
* fix(queue): prevent applyQueueDropPolicy from selecting in-flight items as overflow victims
When a burst of inbound messages hits the followup queue cap while the
head item is mid-delivery, applyQueueDropPolicy can select that same
in-flight item as an overflow victim. With the default
dropPolicy: "summarize", the in-flight item ends up recorded in the
overflow summary as dropped even though it is still being delivered,
producing a contradictory record where the same message is both
answered and reported as unanswered-due-to-overflow.
The fix introduces an optional `inFlight` Set parameter to
applyQueueDropPolicy and drainNextQueueItem. The followup queue state
now owns a shared inFlight set that is:
- Populated by drainNextQueueItem during the await run(next) window
- Populated by the collect-merge drain path for activeGroupItems
- Passed to applyQueueDropPolicy in enqueueFollowupRun
The drop policy now computes an effective queue length that excludes
in-flight items, and skips them when selecting splice victims.
Fixes#103246
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(queue): align pending depth with active deliveries
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Each inline link opens in its own tab (exact-URL dedupe); sidebar
target=_blank links open new tabs. Tab strip on top with click to
activate, close buttons, middle-click close, pointer-drag reorder, and
a per-tab context menu (Open in Default Browser, Copy Link, Reload,
Close Tab, Close Other Tabs). Navigation controls sit below the tabs
and follow the active tab. Closing the last tab collapses the sidebar
with no lingering webviews or history.
* fix(opencode-go): remove deprecated mimo-v2-omni and mimo-v2-pro model aliases
These deprecated aliases reject agent requests from the OpenCode Go gateway.
Remove them from the provider catalog and clean up all references in probe
skip lists, CI workflows, and tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(opencode-go): complete deprecated MiMo cleanup
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(config): use shared dedupe cache for legacy toolsBySender warnings
Replace unbounded Set with createDedupeCache({ ttlMs: 0, maxSize: 4096 })
from src/infra/dedupe.ts. The shared helper provides check() with
touch-on-read and max-size pruning — no custom LRU stack needed.
Tests verify eviction and hot-key retention through the public
resolveToolsBySender API without a testing-only cache export.
* test(config): tighten legacy warning cache proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(agents): use Buffer.byteLength for bash output rolling buffer accounting
The outputBytes variable tracks the rolling output buffer size for
bash command execution, but it used string .length (UTF-16 code units)
instead of Buffer.byteLength (UTF-8 bytes). When command output
contains multi-byte UTF-8 characters (emoji, CJK, etc.), the .length
undercount causes the rolling buffer to exceed maxOutputBytes.
Replace .length with Buffer.byteLength() at both increment and
decrement sites to correctly track byte-level buffer size.
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(agents): unify bash output accumulation
* refactor(agents): unify bash output accumulation
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ai): cache and case-insensitively match Azure deployment map
resolveAzureDeploymentNameFromMap re-parsed the deployment-map string into
a new Map on every call (a hot path: streams and lifecycle hooks) and
looked model ids up case-sensitively. A request for `GPT-4o` against a
`gpt-4o=deployment-gpt-4o` map therefore fell back to the raw model id and
404'd on Azure ("deployment not found").
Cache the parsed lookup map keyed by the raw deployment-map string (bounded
so memory stays flat) and normalize keys to lowercase so lookups are
case-insensitive. Deployment names (the values) stay verbatim because Azure
requires the exact deployment name, and the fallback still returns the
original-cased model id.
Fixes#102936🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ai): prefer exact-case match, case-insensitive only as fallback
Addresses review: lowercasing every key regressed configs whose deployment
map distinguishes keys by case (e.g. `GPT-4o=prod-a,gpt-4o=prod-b`), where an
exact `GPT-4o` request previously resolved to `prod-a` but would now collapse
to the last lowercased entry.
Cache an exact-case lookup alongside the lowercased one and resolve exact
first, using the case-insensitive map only as a fallback. Every previously
working exact mapping is preserved; the case-insensitive path only rescues
ids that would otherwise 404. Adds a regression test for exact-case
precedence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(ai): prove Azure deployment map resolves on the real provider path
Adds an integration test that drives the real streamSimpleAzureOpenAIResponses
path against a loopback server (via the AI transport host fetch) and asserts the
deployment name resolved from AZURE_OPENAI_DEPLOYMENT_NAME_MAP is what lands in
the outgoing request `model` field — the value Azure routes on, and the source
of the reported 404 when it fell back to a mixed-case model id.
Covers the mixed-case fix (GPT-4o -> deployment-gpt-4o on the wire) and the
exact-case no-regression case (GPT-4o -> prod-a).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* test(ai): use block-body promise executors to satisfy oxlint
no-promise-executor-return flagged the arrow executors around server
listen/close; use statement bodies so nothing is returned from the executor.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
* refactor(ai): simplify Azure deployment lookup
* refactor(ai): simplify Azure deployment lookup
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(ui): normalize Unicode line/paragraph separators in markdown
LLM outputs can contain U+2028/U+2029 chars that split() ignores,
causing content to render as one unbroken line in the chat UI.
Define LINE_SEP_RE using String.fromCodePoint() and apply it
alongside existing \r\n normalization in all five markdown
processing entry points. Mirrors normalizeDisplayLineBreaks()
in exec-approval-command-display.
* fix(ui): normalize markdown line breaks once
---------
Co-authored-by: Peter Steinberger <peter@steipete.me>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(slack): preserve delivery target case
Co-authored-by: 唐梓夷0668001293 <tang.ziyi@xydigit.com>
* fix(slack): restore API IDs at Web API boundaries
Co-authored-by: 唐梓夷0668001293 <tang.ziyi@xydigit.com>
---------
Co-authored-by: 唐梓夷0668001293 <tang.ziyi@xydigit.com>