Expose opt-in replyToMode off while retaining threaded replies by default. Keep incoming thread/session identity and explicit tool or CLI targets intact. Resolves#120339. Thanks to @Alfridus1 for the report.
* refactor(imap): consume the core identifier-authentication scale
Use the SDK type and canonical strength comparator instead of the private
SenderStrength vocabulary and index ordering. Bind config values to the SDK
union with a compile-time exhaustiveness check, preserving all config keys.
Classify unproven From ownership, including no-evidence and DMARC temperror
results, as unverified. Only an explicit unverified minimum gains admission;
the default verified floor and asserted minimum keep their behavior. Keep
existing token admission and thrown-authenticator retries unchanged.
Document the remaining mutable token/early-rejection paths and expose only
the canonical comparator through the SDK, with packaged-consumer coverage.
The IMAP introduction has no stable release contract.
* docs(security): record landed sender-auth mitigations in the threat atlas
Record the graded exact-pair identifier-authentication gate and inert mutable
allowFrom audit findings. Mark R-008 implemented in core while retaining
per-channel verified adoption and downstream mappers as remaining work.
Keep the existing atlas structure and Medium residual risk.
Both halves of the docs publish starvation bug (openclaw/docs b4b130bd8,
#131155) were discovered by noticing stale published docs, not by any alert:
nothing watched whether the openclaw/docs mirror actually tracked docs-touching
commits on main. A broken sync (dead token, MDX failure, disabled workflow,
GitHub incident) would again go unnoticed.
Add a scheduled Docs Mirror Freshness check (every 30m) that parses the watched
paths from docs-sync-publish.yml itself - so the staleness definition can never
drift from the sync trigger - finds the newest docs-touching main commit via
the commits API, and compares it against the mirror's .openclaw-sync/source.json
SHA. When the mirror trails by more than 60 minutes it dispatches one
docs-sync-publish recovery run (skipped when a sync is already queued or in
progress) and fails the run so the workflow actor is notified.
The check lives in openclaw/openclaw rather than openclaw/docs because the path
filters it must judge against live here, and same-repo workflow_dispatch heals
with the default GITHUB_TOKEN instead of a cross-repo PAT.
Live-proven: fresh path against the real repos; stale path (detection,
active-run suppression, failing exit) via a simulated stale mirror SHA;
recovery dispatch endpoint via a real manual sync run (33113089552).
* feat(security): warn on inert mutable allowFrom entries
Report configured entries that cannot authorize with name matching disabled,
and preview lockout counts before disabling mutable matching. Distinguish
mutable aliases from exact identifiers with unproven ownership in audit text.
Reuse channel ingress declarations through one SDK classifier without changing
admission policy. Keep Discord audit findings redacted and exclude symbolic
access-group references from mutable-identifier warnings.
Refs #131099.
* test(security): keep entry-authentication audit coverage in its lanes
* fix(tooling): avoid shared tsx cache startup stalls
Use the shared preloader before tsx initializes so maintained build, check, and test commands retain memory transforms without scanning other checkouts' disk caches. Preserve changed-cwd forks and copied tooling closures.
* test: align command contracts with tooling bootstrap
Allow source-only locale contributions to load intentional English bootstrap copy until post-merge generated memory exists. Derive refresh targets from canonical locale data while preserving strict validation.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(ui): show activity cards for online people
Record live person timing independently of heartbeat freshness and expose keyboard- and touch-accessible cards with visible session links. Preserve continuous online intervals across overlapping tabs without adding persistent activity history.
Closes#130649
* build(protocol): refresh generated presence timing fields
* fix(gateway): scope presence snapshots to read-access operators
Apply one recipient projection to hello, system-presence, and events. Keep person timing and time-zone details behind read access, and filter watched references with the canonical session-list policy without leaking hidden counts. Preserve idle-person metadata for readers and prevent non-reader activity-driven frames.
* fix(plugins): preserve Gateway capability load context
Carry the owning registry's metadata generation, workspace, install records, and built-artifact preference through capability discovery. Preserve standalone source loading and existing speech eligibility while avoiding synchronous source transforms on cold Talk catalog requests.
Fixes#130777
* test: align integrated presence and capability coverage
Keep the recipient matrix in its timing-aware owner, close the suite-owned Gateway for shutdown proof, and spy on the canonical install-record reader. Remove the retired private carrier's assertion allowance.
* fix(gateway): broadcast presence when clients connect
Publish completed connection rows through the canonical scoped broadcaster so established readers see first connections and reconnects without waiting for profile edits or activity. Preserve shared online intervals and reset them only after the final socket closes.
* fix(ci): align presence fixtures and sidebar type ownership
Reuse the canonical sidebar host type to remove the type-only lazy-runtime cycle. Assert omitted scopes through hello auth and denied presence reads, and supply presence version callbacks in the shared WebSocket context fixture. Retain startup admission and drain assertions without changing deadlines.
* fix(ui): retain latest scroll commands until their destination
Do not treat the virtualizer's idle debounce or a decreasing offset as reader takeover. Settle at the actual DOM end within one pixel, retaining the separate eight-pixel UI-follow policy and explicit input cancellation. Cover retargeted idle delivery and near-end precision with the real dependency, synchronize pointer baseline capture, and type the existing composer snapshot as its textarea contract.
Fixes#130892
Hard OpenAI OAuth failures now offer one shared login action across normal replies, returned provider errors, fallback summaries, Control UI guidance, and automation alerts. OAuth mode survives failure aggregation so API-key failures never receive the Codex pairing action.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(plugins): surface plugin capability consent in Control UI and CLI
Adds plugins.inspect (declared manifest surface, operator grants, install
provenance/integrity, ClawHub trust), a Control UI consent dialog on install
and external-plugin enable, a server-side acceptance gate persisted on the
install record, artifact-anchored widen diffing, and --accept-capabilities
for non-interactive CLI use.
NOT READY TO LAND: autoreview found critical gaps (see PR notes) — the
declared surface omits 20 of 21 contract families, native plugins always
report zero hooks, several install/enable paths bypass the gate, and the
acknowledgment is not bound to the reviewed surface.
* refactor(plugins): bind capability consent to the reviewed surface
Collapses the consent error payload to the fields the client cannot fetch
(reviewToken, widened, acceptedAt) and pulls identity/declared/grants/source/
trust from plugins.inspect, shrinking the registry-free protocol reader from
395 to 91 lines and removing its divergence from the closed schema.
Acknowledgment now carries the SHA-256 reviewToken of the surface the operator
saw; the server recomputes the final staged artifact's surface and rejects any
mismatch before persisting acceptance. That closes review-then-swap, laundering
of forged acceptance through an unchanged update, and cross-artifact replay.
All 22 manifest contract families are now declared, hashed and diffed, so a
privileged family such as gatewayMethodDispatch can no longer be added without
re-consent. Consent reads the manifest runtime discovery will execute, ambiguous
install ownership fails closed, integrity resolution has one owner and no longer
labels npm SHA-1 shasums as SHA-256, and code plugins disclose that hooks
register at runtime instead of rendering an empty "no hooks" row.
* fix(gateway): register plugins.inspect in method inventories and regenerate protocol
Adds plugins.inspect to the advertised-method inventories (widening the
fixed-size slice windows so older indices stay stable), regenerates the Kotlin
protocol bindings, drops an unused exported type, and replaces two nested
conditional spreads with a plain conditional.
* refactor(plugins): split oversized consent modules and clear lint findings
Extracts the MCP controller out of the plugins page, unchanged-install
reconciliation out of update-installed, and the install lifecycle suite out of
the management-service tests, bringing all three back under the max-lines limit
without suppressions. Also renames a shadowed binding, drops an unnecessary
generic, removes a spread-to-modify in a map, and types catch callbacks as
unknown.
* chore(protocol): regenerate Kotlin bindings after rebase
* feat(plugins): let chat /plugins install review and accept capabilities
The consent gate applies to chat installs too, but the command had no way to
give consent, so external installs dead-ended on a CLI-only flag. Chat now
replies with the plugin's declared capability surface and the exact command to
rerun, and accepts a trailing --accept-capabilities mirroring the existing
--force acknowledgement. ClawHub trust acknowledgement stays CLI-only.
Staged-artifact verification is unchanged: the reviewToken is still checked
against the final artifact before acceptance is recorded.
* refactor(plugins): single-source the declared-surface groups and manifest precedence
The ordered capability group list was defined independently in the consent
engine, the protocol error reader, the CLI formatter and the Control UI, so a
new contract family had to be added in four places with nothing enforcing it.
All four now derive from one canonical list in the protocol schema with a
compile-time exhaustiveness guard.
Native-versus-bundle manifest precedence is centralized in one helper that both
discovery and staged consent call, so the two cannot drift again — that
divergence was a real bug where consent read one manifest and the runtime
executed another.
Also documents that carrying acceptance forward requires pinned artifact
integrity, so integrity-less sources such as local paths ask on every install.
* fix(plugins): enforce reviewed consent across activation flows
Route setup, repair, linked installs, updates, and chat activation through artifact-bound capability consent. Reuse canonical package discovery and recheck staged activation before config publication. Invalidate stale Control UI review requests on reconnect.
Verified focused owner and sibling tests, runtime rebuild, and real isolated CLI/Gateway install, inspect, enable, widening, and stale-token rejection flows.
* test(plugins): cover beta installs through capability consent
* test(plugins): align consent fixtures with staged artifacts
* fix(ui): review staged plugin capabilities once
* test(ui): inline the remaining plugin consent confirmation
* test(plugins): verify consent with deferred install transactions
* refactor(setup): share inference execution plan construction
* test(ui): settle applied config before deferring refresh
* fix(plugins): protect consent provenance and reuse acceptance
* refactor(state): make cron and subagent rows JSON-canonical
* refactor(state): make gateway origin device tokens canonical at v13
The lazy ensure predates the table joining the canonical schema; at the
v13 bump the schema owns creation, so the feature-local DDL, WeakSet
dedupe, and lazy-list entry retire. The legacy-file guard the ensure
carried stays at each call site.
* test: drop obsolete lazy-ensure coverage for origin device tokens
The table is canonical at v13; same-version lazy creation no longer
exists to protect. Origin CRUD, isolation, and rotation coverage remains
in the surviving cases.
* refactor(state): fold installed_plugin_index into config_machine_state
The singleton index row becomes one JSON value under
plugins.installedIndex with its rollback-fencing revision inside the
value; reads, CAS restore, and the lease-held write transactions use
direct Kysely on config_machine_state so the state_leases assertion
stays in-transaction. The v13 migration imports the row and drops the
table; the additive workspace_dir entry folds with it. Doctor guidance,
docker staging, and the e2e probes name the machine-state row.
* refactor(state): merge workspace_attestations into workspace_setup_state
One row per workspace now carries both setup milestones and the
attestation clock: nullable setup columns represent attestation-only
workspaces (replaceWorkspaceAttestation can precede any setup write) and
setupExists derives from a non-null version. The bootstrap-hash FK
repoints to the merged table; migration receipts keep the historical
workspace_attestations discriminator string. The v13 migration grows and
rebuilds the table, merges attestation rows (orphans without a path
alias drop — their hashes re-derive at the next bootstrap attestation),
and the consolidation kind is renamed state-consolidation-v13 to cover
the batch.
* test(state): cover the workspace merge and consolidation fallout
The v12-to-v13 regression seeds merged, attestation-only, and orphan
attestation workspaces; the 13-to-12 downgrade fixture recreates
workspace_attestations and installed_plugin_index from the folded data;
the fold-in migration gates the additive workspace_dir column for
pre-additive rows; the workspace merge now triggers on the setup table's
own shape so stable-era databases without an attestations table still
reshape; the consolidation applied-message covers the batch.
* refactor(state): fold shared auth profile singletons into config_machine_state
The shared-state auth_profile_stores/auth_profile_state rows (fixed key
'shared') become authProfiles.store/authProfiles.state machine-state
values; the agent-DB tables of the same names are untouched. Git-backup
redaction moves from table-drop to the authProfiles. secret prefix with
seeded-secret absence proof; migration receipts keep the historical
table-name discriminators; the shared-auth relocation and receipt
verification project the KV cells back to the receipt-era row shapes so
persisted digests stay byte-compatible. mcp_oauth_stores stays a table —
its multi-key fold is a named follow-up.
* test(state): finish shared-auth fold coverage and annotate boundary casts
Auth seeders and assertions across the e2e/scripts/secrets suites target
the authProfiles machine-state cells; the v12-to-v13 regression proves
payload-byte fidelity, non-shared-row drop, and insert-if-absent
precedence; the downgrade fixture recreates and repopulates both v12
tables. Boundary type assertions in the plugin-index store carry SAFETY
invariants per the ratchet.
* chore: shrink assertion-safety baseline for plugin-index store
* refactor(doctor): delete the dead onboarding-recommendations migration
Its input — the unscoped 'primary' onboarding row — existed only between
9a93a52a8a and 473962b7de, a two-day beta window; no shipped stable
can produce it and the runtime table folded away at v12. The audit
backup list keeps recognizing system-agent.jsonl artifacts because beta
installs that ran that import may still carry its backups.
* docs: sync the 13-to-12 downgrade example with the executable fixture
* style: format the synced downgrade example
* style: drop unused import and duplicate union constituent
* fix(state): keep orphan attestations across the v13 workspace merge
The merged workspace_setup_state required a workspace path, but legacy
orphan hashed-key attestations never recorded one. workspace_path is now
nullable (setup rows still enforce it via CHECK), the v13 migration and
the doctor file import keep orphans with a NULL path that heals on the
next live access, and the 13-to-12 downgrade keeps attestation-owned
hashes. Doctor test seeds move to the folded KV row.
* perf(state): retire unused cron indexes
* fix(state): preserve v13 migration recovery
* fix(state): preserve v12 lazy-table upgrade
* docs(state): document v13 auth relocation
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(cloud-workers): build complete source runtime artifacts
Allow the canonical package builder to include explicitly selected source runtime plugins with exact target-installed dependencies and regenerated inventory. Preserve concise enrollment prerequisite errors without changing trust or normal core publication.
Fixes#130702. Complete source-built cloud worker packages now retain required runtime plugins; deployment artifacts must be rebuilt from the landed source.
* fix(packaging): reject stale selected plugin peer metadata
* fix(pr): release locks after linked worktree teardown
* test(pr): verify native cleanup and retained lock ownership
Initialize the existing script-local supervisor root before canonical Git resolution, and replace overlapping teardown fixtures with the real native merge and gc lifecycle.
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(channels): introduce bots when they join group rooms
* feat(channels): add Discord and Telegram join introductions
* fix(channels): isolate untrusted evidence and select allowed join targets
* refactor(channels): scope joinIntro to implementing channels
* fix(channels): keep a delivered join introduction settled when its durable commit fails
* feat(channels): read more room history and document join introductions in detail
* chore(config): regenerate bundled channel metadata after rebase
* fix(codex): reclaim binding ownership when sessions are deleted
Prepare harness deletion outside the SQLite writer and compare-delete exact companion ownership at the session removal edge, with compensation before commit and native subscription release afterward. Preserve live siblings and upstream native history. Repair proven orphan bindings through the existing Doctor planner under maintenance ownership.
* fix(codex): reuse prepared auth for resumed thread ownership
* docs(plugins): clarify shared migration and auth contracts
* test(codex): use the canonical config type facade
* fix(codex): preserve native threads during manual resume
Record pending attachment intent and validate the immutable native tool catalog
before applying configuration to the selected thread. Require uninterrupted
physical-client ownership through native unsubscribe, resume, and binding commit.
Keep the thread intact on unsupported policy, competing work, or failed proof.
Reuse canonical retained-owner cleanup and preserve unsafe-client retirement.
Cover the real command producer, cold and loaded resumes, competing leases,
lazy-store commit fencing, and the Gateway competing-owner path.
* test: isolate Codex auth and session completion fixtures
* fix(memory): isolate sqlite-vec KNN in subprocess
* fix(memory): satisfy KNN subprocess CI guards
* test(release): include memory KNN child artifact
* test(memory): update KNN subprocess fixtures
* fix(memory): bound KNN child row payloads
* fix(memory): preserve published readers during cancellable KNN
Keep shadow reindex database, vector readiness, FTS and metadata state in
an owned async context. Public operations retain the published connection,
and escaped shadow continuations cannot write to it after rebuild closes.
Terminate the one-shot query child through its owned handle and retain
admission until close. Remove process-tree/PID probing and test-only hooks.
Cover publication overlap, callback reads, cancellation and recovery.
Co-authored-by: OpenClaw Assistant <assistant@openclaw.local>
* refactor(memory): separate database context ownership
Keep the published reader and shadow writer context in its own owner module. Repair plugin test imports and typed lint findings without changing the verified KNN behavior.
---------
Co-authored-by: OpenClaw Assistant <assistant@openclaw.local>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Install public Node and Corepack artifacts in one strict root child shell
with umask 022, leaving the caller's private umask unchanged.
The real sanitized AWS bootstrap failed with exit 127 under umask 0077:
root-owned 0700 installation directories hid the installed Node binary
from the non-root caller. The same-host corrected run completed the
pinned Node 24.19.0/pnpm 11.22.0 install and verified readable public
artifacts, caller umask 0077, temporary HOME 0700, and private files 0600.
Existing bootstrap tests: 2 passed. Shell checks and both required
independent reviews passed. The real privileged-install reproduction is
retained instead of adding a brittle extracted-source installer harness.
* feat(apps): resolve the per-profile accent live on iOS, macOS, and Android
Named follow-up from #130340: native apps now fetch the caller's own
profile accent (users.prefs.get, strict #rrggbb normalization) and prefer
it over the gateway accent, refetching on users.prefs.changed — the
gateway targets that event at the caller's own profile, so clients need no
identity logic. macOS stores it separately from the seam color so
settings-pane config refreshes cannot clobber it, and fetches bypass
ControlChannel.request to avoid degrading the channel on older gateways.
Profile-less and token connections are unchanged. Removes the ios/android
users.prefs.changed allowlist entries now that handlers exist.
* chore(macos): satisfy swiftformat explicit-self on profileAccentHex
Make ask_user use native Telegram controls when one single-select question can be represented directly, while keeping multi-select and batch questions on the text fallback. Add native Other-to-ForceReply input, preserve Gateway-owned option ordering across Telegram, Slack, and Discord, and prevent later progress from overwriting the delivered question.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(buzz): retain bounded passive context for accepted turns
Add opt-in connection-owned room and thread context using the shared history window. Preserve sender admission, current roster filtering, UTF-8 bounds, and snapshot consumption without passive inference, session recording, or typing. Related: #129599.
* docs(config): align baseline with landed Feishu sticker sets
* perf(control-ui): load built-in theme palettes on demand
Every built-in theme's tokens shipped in the startup stylesheet, so the default
path downloaded six palettes it never painted and each new theme taxed everyone.
That is what pushed the startup CSS ceiling from 45 to 47 KiB when Tide, Beacon,
and Phosphor landed.
Moves the twelve non-default palettes into public/themes/<family>.css, one file
per family covering both modes. Claw stays inline because its tokens are the
:root defaults, so the default path loses nothing and gains the bytes back.
The first-paint story this needed: index.html's boot script now links the active
family's palette during head parsing, which makes it render-blocking exactly
like the app stylesheet, so a persisted theme paints its own colours on the
first frame instead of flashing the default. The href is built from the mount
prefix the gateway already stamps on <html>, so it follows a configured Control
UI base path without the script having to know one. theme.ts keeps the link
correct when the theme changes at runtime, reusing the helper the webfont
stylesheets already use.
The nested resolve-theme ternary became a family table in the same script, since
it now picks an asset as well as a data-theme value.
startup CSS 45.8 -> 42.2 KiB gzip, below the 44.3 KiB it measured before the
three themes landed; ceiling restored 47 -> 45 KiB
base.css 64.0 -> 35.8 KiB raw
Adds a regression test that blocks every bundle script and asserts the palette
still applies, so moving this back into the app bundle fails instead of silently
reintroducing the flash. Verified it catches that: with the boot-script link
removed the assertion reports `expected null to be '/themes/tide.css'`.
* fix(control-ui): publish themes after their palettes load
* fix(control-ui): clean up palette completion listeners
* refactor(control-ui): consolidate theme name resolution
* fix(channels): bundled channels reject the documented responsePrefix override
* fix(feishu): pass channel and account to the reply prefix resolver
The ordinary reply dispatcher built the prefix context without channel or
accountId, so channels.feishu.responsePrefix and its account override fell
through to the global value. The comment dispatcher already passed them.
* fix(channels): accept twitch's root responsePrefix and type ClickClack's
Twitch validates as a two-branch union with both branches closed, so
responsePrefix declared only on the account shape left
channels.twitch.responsePrefix rejected whenever accounts is present. Also
declares responsePrefix on ClickClackAccountConfig so typed callers can express
what its schema already accepts.
* fix(clickclack): apply the configured responsePrefix in model mode
Model mode sends the completion straight to ClickClack instead of going through
the agent reply pipeline, so the documented responsePrefix parsed and then did
nothing. Resolve it through the public reply-pipeline seam so template
variables like {model} render from the completion's own provider and model.
* fix(clickclack): do not double the response prefix in model mode
systemPrompt is operator-owned, so a model can be instructed to emit the
configured prefix itself and the unconditional concatenation then sent it
twice. Match the shared reply normalizer's startsWith guard.
* test(clickclack): resolve inbound access through the real resolver
Main now requires access.channelIngress, which the hand-built fixture did not
provide, so the model-mode messages never dispatched. Drop the fixture and let
handleClickClackInbound resolve access from a DM-shaped message, matching the
sibling bot-loop tests.
* test(channels): reject a key missing from any composed branch
A union alternative is a configuration mode an operator can pick, so a key
present in only one mode is unusable in the other even though the union still
validates. Treat any rejecting alternative as a rejection, and walk allOf when
collecting account schemas.
* fix(channels): offer responsePrefix only where the reply path applies it
Applying the prefix to an outbound reply is per-channel wiring, not a shared
step. Buzz, Nostr, QA Channel, Raft, Reef and SMS never read it, so accepting
the key there validated a setting their delivery path ignores. Anchor the
contract to the channels that consume the prefix.
* fix(channels): complete shared response prefix delivery
Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>
* docs(config): refresh response prefix baseline
* test(tlon): use canonical prefix fixture helpers
* test(channels): prove Twitch reply prefix delivery
Exercise schema admission and the real shared reply dispatcher at the Twitch native send boundary, replacing redundant schema-only coverage. Keep Tlon fixtures on canonical imports and recording options.
* docs(channels): clarify explicit message prefix behavior
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(ui): save appearance preferences per user profile
When a Control UI connection is bound to an authenticated user profile
(trusted-proxy, Tailscale, GitHub identity), theme, theme mode, and accent
become per-profile: stored in the existing user_preferences KV store via
users.prefs.set, overriding gateway-wide ui.prefs, with live cross-device
updates over a new additive users.prefs.changed event scoped to the same
merged profile. Restore default deletes only the profile key and falls back
to the gateway-wide value. talk.config projects the caller's profile accent
for native clients. Profile-less connections (token/password/none auth)
keep the existing gateway-config behavior byte-identically.
Release-note context: on multi-person gateways appearance choices are now
personal and follow you across devices; an admin restyling their own UI no
longer reskins the whole team.
* chore(protocol): allowlist users.prefs.changed for mobile clients
iOS and Android resolve the profile accent through talk.config on connect
and config refresh; live per-profile appearance push on natives is a named
follow-up.
* chore(protocol): regenerate Swift/Kotlin models for users.prefs.changed
* fix(ui): honor explicit defaults and identity switches for profile appearance
Addresses the three ClawSweeper review findings:
- resetValue for profile-bound appearance keys is the deletion fallback
(gateway value), so an explicit product-default selection persists as a
profile write instead of being misclassified as a reset.
- An identity switch between two known scopes forces a full appearance
reconcile and clears appearance keys the returning identity never set,
so a shared browser cannot keep the previous profile's look (boot keeps
the last-seen shortcut).
- talk.config joins the profile-dependent dispatch gate so a pending
GitHub identity sync completes before the profile accent is projected;
token clients pass through untouched.
* chore(ui): satisfy assertion-safety ratchet and docs formatting
* refactor(ui): extract server-prefs storage primitives
server-prefs.ts crossed the 700-line cap after the review fixes; the
stateless localStorage persistence primitives move to
server-prefs-storage.ts (no behavior change). Shrink-prunes the
assertion-safety baseline for the moved casts.
* fix(ui): keep imported custom themes out of profile storage
Custom palettes are browser-local by contract, so a profile must never
carry theme=custom to a browser that cannot render it. The wire contract
drops custom from the storable theme values (readers self-heal any stored
value), a profile-bound custom selection stays retained browser-local
(including the offline-queued path), and the exhaustive theme test now
encodes the exception.
* fix(webchat): render CLI harness-injected turns as collapsed context, not operator bubbles
Claude Code writes harness-generated user-role turns (Skill instruction
bodies, continuation summaries) into its native transcript with
isMeta/isCompactSummary flags. The claude-cli history importer dropped
that provenance, so webchat rendered those turns as right-aligned
operator bubbles the operator never sent.
Record the provenance at the import boundary as structured
InputProvenance (internal_system / cli_harness_context) — the same
contract the embedded runner uses to keep internal system prompts out of
operator bubbles — and teach the system-notice path to render such turns
as a collapsed "Injected context" notice that does not start an operator
turn.
Regression coverage: importer unit test (provenance recorded only for
flagged rows), live chat.history gateway RPC test (provenance survives
merge + display projection), and a chat-thread UI test (injected turn
becomes a collapsed notice while a real operator message keeps its
bubble). Before/after webchat captures via
scripts/capture-injected-turn-notice-proof.mts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Publish openclaw/fix-cli-injected-turns-rendering-as-operator-messages
OpenClaw-Publication: 62f03fec-179b-4c5e-9423-4df80809de41
* chore: drop changelog entry; CHANGELOG.md is release-generated
Release-note context lives in the PR body per repo policy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: register injected-turn proof capture as ui:proof script
Knip's full-tree unused-file scan requires capture scripts to be
package.json entry points, matching the sibling ui:proof:* captures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gateway): cover isVisibleInTranscriptOnly harness rows; absorb provenance helper
Claude Code groups isMeta, isCompactSummary, and isVisibleInTranscriptOnly
as its own "not a real operator turn" predicate (verified in the v2.1.246
bundle); import all three through one canonical predicate. Inline the
one-use provenance literal at the importer, returning input-provenance.ts
to its main state.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>