Rewriting apps/.i18n/native-source.json without regenerating the derived
Android/Apple artifacts lands a stale iOS catalog and turns the repo-wide
apple/android i18n checks red for every PR (this is how main broke after
recent app-string additions: the inventory and translations were
committed while Localizable.xcstrings was not regenerated).
native-app-i18n.ts sync --write now chains syncAndroidAppI18n and a new
exported syncAppleAppI18n (catalog + InfoPlist strings) so one command
leaves the whole i18n tree consistent. The shared locale list moves to
scripts/native-i18n-locales.ts to break the import cycle between the
inventory scanner's top-level CLI await and the derived generators.
The Mac app injects a 52px transparent titlebar, so native chrome centers
at y=26 while the 48px pane header centered its content at y=24, leaving
the session title/chip/actions 2px high. Native desktop shells now size
.chat-pane__header from --openclaw-native-titlebar-height (same 50px
fallback as .macos-titlebar-controls).
* fix(secrets): use resolveStateDir for .env path to cover legacy .clawdbot dir
* fix(secrets): replace override-based legacy .env test with true automatic fallback, add apply-path coverage
* fix(config-cli): emit JSON error on config get --json for missing paths
When is used with a nonexistent
path, the command emits no JSON on stdout, only a text error on stderr.
This breaks automated consumers that expect machine-readable errors.
Fix: output JSON to stdout before exit(1) when
opts.json is set. This matches the existing JSON error output pattern
already used by runConfigValidate and handleConfigMutationError.
Also re-throw synthetic exit errors in the catch block so the
--json branch's writeRuntimeJson+exit is not caught and re-emitted
as a text error.
* fix(config-cli): use typed ExitError signal instead of __exit__ string sentinel
* fix(secrets): reuse resolved stateDir for .env path in audit
* fix(secrets): pass resolved stateDir into apply .env scrubbing, add root-switch regression
* test(config-cli): migrate remaining __exit__ sentinel to ExitError
* fix(secrets): cover config and state dotenv roots
* style(secrets): format dotenv path helper
* test(secrets): track temporary directories
* test(config): expect typed exit for missing patch files
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Peter Steinberger <peter@steipete.me>
* fix(mentions): support single-char CJK names in mention pattern matching
The \b word boundary assertion does not recognise CJK characters as word
characters in JavaScript regex, so single-char CJK mention names (e.g.
@包) failed to match in group chat messages. Replace \b with explicit
lookaround assertions that treat CJK characters as boundary-worthy when
the mention pattern contains CJK characters.
Fixes#87303
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): pass agentId to buildMentionRegexes in CJK mention tests
The test config uses agents.default.identity.name but was calling
buildMentionRegexes(cfg, undefined). Without an agentId, resolveAgentConfig
is never called and the identity is not resolved, producing empty regexes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): use agents.list config shape for CJK mention tests
Use the canonical agents.list array config shape instead of the
non-existent agents.default object. Pass the matching agent id to
buildMentionRegexes so resolveAgentConfig finds the identity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(mentions): match derived names with Unicode boundaries
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* test(ui): cover plan-snapshot replay on reconnect end to end
QA follow-up to the plan-replay work: the mock gateway scenario can now
serve an inFlightRun snapshot plus session run state through chat.history
and chat.startup, and a Playwright suite proves the checklist seeds from
the replayed snapshot on load, is replaced in place by a newer live plan
event, clears on the terminal outcome, and survives a mid-run page
reload. Registered as QA scenario control-ui-plan-replay-reconnect.
* test(ui): satisfy plan replay e2e lint
* fix(elevenlabs): fall back to default base URL when config value is malformed
* fix(elevenlabs): reject malformed and non-http(s) base URL overrides
* fix(elevenlabs): redact configured URL from base URL validation errors
* fix(elevenlabs): preserve realtime WebSocket overrides
* test(elevenlabs): avoid stale realtime test overlap
* style(elevenlabs): format realtime URL assertion
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(ui): restructure Settings IA around user-facing pages
Settings sidebar shrinks to settings-only destinations grouped by attention:
General/Appearance/Notifications up top, Connections, Agents & Tools,
Privacy & Security, System. Sessions, Worktrees, Activity, and Memory
Import leave the takeover: Sessions/Activity return to the workspace
sidebar, Worktrees becomes a Sessions hub tab, Memory Import is reached
from the Agents memory panel. Model Setup becomes a Models subpage.
New config pages: Notifications (web push), Privacy & Security (curated
policy rows + security/approvals schema sections), Advanced (uncurated
schema sections + raw editor, replacing the General quick/advanced
toggle). General slims to model defaults, language, and gateway host;
identity moves to Profile, lobster appearance rows to Appearance.
* feat(agents): widen system-agent config writes to operator parity
Every config surface the Control UI can edit is now agent-writable behind
the exact-operation human approval gate, except a small documented
denylist ($include, auth, env, models, secrets). tools.* and
plugins.entries.* writes are allowed; plugin_uninstall works for plugins
that provably do not back the active default inference route (fail closed
otherwise); set_default_model takes an optional agentId and live-tests a
per-agent model with the same rigor as the default route. Per-agent
routing fields on non-default agents are direct-writable; the default
agent's route keeps the verified set_default_model/onboard paths. A new
config-write-parity contract test forces explicit classification of every
top-level config key.
* feat(ui): sync operator display prefs through config ui.prefs
Theme, theme mode, text scale, locale, and chat display prefs get a
canonical server-side home in config ui.prefs. The Control UI mirrors
them into localStorage for instant boot, writes local changes through to
the gateway (best-effort; viewer-scope or offline clients stay
device-local), and applies server-side deltas on connect and on every
config snapshot reload — so an agent changing the theme through the
approval gate takes effect in the UI, and prefs follow the operator
across devices.
* chore: prune quick.ts from max-lines baseline after slim-down
* style: oxfmt changed files
* chore: regenerate plugin-sdk api baseline for ui.prefs config surface
* fix: dedupe runPluginUninstall dep type and narrow locale extraction
* docs: update Control UI settings structure and ui.prefs sync
* fix: address autoreview findings on parity guards and prefs sync
- plugins.entries.* config writes get the same active-route ownership
check as plugin_uninstall (fail closed when unprovable)
- system-agent prompt now describes the guarded write policy instead of
the old blanket refusals
- per-agent set_default_model logs the agent-scoped label
- legacy /config?section=... deep links redirect to the Advanced page
- prefs sync: pushes coalesce and drain serially (no CAS races dropping
updates), replaced config hashes mark pre-patch snapshots as stale, and
the last-seen server value persists per gateway scope so reloads cannot
revert unpushable viewer-scope edits
- lint: theme membership checks use Sets
* fix: harden uninstall route guard and scope prefs queue per gateway
- plugin uninstall ownership now covers execution owners (runtime policy,
embedded harness override) in addition to the provider, and re-verifies
inside the commit boundary so a concurrent route switch cannot slip a
destructive uninstall past the pre-approval check
- prefs push queue binds to one gateway client; switching gateways drops
the old queue (device-local fallback) instead of cross-writing prefs
* style: top-level type import in config-page test
* fix: close review races in config-write guards and prefs reconciliation
- config_set/config_set_ref re-verify the inference-route guard inside
the commit boundary so conditional verdicts (per-agent routing, plugin
entries) cannot be flipped by a concurrent edit after approval
- server prefs apply per changed field, so a server change to one pref
no longer reverts unpushable local edits of the others
- replaced-hash staleness retires once post-patch state is observed, so
a genuine restore of the pre-patch config becomes authoritative again
- moved-section deep links (communications notifications, automation
approvals) redirect to their new pages
* style: satisfy no-useless-assignment and loop-condition lints in prefs sync
* fix(ci): resolve unhandled profile-test rejections and knip unused exports
The profile page's new identity section reads context.config; the test
context mock now provides it, eliminating the unhandled rejections that
failed checks-ui and the compact-large shard. Exports flagged by the
dependency gate (extract/patch helpers, section-key and prop types) go
module-private; extraction coverage moves behind applyServerUiPrefs.
* refactor(agents): move config-write policy to its own module
The static path classifier and documented denylist move to
config-write-policy.ts, shared by the execution guard and the parity
contract test — no test-only exports left for the dependency gate to
flag. Blocked-root refusals now cite the denylist's documented
escalation for that root.
* feat(clients): provider-grouped model picker with fast, verbosity, and inherited-thinking controls
* chore(i18n): translate model-control strings and regenerate platform catalogs
* feat(clients): inherited-default restore for fast mode and verbosity controls
* fix(clients): preserve control state in thinking snapshots and persist inherited verbosity
* chore(i18n): resync native inventory after rebase onto message-reader main
* chore(i18n): realign locale artifacts after rebase onto message-reader main
* refactor(clients): drop superseded fast-mode accessors and picker convenience flagged by periphery
* chore(i18n): resync inventory line refs after dead-code removal
* fix(ci): bound CLI installer smoke curl with connect and max-time
The non-root CLI installer smoke pipes curl directly into bash under a
single docker-run timeout. curl has no default connect or total timeout,
so a stalled CDN connect or hung response body could pin the container
until the outer docker-run budget kills it. Bound the curl with the same
--connect-timeout 30 --max-time 300 shape already used by install-sh-smoke
and install-sh-e2e, and enable pipefail so curl failures are not masked
by the bash consumer.
* test(ci): cover bounded CLI installer pipeline
* fix(ci): bound preceding non-root installer download
* fix(ci): harden bounded installer URL handoff
Co-authored-by: zhang-guiping <zhang.guiping@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(acp): persist spawn label to target session store instead of requester store
When /acp spawn --label <name> creates a cross-agent ACP session (e.g.
from main into cursor or codex), the label was written to the requester's
store partition rather than the spawned session's store. The write was a
silent no-op because the session key (agent:cursor:acp:<uuid>) did not
exist in the requester's store. Follow-up commands like /focus <label>
and /acp status <label> then failed with "No session found with label".
persistSpawnedSessionLabel now resolves the canonical store path from the
spawned session key via resolveSessionStorePathForAcp before writing, and
only mutates the in-memory requester session store when both stores are
actually the same.
Fixes#106136.
* fix(acp): export resolveSessionStorePathForAcp for cross-store label persistence
* refactor(acp): extract spawn binding helpers to bindings.ts
* fix(acp): remove unused oxlint-disable after extracting spawn bindings
* chore(acp): prune stale max-lines baseline entry for lifecycle.ts
* test(acp): tighten cross-store spawn label coverage
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(infra): return undefined for bigint exceeding MAX_SAFE_INTEGER in normalizeSqliteNumber
Number(bigint) silently loses precision for values beyond
Number.MAX_SAFE_INTEGER (2^53-1) and returns Infinity for extremely
large values. Downstream code comparing these values against thresholds
(e.g. expiration timestamps, byte counts) could make incorrect decisions.
Return undefined instead so callers can handle the out-of-range case
explicitly.
* fix(sqlite): harden bigint normalization proof
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(cli): wrap readFileSync ENOENT with user-friendly messages and suppress cause leak
* fix(secrets): type missing plan errors
* style(secrets): format missing plan error branch
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(daemon): reload handoff outwaits gateway drain before re-bootstrapping
The launchd reload handoff waited only 15 x 0.2s (~3s) after bootout for the
label to unload, but the booted-out gateway keeps the label registered until
its drain-before-exit window (up to 300s) completes. Bootstrap then failed
with EIO mid-drain, and the kickstart -k fallback cannot succeed on a
booted-out label, leaving the LaunchAgent deregistered and the gateway down
until a manual bootstrap.
Extend the post-bootout wait to cover the full restart-deferral budget plus
margin (315 x 1s, derived from DEFAULT_RESTART_DEFERRAL_TIMEOUT_MS), and
replace the single dead kickstart fallback with a bootstrap retry loop that
only falls back to kickstart -k while the label is actually registered, so
the handoff never exits with the service deregistered.
Closes#110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(daemon): scale reload bootout wait to the effective drain budget
A config-raised gateway.reload.deferralTimeoutMs would outlast the fixed
default-derived wait and reopen the stranded-LaunchAgent race. Thread the
effective deferral timeout from restartLaunchAgent into the handoff and derive
the reload bootout wait from it; unbounded (<=0) configs keep the finite
default wait, with the bootstrap retry loop covering the overshoot.
Related: #110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(daemon): derive reload wait from launchd ExitTimeOut and keep failed bootstrap status
Review follow-up: the bootout SIGTERM path is bounded by the plist's
ExitTimeOut, not the gateway's restart-deferral config, so derive the reload
bootout wait from LAUNCH_AGENT_EXIT_TIMEOUT_SECONDS plus margin and drop the
deferralTimeoutMs threading. Capture a failed bootstrap's status in the else
branch: after a completed if with a false condition $? is 0, so exhausted
retries logged 'restart done' and exit 0 while the LaunchAgent stayed
deregistered. Adds an execution-level retry-exhaustion test that runs the
generated script against an always-failing launchctl stub.
Related: #110137
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(daemon): exercise delayed launchd reload handoff
Run the generated reload handoff through delayed-stop and exhausted-bootstrap
paths, and clarify that the wait covers launchd's ExitTimeOut stop window.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* test(daemon): narrow generated handoff script
Fail clearly when the spawn arguments omit the generated script and pass a
narrowed string to the execution helper.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* fix(daemon): retry bootstrap after launchd kickstart race
Continue the bootstrap retry loop when a label disappears between the
registration check and kickstart, and cover the race by executing the generated
handoff script.
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
* test(daemon): make handoff no-wait sentinel explicit
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
---------
Co-authored-by: MatthewSynthia <299972631+MatthewSynthia@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(mattermost): adopt durable ingress drain at the websocket chokepoint
Posted events processed detached from the websocket receive with only a
5-minute in-memory guard; a crash lost the post and reconnect never replays
it. Raw posted envelopes now journal durably (event_id = post.id per the
upstream Post model, lane per channel_id, one row per post) before handler
scheduling; dispatch runs through the core drain with deferred claims through
debounce, merged-flush fan-out adoption, gated-turn settlement, and 30d/20k
tombstones covering the old 5min/2k guard, which is deleted after its parity
test. post_edited stays excluded and cannot be swallowed by posted
tombstones. Cold-gap limitation stated: Mattermost cannot replay posts missed
while disconnected.
Autoreview blocked by codex sandbox network in the build stage; full manual
review performed (updated one websocket test asserting the pre-adoption
parsed-post contract to the raw-envelope contract). Part of #109657 wave 2.
* style(mattermost): keep ingress monitor type internal
* fix(mattermost): retry then loudly escalate a failed durable append
Landing autoreview caught a real loss path: a durable enqueue failure at the
websocket chokepoint was logged and swallowed — the raw envelope discarded,
the connection kept running against a broken store, and reconnect never
replays, so a transient SQLite failure silently lost the post. The append now
retries with short backoff for transient blips; a persistent failure
propagates and the websocket terminates loudly so the outage is
operator-visible instead of silently dropping every subsequent post.
Regression test covers both the absorbed-transient and escalation paths.
* style(mattermost): format rebased ingress handler
* docs(mattermost): document bounded auth-failure retries under deferred claims
* fix(mattermost): guard the drain pump against stop racing the async prune
stop() disposing before the startup pump finished pruning let the pump
lazily create a fresh undisposed drain and dispatch after shutdown. The pump
now re-checks running after the prune, and stop() disposes again after
awaiting the pump so a drain created mid-race is torn down. Regression test
blocks the prune across stop and asserts no dispatch.
* fix(mattermost): serialize durable admissions to preserve lane order
Concurrent websocket callbacks let a post in append-retry backoff be
overtaken by its successor, inverting same-channel arrival order in the
queue. Admissions now chain (order over latency, mirroring the iMessage
admission tail); regression proves a retried post still lands ahead of a
concurrently received one.
* test(mattermost): assert lane order via dispatch sequence
* fix(mattermost): stop() awaits in-flight admissions before disposal
* fix(mattermost): satisfy ingress lint checks
* fix(mattermost): honor envelope-level channel ids in the durable inspector
Posts can carry their channel id on the post, the event data, or the
broadcast envelope — the monitor dispatch honors all three, but the ingress
inspector and claim-side validator required the nested field, rejecting valid
posts as permanent and (via the storage-failure escalation) tearing down the
socket for a failure that never happened, losing posts reconnect cannot
replay. Both sites accept the three shapes; regression proves an
envelope-level post dispatches.
* fix(anthropic): add claude-fable-5 to CLI allowlist, aliases, labels, and context window
CLI path was missing claude-fable-5 across all four metadata layers while
the direct Anthropic provider path in register.runtime.ts already had full
support via isAnthropicFable5Model() / resolveAnthropicFixedContextWindow().
- CLAUDE_CLI_DEFAULT_ALLOWLIST_REFS: add claude-cli/claude-fable-5
- CLAUDE_CLI_MODEL_ALIASES: add fable/fable-5/claude-fable-5 mappings
- CLAUDE_CLI_CONTEXT_WINDOWS: add claude-fable-5 -> 1_000_000 (was falling back to 200K)
- CLAUDE_CLI_MODEL_LABELS: add Claude Fable 5 (Claude CLI)
- resolveClaudeCliImageMediaInput: add fable-5 to 2576 max-side tier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(anthropic): route fable alias through context/model-ref canonicalization paths
* fix(agent): include claude-cli in fable-5/mythos-5 fixed context window resolution
* fix(anthropic): add claude-fable-5 to static CLI manifest catalog
* test(anthropic): cover fable-5 alias canonicalization in bare and provider-qualified forms
* style(anthropic): fix indentation in plugin manifest
* fix(anthropic): publish Fable CLI output limit
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>