Commit Graph

11514 Commits

Author SHA1 Message Date
Ben Badejo 9b36c8bc56 fix(docs): document OpenAI audio auth with OAuth (#128885)
Co-authored-by: Benjamin Badejo <ben@benbadejo.com>
2026-08-24 15:58:05 -07:00
Martin Cleary 7f24f54214 docs: require discussion for material SQLite changes (#128876) 2026-08-24 23:53:14 +01:00
qingminlong 97cc8d6d34 docs: correct directory JSON failure envelope (#124989)
* docs: correct directory JSON failure envelope

* docs(cli): document canonical config and directory JSON failures

Preserve the contributor directory error-envelope correction and update config get to distinguish schema-valid unset paths from unknown paths while linking the shared canonical CLI JSON failure contract.

Co-authored-by: qingminlong <qing.minlong@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-24 12:48:27 -07:00
Peter Steinberger 3c2667ca40 fix(gateway): fill older chat pages without duplicate transcript storage (#128810)
* fix(gateway): page visible chat history without duplicate storage

* chore: keep release changelog owned by release automation
2026-08-24 10:43:59 -07:00
Ayaan Zaidi 614e084cf8 improve: serialize Telegram Mantis proofs (#128811)
Queue Telegram-visible Mantis proofs at workflow level so only one shared-user run allocates a runner at a time.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-24 23:10:45 +05:30
Vincent Koc 7d08e40e2f fix(qa): run explicitly selected Telegram RTT scenarios (#128795)
* fix(qa): support explicit Telegram RTT scenarios

* docs(qa): document Telegram RTT scenario selection

* fix(qa): expose Telegram RTT scenario in manual E2E

* fix(qa): gate historical Telegram downgrade proof
2026-08-24 10:26:30 -07:00
Shakker 0fc18f7447 fix: prevent stale transcript projections from publishing (#126947)
Fence transcript projections by source generation so stale rebuilds cannot publish or satisfy readers.

Refs #126914.
2026-08-24 17:38:34 +01:00
Shakker 59abf3de62 feat: persist stateful transcript display semantics (#126943)
Persist stateful transcript display semantics through one shared reducer for incremental and rebuilt projections.

Refs #126914.
2026-08-24 17:36:21 +01:00
Peter Steinberger 053b89d80f improve(ui): open short session links without extra lookup (#128778)
* perf(ui): remove short session route waterfall

* docs: clarify short link gateway requirement
2026-08-24 09:33:20 -07:00
Shakker 59a9221180 feat: persist bounded transcript display rows (#126939)
Persist bounded transcript display rows and lazy projection lifecycle in the per-agent SQLite store.

Refs #126914.
2026-08-24 17:28:49 +01:00
Peter Steinberger fc85d5fbfe docs: align systemd host tuning with managed policy (#128752) 2026-08-24 08:38:27 -07:00
Peter Steinberger 9ee238dd45 fix(tui): restore session setting reset directives (#128731) 2026-08-24 06:12:46 -07:00
Finn763 659ca1ed01 docs(install): surface desktop app download links for normal-app installs (#127991)
* docs(install): surface desktop app download links for users who want a normal app install (#126694)

* docs(install): clarify both desktop apps provision gateways

Keep the contributor desktop-download instructions accurate for both Windows Hub and the macOS app: each can provision a local Gateway during first-run setup or connect to an existing remote Gateway.

Co-authored-by: Finn763 <165816600+Finn763@users.noreply.github.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-24 06:06:03 -07:00
Peter Steinberger e30279f89f fix(google): support stable Gemini Embedding 2 request contracts (#128716)
* fix(memory): support stable Gemini Embedding 2

* fix(memory): validate async Gemini batch dimensions

* docs(memory): explain stable Gemini index rebuild

* docs(memory): cover explicit-dimension rebuilds

* fix(google): honor stable Gemini embedding task contracts

Co-authored-by: Franck MEYER <meyerfranckpro@gmail.com>

---------

Co-authored-by: Codex OpenClaw Migration <noreply@local>
Co-authored-by: Franck MEYER <meyerfranckpro@gmail.com>
2026-08-24 05:38:54 -07:00
Peter Steinberger 33b10fe4aa fix(macos): user accent from Control UI is clobbered by config snapshots and never live-updates the chat window (#128703)
* fix(macos): honor user accent precedence in config snapshot and live-update chat window

The Control UI user accent (ui.prefs.accent) landed in #128432/#128577 with
precedence user accent -> operator ui.seamColor -> theme default, and the
gateway's talk.config payload already applies it. The macOS app had two gaps:

- ChannelsStore.applyUIConfig read raw ui.seamColor from the config.get
  snapshot and clobbered the user accent set from talk.config depending on
  arrival order. It now resolves ui.prefs.accent ?? ui.seamColor via a
  testable helper mirroring the gateway precedence.
- The native chat window read AppStateStore.seamColorHex once at window
  construction, so accent changes never live-updated. MacChatSurface now
  reads the @Observable store in body, deleting the one-shot userAccent
  plumbing.

Docs: configuration-reference.md documents the precedence for native-app
chrome. Regression test fails pre-fix (snapshot returned the operator seam
color instead of the user accent).

* fix(macos): refresh config from gateway config.changed events

Addresses the review finding that no macOS consumer turned the gateway's
hash-only config.changed broadcast into refreshed shared state, so a
Control UI accent change never reached an open native chat window while
the app ran. ChannelsStore now subscribes to gateway pushes and re-fetches
config.get on config.changed, reconnect snapshots, and sequence gaps.

The refresh applies non-force so an in-progress local settings draft wins
(the gateway rejects stale-hash writes anyway). The in-flight reload queue
gains a closed pending level (none/refresh/force) so a refresh arriving
during a load is coalesced instead of dropped, and a requeued refresh
cannot clobber a dirty draft the way the old boolean force-pending did.
2026-08-24 05:37:59 -07:00
Vincent Koc 41050b2f3b fix(ci): isolate release QA lane locks (#128458)
Extract the QA concurrency portion of #127140 while leaving the iOS runner authorization changes out.
2026-08-24 05:03:10 -07:00
Peter Steinberger 295809df98 feat(geolocation): resolve client addresses to a coarse city via a bundled plugin (#128546)
* feat(geolocation): resolve client addresses to a coarse city via a bundled plugin

The Activity identity card could show a client's IP address but not where it
was, so an operator still had to look the address up by hand.

Add a bundled `geolocation` plugin that owns address-to-place resolution behind
one authenticated route, `GET /plugins/geolocation/lookup?ip=`. It downloads a
MaxMind-format database on first lookup into the state directory, answers from
that local copy, and refreshes it monthly, so a lookup never sends an address
to a third party. The Control UI renders the resolved city on the device row
next to the address and the client-reported time zone.

The default source is DB-IP City Lite under CC BY 4.0. That license requires
attribution, so every response carries the credit and the UI renders it next to
the value; the database is downloaded at runtime and never redistributed.
Plugin code and the `maxmind` reader are MIT. No free city-level IP database is
MIT-licensed, so the obligation lives with the data rather than the code, and
`databaseUrl` plus the attribution fields make the source swappable.

No new core provider kind: with one implementation the plugin owns everything
through the existing HTTP-route seam, keeping core plugin-agnostic. A second
provider is what would justify promoting this to a registry contract.

Availability and lookup failure stay distinguishable: a missing or still
downloading database answers 503, never `found: false`. A failed refresh serves
the cached copy, and a body that does not parse as an MMDB is discarded without
replacing a working database.

* fix(docs): correct geolocation config examples and add zh-CN glossary entries

The config examples used `plugins.<id>` instead of the real
`plugins.entries.<id>.config` shape, which the docs config-example
validator and src/config/docs-config-examples.test.ts both reject.
New doc labels also need zh-CN glossary entries.

* chore(labeler): cover the geolocation extension directory

AGENTS.md requires a labeler entry plus a GitHub label for every new
plugin surface; test/scripts/labeler-extension-coverage.test.ts enforces
the labeler half.

* fix(geolocation): address review findings on caching, download bounds, and scope

Cold-start lookups were permanently suppressed. The loader cached one promise
per address including failures, so the 15s browser deadline expiring against a
first download that takes ~46s cached a blank forever, and a mounted row only
looks up again when its IP changes. Lookups now return a discriminated
located/absent/unavailable result: only definitive answers are cached, and the
element retries an unavailable one on a widening 5s/15s/45s schedule.

Download limits ran after allocation. The size check happened only after
`response.arrayBuffer()` had buffered the whole body, and gunzip had no output
ceiling, so a replaced source or a compression bomb could exhaust Gateway memory
before rejection. The body now streams against a compressed ceiling enforced
per chunk, and inflation uses zlib's maxOutputLength.

Cached placements were not scoped to the Gateway. The cache keyed only by
address while endpoint and credentials come from the shared Gateway context, so
a switch could render the previous Gateway's answer. The shared reset hook now
supports multiple subscribers - a single slot silently dropped whichever
registered first - and the geolocation cache subscribes.

Unresolvable ranges no longer trigger a download. Only loopback suppresses `ip`
at connect, so Tailscale carrier-grade-NAT and LAN addresses are recorded and
displayed. No geolocation database contains them, so a tailnet-only or LAN-only
Gateway was downloading 125 MB to answer nothing. The route now answers those
ranges without loading the database, using the already-public
`isPrivateOrLoopbackHost` seam so the SDK surface budget is unchanged.

The quickstart queried a reserved documentation range while showing a located
response, which cannot happen; it now uses a routable address and documents the
not-found case.

* fix(deps): resync the lockfile after dropping the net-policy dependency

The geolocation plugin briefly depended on @openclaw/net-policy before
switching to the already-public isPrivateOrLoopbackHost SDK seam. The
package.json entry was removed without regenerating the lockfile, so the
frozen-lockfile install failed and every downstream CI job failed with it.
2026-08-24 04:56:03 -07:00
Vincent Koc cf4f112425 perf(ci): split Telegram prerelease tests (#128459)
* perf(ci): split Telegram prerelease tests

* fix(ci): preserve Telegram test process recycling

* fix(ci): shard runnable Telegram tests

* fix(ci): cap Telegram prerelease jobs

* fix(ci): correct Telegram shard heredoc

* test(ci): type Telegram prerelease matrix

* docs(ci): explain Telegram prerelease shards
2026-08-24 03:02:46 -07:00
Peter Steinberger 234df15a6d chore: refresh dependencies after seven-day cooldown (#128414)
* build(deps): refresh dependencies after cooldown

Apply dependency, toolchain, action, image, and exact tool updates released by the inclusive 2026-08-16 seven-day cutoff. Adapt owner boundaries for the resulting CUA, logging, Teams, Markdown, native, and test-harness contract changes while retaining versions blocked by upstream compatibility constraints.

* fix(ui): align markdown renderer env typing

* fix(deps): align postcss and mistral peer contracts

* fix(deps): repair refreshed dependency contracts

* fix(deps): retain tslog startup budget

* fix(ci): verify Android tools with SHA-256

* fix(ci): fence Android SDK cache version
2026-08-24 03:01:54 -07:00
Peter Steinberger 4c48c13ab6 fix: surface cloud worker enrollment rejection evidence (#128632) 2026-08-24 02:32:24 -07:00
Peter Steinberger 5e253f4f42 fix(cli): render hooks report JSON failures (#128641) 2026-08-24 02:21:41 -07:00
Peter Steinberger 12d0fd2ef8 refactor(anthropic): replace handwritten Claude sessions with Agent SDK (#128131)
* refactor(anthropic): explore official Claude Agent SDK runtime

* refactor(anthropic): replace handwritten Claude sessions with SDK

* refactor(anthropic): collapse SDK live-session ownership

* refactor(anthropic): simplify SDK ownership and preserve live skills

* fix(anthropic): fence cancelled SDK runs before process startup

* fix(anthropic): harden SDK approvals, lifecycle, and packaging

* refactor(anthropic): own SDK process trees and streamline runtime

* fix(anthropic): repair rebased packaging and legacy test fixtures
2026-08-24 01:59:16 -07:00
Peter Steinberger 554fb212c9 fix(nodes): report camera positions the hardware actually reached (#128595)
* fix(nodes): report camera positions the hardware actually reached

`camera.ptz.control` returned a position it never verified, and
`camera.snap`/`camera.clip` could capture from a camera the caller did
not ask for. Both told the agent an action succeeded when it had not.

PTZ read its post-write status from the same UVC connection that issued
the write. Gimbal cameras echo a pending setpoint back on the writing
connection, so the check confirmed its own write. Those cameras also
service camera-terminal controls only while a video stream is active, and
no capture session was held, so writes could be discarded entirely while
reads returned phantom values.

Hold a frame-discarding capture session across every PTZ operation, close
the writing controller, and verify through a fresh connection against each
axis's advertised resolution. An axis that misses now reports through the
existing CAMERA_PTZ_PARTIAL outcome with observed versus requested values
and what to check next.

Apple camera selection accepted an explicit deviceId and silently fell
back to the default camera when nothing matched. Linux already rejected
this, and CameraPTZService already rejected it in the same app. Centralize
exact selection in OpenClawKit so macOS and iOS both fail with a
device-not-found error; the facing/default fallback stays only for
requests that supply no deviceId.

camera.ptz.status now activates the camera and its privacy indicator for
the duration of the read. That is the cost of returning real positions.

* fix(nodes): tell callers how to recover from an unknown camera ID

Device IDs change when cameras are reconnected, so a bare
device-not-found error dead-ends the caller. Both Apple errors and the
docs now point at camera.list for current IDs.

Addresses the ClawSweeper P2 finding on #128595.
2026-08-24 01:56:12 -07:00
Peter Steinberger 53dcaaedec feat(gateway): generic operator roles for non-maintainer access (#128548)
* feat(gateway): generic operator roles for non-maintainer access

Adds gateway.roles: named role bundles over a closed capability set —
sessions.others (none/view/suggest/write), an agents allowlist, and an
operator-scope ceiling. Roles are person-level (additive user_profiles.role
column, SQLite stays at v9); users.setRole (admin-only) assigns them. With
no gateway.roles config, behavior is unchanged for solo deployments.

Enforcement is deny-by-default from a host-minted actor identity
(system vs operator+profileId on server-only client.internal, never
accepted from the wire) and covers every entry point: WS RPCs, OpenAI-compat
and Responses HTTP, tools invocation, cron, questions, usage, task
suggestions, session catalog/sharing/reads. The agents allowlist gates both
session creation and run-start on existing sessions. Subagent completion
announce and descendant wake mint explicit system authority so role
boundaries never silently drop parent notifications.

The enforcement surface is expressed through a narrow policy vocabulary
(operatorSessionCap, hasOperatorBoundary, authorizeSessionSharing) rather
than per-handler policy internals.

* fix(gateway): heal PR CI after rebase onto main

- Break import cycles: extract GatewayOperatorRoleActor leaf contract; merge
  session-group-mutation-targets into session-sharing-target-input.
- Split sessions-suggestions.test.ts (max-lines) into a visibility suite.
- Add users.setRole to the 2026.8 train registry test and regenerate the
  Kotlin protocol client.
- Startup UNAVAILABLE gating now precedes session authorization: session
  stores are not loaded during startup, so authorization reads would deny
  with a misleading non-retryable error.
- sessions.assignOwner keeps its documented visibility-authorized contract
  when no operator role caps the caller; view/suggest-capped roles still
  cannot reassign foreign session ownership.
- Test stubs updated for main's socket readyState guard (#128144) and the
  system-authority arg on channel-native resets.

* test(gateway): chat.send pending-profile dispatch carries its required session target

chat.send requires a non-empty sessionKey at the protocol level; the mutation
pipeline now rejects targetless frames before profile-dependent dispatch, so
the pending-profile test must send a realistic frame.
2026-08-24 01:20:03 -07:00
Peter Steinberger 4ac8cd3dad fix(telemetry): report only publicly known plugin identities (#128603) 2026-08-24 01:06:39 -07:00
Peter Steinberger e4b36f2e97 feat(ui): open sessions in real browser tabs and keep sidebar visibility per-tab (#128474)
* feat(ui): make sidebar visibility per-tab and free Cmd-click for new tabs

Sidebar visibility was persisted in localStorage, which is shared origin-wide,
so collapsing it in one tab leaked into every other tab and window on reload.
Visibility is view state, not a preference: stop persisting it and keep it in
the tab-local navigation snapshot. Width stays persisted.

A tab whose first load targets one specific conversation now starts collapsed,
seeded once at bootstrap from the canonical session-URL contract so bare /chat
and dashboards are unaffected and SPA navigation never re-collapses. Cmd-B,
the toggle, hover-peek, and the remote UiCommand path all still work.

Cmd/Ctrl-click on a sidebar session row now falls through to the browser so
sessions open in a real new tab or window; multi-select moves to Alt/Option-
click, which previously fell through to the browser's link-download behavior.
Shift-click range selection is unchanged.

* test(ui): prove per-tab sidebar visibility at the browser boundary

Covers the two states that matter: a bare /chat first load keeps the sidebar,
and a tab opened directly on one conversation renders that conversation
full-width with the sidebar collapsed and Cmd-B restoring it. Waits on the
composer, not just the missing sidebar, so the capture cannot pass against a
blank pane.

* fix(ui): narrow new-tab sidebar collapse intent

* fix(ui): collapse sidebar for catalog sessions opened in new tabs

* test(ui): split bootstrap navigation visibility coverage
2026-08-24 00:49:39 -07:00
Peter Steinberger 3fa3276381 feat(nodes): opt-in container isolation for node-hosted worker sessions (#128447)
* feat(nodes): opt-in container isolation for node-hosted worker sessions

nodeHost.workerRuns.isolation=container runs each worker session inside a
Docker-compatible container (docker/OrbStack/podman): stdio launch transport,
exactly two bind mounts (bundle read-only, workspace read-write), allowlisted
env, and the container itself as the durable launch identity — created stopped
and journaled before the descriptor is delivered, killed/removed on cancel,
fence, shutdown, and recovery, with an owned-orphan sweep at startup. Missing
or changed engines disable hosting with an explicit diagnostic instead of
falling back to bare processes. Additive nullable worker_container_json column
on node_worker_launches (schema version unchanged).

* fix(test): base-shape fixture skips additive columns of stripped tables

node_worker_launches is excluded from claw-scoped state schemas, so the
read-only compat fixture cannot drop its additive column there; drop only
columns whose owning table exists.

* fix(test): guard split destructuring in base-shape fixture

* test(state): record worker_container_json in the canonical additive-column list

* fix(nodes): harden container isolation per review findings

- Revalidate the engine daemon target immediately before container creation;
  a replaced daemon receives zero create/start requests (regression proves it).
- A pending launch keeps its worker slot until the journaled container is
  killed/removed; cancellation cannot free capacity over a live container.
- Windows node hosts fail closed at startup for isolation=container with an
  actionable diagnostic (native paths cannot be container mount targets).
- Container identity moves from an additive column to the same-version
  companion table node_worker_launch_containers (bare STRICT, lazily ensured
  on first container write, pruned with the launch journal): the launch table
  is contract-optional and shipped readers reject additive columns there. An
  exact v9 predecessor now provably opens and uses the journal after the
  candidate populated container rows.
2026-08-24 00:19:45 -07:00
Ayaan Gazali 834fc445cc docs(channels): scope implicitMentions overrides to the channels that read them (#119320)
* docs(channels): scope implicitMentions overrides to the channels that read them

Only Mattermost, Slack, and Tlon call resolveChannelImplicitMentions, and they
are the only channels whose schemas accept the key. The other documented
producers pass no policy, so allowedImplicitMentionKinds stays undefined and
every produced fact counts as a mention.

* docs(channels): correct implicitMentions override wording

QQBot is a listed reply-to-bot producer with a passthrough schema, so the key
is accepted and ignored there rather than rejected. Also drops 'yet', which
implied a planned rollout while #80234 is still an open product decision.

* docs: limit implicit mention claims to bundled channels

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-24 00:13:35 -07:00
synth 17abdfc78c fix(system-agent): leave reasoning room in local model setup probes (#124405)
* fix(system-agent): increase setup inference probe max tokens from 32 to 128

The 32-token cap starves reasoning models that spend tokens on thinking
before emitting visible text. The probe gets a reasoning-only turn and
reports healthy local models as broken inference.

Increase to 128 tokens to accommodate reasoning model thinking overhead.

Fixes #124345

* fix(system-agent): leave reasoning room in setup probes

Co-authored-by: synthclaw <synthalorian@gmail.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-23 23:46:27 -07:00
Peter Steinberger 2e606d96e1 docs(onboarding): preserve the selected model in setup guide (#128547) 2026-08-23 23:46:23 -07:00
Vincent Koc 1696ff97ef fix(ci): settle Claude cache probe before generation baseline (#128507)
* test(gateway): settle Claude cache probe before baseline

* docs(test): describe Claude cache settlement probe
2026-08-23 23:18:58 -07:00
Vincent Koc 6ce272c2a6 docs(install): use managed gateway restart in Ansible guide (#128522)
Punchcard-Session: clear-meadow-workshop-qv
2026-08-23 23:00:28 -07:00
Peter Steinberger b136df2307 feat(channels): matrix custom emote discovery, discord guild-emoji cache (#128518)
Matrix gains emoji-list backed by MSC2545 image packs: room state
im.ponies.room_emotes (any state key) plus the personal
im.ponies.user_emotes account-data pack, sticker-only entries excluded,
identifiers are literal shortcodes (Matrix reactions send the annotation
key as-is; the mxc URL is exposed alongside). Discord emoji-list results
now ride the lifecycle-owned bounded entity cache, invalidated by
GuildEmojisUpdate; the non-privileged GuildExpressions intent is added so
that dispatch actually arrives. Google Chat custom-emoji listing is
documented as unavailable: customEmojis.list requires user auth while the
plugin is a chat.bot service-account channel.
2026-08-23 23:00:22 -07:00
Ayaan Zaidi ca6fea301b refactor(plugins)!: remove OpenProse (#128494)
Remove the bundled OpenProse plugin and /prose command now that upstream owns the maintained Agent Skill. Preserve /prose as migration documentation and let Doctor clean stale plugin configuration.

BREAKING CHANGE: The bundled OpenProse plugin and /prose command are removed.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-08-24 10:39:46 +05:30
ClawSweeper 447ffb0719 fix(exec): align Guardian approvals for bound compound plans (#128279)
* fix(exec): review bound compound gateway plans

* test(exec): allow optional plan revalidation

* fix(exec): publish Guardian review lifecycle

* fix(exec): bound compound Guardian reviews

* fix(exec): bind Guardian reviews to execution lifecycle

* fix(ci): tolerate bounded startup RSS variance

---------

Co-authored-by: RoboClaw <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-23 22:07:14 -07:00
Vincent Koc 52533dc89e docs(nodes): clarify Mac node system commands (#128497)
Punchcard-Session: silver-summit-cedar-9k
2026-08-23 22:04:45 -07:00
Bek 07a5945e55 Revert "fix(codex): preserve child hooks across turns (#128485)" (#128493)
This reverts commit a5c8e00cb6.
2026-08-24 00:55:28 -04:00
Peter Steinberger 6178409d9a fix(dashboard): preserve widget content ownership (#128489) 2026-08-23 21:53:39 -07:00
Peter Steinberger 6417bcb7b1 docs(telemetry): lead with what is on by default and thank opt-in users (#128491) 2026-08-23 21:39:10 -07:00
Bek a5c8e00cb6 fix(codex): preserve child hooks across turns (#128485)
* fix(codex): preserve native hook relays across turns

* fix(codex): fail closed on relay renewal loss
2026-08-24 00:31:42 -04:00
Ayaan Gazali 4f8f050f5b docs(whatsapp): document the canonical ack reaction keys (#118161)
* docs(channels): record the WhatsApp ack-reaction exceptions

The shared reference promises channel and account ackReaction overrides plus an
identity fallback for every channel, but only Discord, Matrix, Slack, and
Telegram accept those keys, and WhatsApp sends no acknowledgment at all when
messages.ackReaction is unset. Also notes that group activation always bypasses
the group-mentions mention check.

* docs(whatsapp): qualify the ack-reaction exception

The eligibility path reads the account-aware channels.whatsapp.reactionLevel
before the message settings, so "only" applies to the emoji and scope rather
than to the whole decision.

* docs(whatsapp): describe supported ack reaction overrides precisely

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-08-23 21:31:00 -07:00
Ayaan Zaidi 055a7302c4 docs(hooks): clarify command logger storage (#128478)
Correct the command-logger documentation to match runtime behavior: emitted command events are written only to logs/commands.log. Keep schema v9 and its compatibility surface unchanged.
2026-08-24 09:50:28 +05:30
Jesse Merhi 0e8faacd71 fix(scripts): build heap ignores its systemd memory budget and takes the full default (#123979)
* fix(scripts): size the tsdown heap from the build's own cgroup budget

The build heap probe only read the cgroup root (/sys/fs/cgroup/memory.max and
the v1 equivalent). Those files exist only when the process runs in a
namespaced container cgroup; under systemd the budget lives on the process's
own slice, and the v2 root carries no limit at all. So every systemd-managed
build found no limit, fell back to /proc/meminfo MemTotal, and took the full
12288 MB default heap regardless of its actual budget.

Observed on a 15.4 GiB host: openclaw-main-update.service ran tsdown with
NODE_OPTIONS=--max-old-space-size=12288 while its user@999.service slice was
bounded at 5 GiB, reaching 3.2 GB RSS and 6.25 GB peak before the host began
OOM-killing unrelated services.

Resolve the limit from /proc/self/cgroup and walk that chain instead, reading
memory.high alongside memory.max (memory.high throttles reclaim rather than
failing allocation, so a heap above it stalls the build instead of OOM-ing),
and take the tightest bound found. Root paths stay as the container fallback,
and an explicitly injected path list still disables detection.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): resolve the build heap budget from the v1 memory controller too

The slice walk only accepted the unified 0:: record, so a legacy or hybrid
systemd host fell back to the root probe and kept taking host memory. One
resolver now walks both hierarchies leaf-to-root, which makes the static root
list its own depth-0 case and removes it.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): read cgroup controller mounts instead of assuming their paths

v1 controllers can be co-mounted at the cgroup root, where memory.limit_in_bytes
sits under the slice with no per-controller directory, so the hardcoded
/sys/fs/cgroup/memory probe missed the budget and the build took the full
12288MB default. Mount points now come from mountinfo.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): translate cgroup records through the mount root

mountinfo field 4 is the subtree a cgroupfs mount exposes. Under a container
mount the /proc/self/cgroup record stays host-absolute, so walking it verbatim
probed paths below the visible mount and the build fell back to host memory.
Records now translate through the mount root before the walk.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): skip cgroup mounts that cannot represent this process

Falling back to the mount root for a record outside the mount's subtree sized
the build from an unrelated cgroup: an inherited namespace clamped the heap to
the 2048MB floor from a foreign 1GiB limit. Non-representable mounts are now
skipped, and the blind root probe only runs when no memory record exists.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): keep every cgroup mount view, not just the last one seen

One hierarchy can be visible through several mounts and only some expose a
subtree containing this process. Retaining only the last view dropped the
budget whenever a non-representable bind view came later, sending the build
back to host MemTotal.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): decode octal-escaped mountinfo paths before matching cgroups

ClawSweeper P2 on 7e64ad61f7: the cgroup resolver compared mountinfo's mount
root and mount point verbatim. The kernel escapes space, tab, newline, and
backslash in those two fields, so any cgroup mounted under such a path never
matched, the bounded slice was missed, and heap sizing silently fell back to
host memory.

Decode both fields before matching. The decoder lives in scripts/lib beside the
other shared script helpers rather than inline, so the scripts program has one
copy rather than a new ad hoc one.

Regression test fails pre-fix: a v2 mount at "/sys/fs/cgroup\040dir" with a
5 GiB memory.high yields --max-old-space-size=12288 (host fallback) before the
fix and 4352 after.

Follow-up, deliberately not bundled here: src/infra/sqlite-wal.ts,
src/commands/doctor-state-integrity.ts, and src/plugins/bundled-source-overlays.ts
each carry their own private copy of this same decoder. Consolidating all four
into @openclaw/normalization-core is the right end state, but it touches a
shared package plus three core modules and belongs in its own reviewable change.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): resolve cgroup-namespace-relative records to their mount

ClawSweeper P1 on d6fe49dd3f: inside a cgroup namespace /proc/self/cgroup
reports the namespace root ("0::/") while mountinfo field 4 stays the host
subtree the cgroupfs was mounted from ("/docker/<id>"). relativeCgroupPath then
found no prefix match and returned null; because a memory record had already
been seen, the root probe was skipped and the build fell back to host MemTotal.
A constrained container therefore missed its own budget entirely.

That namespace root is exactly what the mount exposes at its mount point, so it
resolves to "/" rather than failing closed.

Regression test fails pre-fix: a "0::/" record against a /docker/2f1a9c mount
root with a 5 GiB memory.max yields --max-old-space-size=12288 before the fix
and 4352 after.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): reject inherited cgroup mount views instead of guessing

ClawSweeper P1 on b4d200c5d2: the previous commit resolved a namespace-relative
record against any mount root, including the inherited views cgroup_namespaces(7)
documents, whose field-4 root reads "/..". Which cgroup such a view exposes is not
derivable from mountinfo, so probing it can size the build from an unrelated
cgroup's limit.

Reject non-canonical mount roots outright. An undecidable view now falls back to
host sizing, which is current main's behavior, rather than silently adopting the
wrong budget.

Regression test covers the "/.." inherited mount: it must yield host MemTotal
sizing, not the 5 GiB limit sitting behind that mount.

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(scripts): fail closed on namespace-root records against non-root mounts

ClawSweeper P1 on 731d3bbc8e: a "0::/" record does not prove that a mount
rooted at some other subtree exposes this process's cgroup. Resolving that pair
could cap the build heap from an unrelated cgroup's limit.

Return no mapping for it. An undecidable pair now falls back to host sizing,
which is current main's behavior, so the failure mode is a missed optimisation
rather than a wrong budget. The "/.." inherited-mount rejection stays; this
covers the broader ambiguous mapping it did not.

The namespace-relative test is repointed accordingly: an unrelated mounted
subtree must yield host sizing, not that subtree's limit.

Net production change: none (4 lines swapped).

Co-authored-by: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com>

* fix(build): cap tsdown heap to the real budget and refuse hosts that cannot build

The 2048MB floor was applied on top of a discovered cgroup limit, so a small
container was handed a heap larger than it could honour. Measured in real
cgroups, that does not OOM-kill, it thrashes: a 1500MiB container sat pinned at
its ceiling for 10 minutes with oom_kill at 0, never finished the second of
eleven invocations, and starved every other process on the host.

Cap to the discovered budget, then refuse up front when that budget cannot hold
the build. The threshold is the whole-build peak, not a single pass: a full
eleven-invocation build peaks at 4730MiB, so a 5GiB slice completes while 4GiB
and 2816MiB slices are both killed partway through the third invocation.

The refusal runs before any output is cleaned, so a host that cannot rebuild
does not also lose the build it has.

* fix(build): harden tsdown heap admission

* fix(build): guard the default tsdown plan

* fix(build): preserve runtime-only Docker builds

* fix(build): admit only declaration cache misses

* fix(build): scope heap admission to real budgets

* fix(build): guard direct unified declarations

* fix(build): guard the canonical tsdown config

* fix(build): satisfy cache planning lint

* fix(gateway): release empty orphan leases

* fix(build): cap cgroup budget by host memory

* fix(build): serialize the canonical tsdown config

* test(build): freeze host memory fixtures

* fix(build): honor cgroup v1 soft limits

* fix(build): respect cgroup v1 hierarchy mode

* fix(build): admit unified runtime plans

* fix(build): admit every unified runtime path

* fix(build): collect repeated tsdown filters

* fix(build): ignore cgroup v1 soft limits

* fix(build): use explicit heap override as opt-in

* refactor(build): simplify memory admission

* fix(build): harden constrained build recovery

* fix(ci): prebuild runtime before real CLI shards

* fix(build): honor runtime-only runner environment

* fix(ci): satisfy tooling shard lint
2026-08-24 14:18:48 +10:00
Peter Steinberger 4d617f44f3 feat(telemetry): opt-in anonymous usage stats on a daily update ping (#128476)
* feat(telemetry): add opt-in anonymous usage reporting

* docs(telemetry): document anonymous update and privacy controls

* feat(telemetry): report show state as JSON

Give the reporting command a machine-readable form and classify the
group and mutations against the CLI JSON-output policy.
2026-08-23 21:18:04 -07:00
Peter Steinberger 0978d55b05 feat(nodes): automatic device placement for sessions.dispatch (#128421)
* feat(nodes): automatic device placement for sessions.dispatch

sessions.dispatch gains autoDevice: true — the gateway selects the eligible
session-host node with the most available worker slots (deterministic
tie-break), retries up to three candidates when a node churns at the
pre-provisioning eligibility fence, and reports the chosen device in the
placement runner projection. Control UI offers Any available node with
actionable disabled reasons. No-eligible-host failures state why.

* fix(ui): break draft-place-state/draft-session-placement import cycle

resolveDraftSessionPlacement only needs four scalar fields; a structural
param type replaces the Pick<DraftPlaceState,...> import that created the
madge cycle.

* fix(ui): keep the devices section hidden when no devices are paired

The Any available node row lives inside the Your devices section; rendering
it with zero paired devices resurrected the section on gateway-only setups.
Gate it on device presence — Connect a machine remains the discoverability
path — and cover both the empty and non-hostable cases.

* fix(gateway): project dispatch runner state through the canonical reader

The dispatch reply no longer synthesizes an available device runner; it uses
the fenced workerPlacementRunnerAvailabilityReader (and disk-space reader)
exactly like session reads, so a node lost after durable provisioning
projects offline consistently. Documents placement.runner.deviceId in the
protocol reference.
2026-08-23 21:08:33 -07:00
Peter Steinberger 2aa5eee34e feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram (#128435)
* feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram

Make custom emojis discoverable by the agent. The message tool's emoji
param now documents custom-emoji syntax per channel (gate-aware, only
naming emoji-list when the action is actually advertised). Discord
emoji-list defaults guildId from the current conversation and returns
reaction-ready { name, identifier, animated? } entries; Slack returns
normalized shortcodes with aliasOf. Telegram gains emoji-list backed by
one canonical allowed-reactions owner (getChat available_reactions,
custom_emoji entries preserved), numeric custom-emoji reactions, and
replaces the dead 'reaction disallow list' error advice with a bounded
sample of the chat's allowed reactions.

* test(channels): expect telegram emoji-list provider-owned read gate in plugin shape contract

* test(telegram): prove emoji-list authority chain via mock-gateway e2e

Ephemeral gateway + mock Bot API + mock OpenAI provider: current-chat
emoji-list returns normalized standard and custom_emoji identifiers with
exactly one getChat call; a delegated cross-chat request is rejected with
the conversation-binding error and zero Bot API requests reference the
foreign chat.
2026-08-23 20:39:06 -07:00
Peter Steinberger 8083d4dd3f fix(memory): enforce canonical SecretRef resolution (#127699)
* fix(memory): enforce canonical SecretRef resolution

Make Gateway runtime snapshots the exclusive owner of memory SecretRef materialization. Bind embedding credentials and headers to provider-owned destinations, and fence per-agent stale reuse by the provider destination/auth contract.

Release note: Memory search resolves secret references through configured provider policy and keeps embedding credentials scoped to their intended destination.

* fix(lmstudio): preserve resolved memory headers

memory remote headers are already materialized by the Gateway snapshot and now bypass SecretRef re-resolution; provider-owned headers retain canonical resolution; final loopback request proof covers literal preservation and precedence.

* fix(memory): bind stale credentials to auth owners

Resolve memory adapter credential owners from snapshot manifest metadata, conservatively fail cold when metadata is absent, and prove Gemini/Google destination changes plus zero-egress unresolved refs.

* fix(memory): scope compatible embedding credentials

Apply destination ownership to the core compatible adapter while preserving destination-owned credentials and intentionally unauthenticated endpoints. Distinguish loopback principals, consolidate duplicate security tests, and verify the final credential boundary through a live isolated Gateway request.

* test(memory): align destination auth precedence

* fix(memory): bind credentials to query identity

Include URL query parameters in embedding destination ownership so provider credentials and headers never cross tenant boundaries.

* fix(memory): preserve query-bound embedding destinations
2026-08-23 20:36:55 -07:00
Peter Steinberger 8e6c34fba3 feat(ui): user-selectable accent color for the Control UI (#128432)
* feat(ui): user-selectable accent color for the Control UI

Adds an Accent color section to Settings -> Appearance: curated preset
swatches plus a native custom color input. The choice persists as the
gateway-synced ui.prefs.accent (#rrggbb) and takes precedence over the
operator-level ui.seamColor, falling back to the active theme's accent
when cleared. The shared apply path now also derives readable
--accent-foreground/--primary-foreground ink from accent luminance, so
light accents keep dark text on primary controls.

Bundled accent-invariant cleanup: the embedded terminal resolves its
background/cursor/foreground from live computed tokens instead of a
hand-copied claw palette (fixes desync on knot/dash/custom themes),
a baked rgba(255,92,92) gradient now uses color-mix over --accent, and
duplicated diff-tint literals collapse into --danger-subtle/--ok-subtle.
Environment branding moved behind a lazy runtime module (-2.5KB gzip
startup JS).

* fix(ui): derive status glows and tints from semantic tokens

Status-dot glows, callout gradients, compaction-indicator borders, chip
borders, and dreams pulse animations baked literal green/blue/amber/red
rgba values -- several stale copies of old --danger/--info hexes -- so
they ignored theme families and light mode's deeper status hues. All 22
sites now color-mix over --ok/--warn/--warn-strong/--danger/--info.

* fix(ui): scope default accent swatch chip vars to a swatch class

Reusing .settings-theme-card--<theme> on the accent swatch made theme-card
Playwright locators resolve to two elements (strict-mode violation in the
prefs-reconnect e2e suite). The default swatch now uses
.settings-accent-theme--<theme>, added alongside the theme-card selectors
in the theme-invariant chip var blocks.

* fix(ui): carry --primary-hover through the accent override

The accent apply path set --primary but not --primary-hover, so dark
Claw/Knot primary buttons reverted to the theme hover color while an
accent was active (ClawSweeper P2). Derive it from --primary like the
accent hover, clear it on reset, and prove the resolved hover color via
a painted probe in the appearance e2e.
2026-08-23 20:32:05 -07:00
Peter Steinberger 9284e23cdd feat(control-ui): show client IP and time zone on the activity identity card (#128438)
* feat(control-ui): show client IP and time zone on the activity identity card

The Activity identity card showed only host and platform, so an operator
looking at a teammate could not tell where that person was connecting from.
Presence already carried a best-effort `ip`, and it was simply not rendered.

Add the client's self-reported IANA time zone to the connect handshake and
presence entry, and render both `ip` and `timeZone` on the device row. The
time zone matters because the connecting address is frequently unusable for
location: connect handling omits `ip` for loopback clients, and tunneled or
Tailscale clients land in private/CGNAT ranges. A browser knows its own zone
regardless of how it reached the gateway.

Both protocol additions are optional fields, so no version bump is needed.

* build(protocol): regenerate Swift models for presence timeZone
2026-08-23 20:07:06 -07:00
Vincent Koc b6798cbb10 docs(slack): use canonical postAs key (#128448) 2026-08-23 18:37:49 -07:00