The remote-mode seed obligation lived only in process memory
(remoteSeedPending), so a gateway restart between sandbox create and the
first exec adopted the sandbox via 'sandbox get' and silently skipped the
one-time seed, leaving an empty remote-canonical workspace with no visible
failure.
Adopted remote sandboxes now probe the managed roots once per process at
the ensure boundary and arm the seed only when every root is missing or
empty, so recovery can never wipe operator content in an already-seeded
workspace (the seed guard shipped in v2026.7.1, so legacy seeded sandboxes
without any marker exist). The probe also heals a create that timed out
client-side after the sandbox actually came up.
Regression test simulates create -> restart -> first exec and fails
pre-fix; a sibling test locks the never-reseed-non-empty-roots invariant.
* fix(gateway): tools.invoke must carry the caller's host-minted role authority
The connect handshake resolves each connection's authority once and stores it
server-side (shared-secret operator owners mint system authority there).
tools.invoke discarded that fact and re-derived ownership from scopes, so a
shared-secret caller with no durable profile resolved to the deny-by-default
role and was refused dispatch on its own agents — while the same connection
could still mutate sessions directly.
Carry client.internal.operatorRoleActor into the synthetic dispatch client and
keep the scope-derived fallback for callers that have no connection actor
(HTTP). Regression test fails pre-fix with the FORBIDDEN agent-allowlist error.
* test(opencode): close the fake CLI before exec to stop ETXTBSY flakes
The catalog suite wrote the fake opencode executable and spawned it
immediately. Under parallel CI shards the write handle could still be open
at exec time, so the launch failed with ETXTBSY and failed the shard.
Write through an explicit file handle with an fsync before close so the
binary is fully durable before the first spawn.
* fix(ci): repair red main type and lint gates
Two gates were failing on main independently of this branch:
- extensions/qa-lab cleanup tests still built OpenClawCrablineChannelDriverSelection
with the retired smokeArtifactPath and a stale capabilityMatrixPath, so
check:test-types failed after the readiness-artifact change (#124189).
Align both fixtures with the current type and its pinned constants.
- scripts/github/release-validation-campaign.d.mts declared the Actions Octokit
client as any (#129726), tripping no-explicit-any. Declare the structural
subset the publisher actually calls instead of suppressing the rule.
Verified failing on clean origin/main before the fix.
* fix(buzz): messages sent during Gateway downtime are dropped after restart
Every new Gateway process subscribed to Buzz rooms starting at its own
start time, because the lookback flag that enables backlog paging is
process-local. That cutoff is applied as the NIP-01 since filter, so any
room message a human sent while the process was down was never returned
by the relay and never reached the agent or the transcript.
The account gateway now persists a per-account recovery watermark in the
plugin state store. First-ever setup still starts from the current time;
later process starts resume from the saved watermark, clamped to the
existing 24 hour retention floor. The watermark advances only after
inbound handling completes and is stored at the admitted message
created_at, so the inclusive since boundary re-offers the last admitted
message to the existing persistent event-id dedupe guard.
* fix(buzz): keep the cold-start recovery cursor behind unfinished work
Codex review found two ways the recovery watermark could still drop room
messages. Backlog replay dispatches up to eight handlers concurrently, so
a newer message finishing before an older one committed the newer
timestamp; a crash then left the older message with no dedupe record and
a cursor already past it. The stored timestamp was also the sender
controlled created_at, so a future dated room event pushed the cursor
past locally observed time and excluded real downtime messages on the
next start.
The account gateway now tracks a recovery frontier per session. A
checkpoint is the highest completed message time bounded by the oldest
still running message, the oldest failed message, and the receipt time
observed when the message was admitted. Checkpointing stays closed until
the bus reports the room backlog fully drained, so a session that is
still paging history cannot commit past events it has not seen yet.
catchUpHistory now reports drained or incomplete for that signal.
* fix(buzz): scope cold-start recovery per room and fence it at enqueue
Admit replay work into the recovery frontier when it enters the dispatch queue instead of when a worker starts it, so a crash cannot persist a cutoff past an event that is still queued.
Key recovery cursors by room instead of by account, so a room configured after the first start resolves to the current time and gets no backfill, while existing rooms resume from their own cursor.
Record an account-level start marker so the first start under a watermark-aware build recovers the existing retention window instead of repeating the reported loss on the first restart after an upgrade.
* fix(buzz): keep cold-start recovery to accounts that already ran
An account with no persisted cursor is a fresh install as often as it is a
pre-watermark upgrade, and stored state cannot tell them apart, so the first
start no longer replays the retention window. It records the current time as
each room cursor and recovers only from the second start onward.
Dropping the account start marker also removes the interrupted-bootstrap
window where a marker written before the room cursors made the next start
treat cursor-less rooms as initialized.
Recovery state capacity is now derived from the supported room limit instead
of a smaller fixed number, and any non-complete history paging outcome, not
just timestamp-over-limit, holds the recovery frontier undrained.
* fix(buzz): bound the recovery cursor store to each account
ClawSweeper found that the recovery watermark opened one fixed plugin-state
namespace for every account while sizing it at the per-account supported room
count. Plugin-state capacity is enforced per plugin and namespace, so once one
account registered its full room allowance, a second account's register call
threw and its rooms stayed at the current-time cutoff, which is the same
dropped-message outcome this PR set out to fix.
The store is now opened against a namespace derived from the account identity,
so each account carries its own supported room capacity. The account moves out
of the entry key into the namespace, which leaves one account-scoped store path
rather than a fallback, and makes cross-account cursor mixing unrepresentable.
* fix(buzz): simplify durable restart recovery
Co-authored-by: yetval <yetvald@gmail.com>
* test(buzz): use real relay response in recovery fixture
Co-authored-by: yetval <yetvald@gmail.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(state): retire six dead shared-state tables at schema v10
agent_model_catalogs, android_notification_recent_packages,
command_log_entries, diagnostic_stability_bundles, media_blobs, and
model_capability_cache landed with the database-first squash but their
runtime writers never reached main; every stable since v2026.6.10 created
them empty (agent_model_catalogs held only rebuildable catalog cache rows
until #111173 removed its writer). State schema 10 drops all six tables
and seven indexes through both the runtime-open and doctor migration
paths, records the retirements, bumps the native reader ceiling, and
corrects stale database-first doc claims that still named these tables
as canonical stores.
* test: move cross-lane schema-version pins to v10
The v10 retirement missed current-version pins outside src/state: the
native guard vitest wrapper, placement-move and node-worker-launch
same-version assertions, and the audit outbound-progress tripwire. The
pinned pre-C04 audit reader is a v9-era build that now refuses v10
databases by the version contract, so the test projects the file back to
the exact v9 shape with the documented 10-to-9 downgrade fixture before
the reader proof; the shared fixture also seeds the v10 retirement
regression.
* test: keep only the used downgrade fixture export
* fix(ui): stop warm sidebar refreshes from blanking foreign-owned rows
The owner-first roster plan (#128767) published its provisional owner-only
window as a full membership replacement on every primary refresh, so on
multi-owner gateways every other user's sessions blinked out of the sidebar
until the shared merge landed - up to once a second under event load. The
provisional phase now publishes only when no roster is on screen (cold start
keeps its fast first paint), and the shared phase merges the owner window
from the initial load's returned rows instead of reading published state;
the mergeExisting option is gone. A warm refresh whose shared phase fails
now keeps the previous roster instead of collapsing to owner-only rows.
The thread and child session lists also rendered rows positionally with
map()+keyed(), so any reorder tore down and rebuilt every shifted row's DOM
(spinners restarting, avatars remounting). Both now use repeat() with key
identity, matching the catalog renderer.
Regression coverage: owner-first-roster.test.ts pins the publish-sequence
invariant and shared-failure retention (both fail pre-fix); the new browser
test proves row DOM identity survives a reorder (fails on map()). The
owner-first tests moved out of index.event-refresh.test.ts, which hit the
max-lines limit.
* test(ui): prove foreign-owned rows survive a warm owner-first refresh
Browser-level regression for the warm-refresh half of this fix: holds the
shared phase deferred after a sessions.changed event and asserts the
foreign-owned row never leaves the DOM. Fails on pre-fix code (the row
count drops to zero the moment the provisional owner window publishes).
* test(anthropic): pin the local retired-profile id to the plugin-sdk constant
The provider-policy artifact keeps this id as a local literal so it never
imports the provider-auth barrel (#129052 regressed dist-less CI checkouts
into 120s jiti compiles of ~2.2k modules); this parity test stops the two
constants from drifting apart.
Codex 0.149 emits a session-configure warning whenever UnderDevelopment-stage
features are enabled, and OpenClaw always enables features.code_mode and
features.apply_patch_streaming_events for native code mode threads. The
warning relays into chat as a SYSTEM card telling operators to edit the
managed codex-home config.toml they should never touch.
Send suppress_unstable_features_warning: true in the same thread-config
block that enables those features. When native code mode is off, OpenClaw
does not send the flag, so warnings for user-enabled under-development
features still surface. The general warning relay is untouched.
Verified live against the pinned @openai/codex 0.149.1 app-server binary:
thread/start without the flag emits the exact warning; with it, none.
Reject malformed relay metadata and cancel oversized response streams at the existing 16 MiB provider boundary.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(anthropic): verify native Claude login before publishing runtime auth
* fix(status): preserve native Claude CLI authentication labels
* refactor(gateway): remove retired native Claude auth projection
* fix(plugin-sdk): retain Claude compatibility types
Keep the released Claude CLI credential reader source-compatible through its documented v2026.10 retirement window and explicitly account for both retained type exports in the public SDK budget.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* fix(plugin-sdk): preserve only shipped Claude compatibility exports
* fix(ci): align Claude native-auth contracts
Retire Doctor fixtures for credentials now owned by Claude CLI and remove the two never-shipped compatibility type exports while retaining the shipped reader through its documented v2026.10 window.
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* test(onboard): remove retired Claude credential file fixture
* fix(auth): skip incompatible profiles before plugin discovery
* fix(auth): avoid cold plugin activation for retired profiles
* test: repair upstream doctor and learning regressions
* fix(onboard): validate authored aliases without plugin discovery
* fix(agents): resolve compaction aliases without plugin discovery
* test(ui): register pairing sidebar before lifecycle teardown
* fix(anthropic): keep provider policy imports runtime-free
* test(vitest): cover codex startup retry test family
* fix(agents): avoid plugin discovery for internal sessions
---------
Co-authored-by: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>