* 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
* 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>
Lease complete mirror file operations through the workspace owner instead of locking only remote transport. Preserve successful writes across exec publication and share serialization across backend handles.
Proven with a real Docker red/green reproduction, 128-workflow mirror/remote stress matrix, exact inventories, failure recovery, 170 focused tests, and updated operator docs. Related: #127441; follow-up to #130031.
* refactor(state): fold singleton tables into config_machine_state at schema v11
Eight singleton tables (skill_curator_state, update_check_state,
clawhub_promotions_feed_state, model_catalog_remote, voicewake_triggers,
voicewake_routing_config, voicewake_routing_routes,
onboarding_recommendations) were each one logical JSON value behind a
fixed key; their bespoke schemas, lazy ensures, and per-table accessors
collapse onto the shared config_machine_state KV under namespaced keys.
cron_store_epochs retires outright: it was born write-only in #114388
and no reader ever existed in any language. Durable values (update
check state, voicewake triggers and routing, per-workspace onboarding
answers) migrate insert-if-absent during the v10->v11 migration; cache
class contents rebuild on next use. Deferred with named reasons:
exec_approvals_config (macOS direct-SQL contract), installed_plugin_index
(same-tx lease fence), node_host_config and web_push_vapid_keys
(secret-table git-backup redaction).
# Conflicts:
# src/skills/workshop/collection-review-state.ts
# src/skills/workshop/collection-review.gateway-admission.test.ts
* test: register v11 guard carve-outs and suppression pin
The v11 migration module joins the raw-SQLite allowlist (migrations are
the named guardrail exception), the lint-suppression allowlist records
the second type-parameter suppression in config-machine-state, and the
identity module keeps only externally consumed exports.
* test: surface CLI stderr when migration-diagnostic assertion fails
* test: expect migration diagnostics on stderr for models plain commands
The #129037 pending-migration cases asserted that aliases/fallbacks
lists never open the state database, but config-health observation
(observeConfigSnapshot -> readConfigHealthStateFromStore) full-opens it
on any config read whose file exists — reproduced identically on clean
main with a main-built dist. The protected contract is exact stdout;
the diagnostic legitimately lands on stderr for every case.
* test: drop unused defaults import from CLI stdout e2e
* test: split session path derivation out of oversized session-files suite
#130016 pushed session-files.test.ts to 1008 lines, over the 1000-line
lint cap and red for every PR's check-lint. The sessionPathForFile
describe moves to a self-contained sibling following the existing
session-files.*.test.ts split pattern; no assertions change.
* refactor(state): fold four more singleton tables into schema v12
tui_last_sessions (cache-class, regenerates on next session switch),
sidebar_sections (persistent section order, migrated as one JSON array),
node_host_config, and web_push_vapid_keys join the v12 fold-in, taking
the retirement to thirteen tables at the same version. The two secret
singletons were blocked on table-granular git-backup redaction; backups
now exclude config_machine_state rows by secret key prefix (nodeHost.*,
webPush.vapidKeys) with a fail-closed row filter and regression proof,
so STATE_SECRET_TABLE_NAMES sheds both tables. The sidebar fold also
retires its lazy-ensure WeakSet and inline DDL; sidebar edits stay
inside the existing session-group write transaction via direct Kysely.
* fix(node-host): omit absent Cloudflare Access config like the column reader
The KV rewrite returned gateway.cloudflareAccess as an own undefined
property where the retired column reader omitted the key; toStrictEqual
consumers (state-migrations doctor-repair test) caught the shape drift.
Mirror the column reader's conditional spread at both construction
sites.
* fix(backup): disclose redacted machine-state prefixes after restore
The prefix-granular secret redaction recorded omitted key prefixes in
the backup manifest but the restore result exposed only excludedTables,
so a redacted restore looked complete while nodeHost.* and
webPush.vapidKeys configuration were intentionally absent. The restore
result and CLI output now disclose the omitted prefixes (JSON mode
carries them via the result shape), with restore-side regression
coverage.
* fix(tui): compare-and-delete retired session pointers
Doctor cleanup read matching pointer keys then deleted them
unconditionally, so a replacement pointer written between the scan and
the delete was erased. The delete now re-checks the stored value inside
the write transaction and only removes pointers that still name a
retired session; a live replacement survives (regression covered).
Also corrects the stale schema-version line in database-first.md.
* docs(memory): add memory provenance and deletion concepts page
New concepts/memory-provenance page tells the operator policy story end to end: recorded session lineage through consolidation, the admission policy and its pipeline-only boundary, memory forget guarantees (whole-entry purge, derived-artifact sweep, forgotten tombstones) and reported boundaries (transcripts, curated writes, paraphrased prose), plus the person/source purge workflow. Cross-linked from memory architecture, dreaming, builtin, overview, CLI, and config reference; registered in docs navigation.
* docs(i18n): add glossary terms for the memory provenance page
* docs(memory): distinguish curated entries from missing lineage in the provenance page
* feat(state): add memory entry origins and session tombstone tables with session-aware write provenance
Additive same-version per-agent tables (memory_entry_origins, memory_session_tombstones) declared canonically with lazy idempotent ensure; read-only agent DB access exposed through the SDK; memory-write observers and the session-memory hook record the authoring session; archived narrative transcripts classified by authoritative run identity.
* feat(memory): admission policy, entry provenance, and openclaw memory forget
memoryPolicy.excludeSessions keeps configured sources out of the dreaming pipeline with recorded exclusions; promotion writes are marker-addressable and carry per-session origins through consolidation; memory forget purges whole entries and every derived artifact (corpus lines, diary quotes, index/FTS/vec/cache, short-term state, backups), reports curated agent writes from transcripts, and tombstones purged sessions so ingestion, backfill, and transcript indexing never re-admit them.
* docs(memory): document admission policy, memory forget, and provenance boundaries
* fix(memory): open the vec probe through openNodeSqliteDatabase
Kysely guardrail forbids direct node:sqlite constructors in production; the in-memory extension probe now uses the canonical opener.
* fix(memory): resolve archived and explicit sessions in memory forget
Explicit --session selectors union live session windows, transcript archives, and exact unresolved ids so an operator-named session always purges and tombstones instead of silently no-oping after lifecycle archival; reports carry per-session resolution sources. ClawSweeper P1 on #130151.
* fix(memory): reconcile consolidation origins for every workspace agent
Shared workspaces record entry origins per source agent; consolidation now re-keys and prunes origins in each participating agent's database (membership from the authoritative dreaming workspace mapping), so a non-owner agent's later memory forget still finds the live consolidated entry. ClawSweeper re-review P1 on #130151.
* fix(scripts): see SAFETY comments after template substitutions
The assertion ratchet scanned each file with a raw ts.createScanner, which
reads the `}` closing a template-literal substitution as a block close. The
scanner desynced there, so every `// SAFETY:` comment after a file's first
`${...}` was invisible and its annotated assertions were counted as bare.
Track substitution brace depth and rescan the closing brace the way the parser
does. The fix uncovers already-annotated assertions in eight files, so the
baseline shrinks accordingly.
* fix(skills): record skill usage again and retire dead curator tables
Skill lifecycle curation shipped in 2026.7.1 with two producers: a trusted
`skill.used` consumer writing `skill_usage`, and a daily sweep aging skills
into `skill_lifecycle`. The weekly collection review (#121653) replaced that
policy and deleted both producers, but left every reader in place. Since then
`skill_usage` has had no writer, so curator status reported `lastUsedAtMs:
null` and `useCount: 0` forever, and `skill_lifecycle` had no writer either, so
pin/unpin/restore either threw "not found" on fresh installs or, worse,
"succeeded" on upgraded ones while gating nothing at all.
`skill_workshop_proposal_origin_runs` was never read in any revision; proposal
provenance is authoritative in `record_json`.
Restore the usage producer at its owner and retire what has no owner:
- `skill.used` events populate `skill_usage` again, registered with the
collection-review maintenance it belongs beside. Curator status derives
curated skills from applied Workshop create proposals and reports real usage.
- Weekly review receives bounded `useCount` / `lastUsedDaysAgo` evidence, with
prompt text stating usage supports keeping a procedure and never alone
justifies a drop.
- State schema 10 drops `skill_lifecycle` and
`skill_workshop_proposal_origin_runs`. Previously archived skills return to
the active collection, where review judges them by content; the migration
logs how many. Reconcile now clears usage only for skills it actually drops.
- `skills.curator.pin`/`unpin`/`restore` stay registered for existing clients
but fail with an explicit retirement message instead of silently doing
nothing.
Retirement code moves to openclaw-state-db-table-retirements.ts to keep the
schema-repair module under max-lines; that split is a pure relocation.
Production delta is +23 raw: about -46 for the change itself, +44 for the file
split, +25 for the ratchet fix in the previous commit.
* 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(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.
* 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.
* 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
* fix(release): preserve validation plan across reruns
* test(release): align rerun plan assertions
* refactor(release): use canonical plan cache action
* style(test): format release plan cache assertion
The workflow ran a Codex agent over the full test suite and pushed
`test: optimize slow tests` straight to `main` under `contents: write`, with no
pull request and no human review. Its gates were a path allowlist, a
no-add/delete/rename rule, a non-decreasing total test count, and
`pnpm check:changed` -- which covers changed lanes, not the full suite. Test
optimization is exactly the class of change where a plausible edit can weaken
coverage without moving the test count, so unattended landing is the wrong
trade. Autonomous commits to `main` are not something this repo wants.
It had also been inert since well before this. The daily-cadence gate excluded
prior runs with `select(.status != "cancelled")`, but a finished cancelled run
reports `status: "completed"` with `conclusion: "cancelled"` -- verified against
run 32506655531, which that filter counts as a prior run. Its `concurrency`
block sets `cancel-in-progress: false`, so main's push rate produced dozens of
cancelled runs per hour and every trigger skipped, reporting green after ~2
minutes of doing nothing. No `test: optimize slow tests` commit has ever landed
on `main`.
`pnpm test:perf:groups` and the rest of the performance tooling it drove stay;
they are useful by hand and documented in docs/reference/test.md.
Repository secret OPENCLAW_TEST_PERF_AGENT_OPENAI_API_KEY now has no consumer
and can be deleted.
* fix(auth): keep a retired auth JSON from stranding a migrated store
Runtime failed closed with AUTH_PROFILE_MIGRATION_REQUIRED whenever a retired
credential file was present, even when the canonical SQLite store already held
the agent's profiles. One leftover auth.json therefore made a fully migrated
install unusable, and the gateway lifecycle preflight refused start/restart on
top of it, so every channel and provider stayed offline until Doctor ran.
A legacy file is now only fatal when the canonical store cannot serve
credentials. Doctor's importer never overwrites a usable stored credential, so
a file sitting beside a populated store is unarchived bytes, not pending
migration: runtime logs a one-time warning and keeps serving. An empty store
with a credential file still fails closed and never falls through to
environment auth. Startup degrades that owner to configured-unavailable
instead of refusing to boot, which lets the lifecycle preflight go away.
* refactor(secrets): retire the auth-profiles.json vocabulary
Auth profiles moved to SQLite, but operator-facing surfaces still named the
retired JSON file. The duplicate-agentDir error told operators to copy
auth-profiles.json to share credentials, which does nothing and lands the
second agent in a migration-required state; `openclaw migrate plan codex`
reported a target file that is never created; and the secrets picker labelled
candidates with a filename that no longer exists.
Renames the SecretTargetConfigFile discriminator to "auth-profile-store" and
corrects the operator-facing text, the migrate plan target, and the docs that
described the file as a live target. Genuine legacy-filename uses in doctor,
the security fixer, and migration fixtures are unchanged.
Also deletes resolveSecretPlanTargetByPath and ResolvedSecretPlanTarget from
the plugin SDK. They have no callers in core, plugins, or tests, and the
symbols are absent from the latest stable tag, so they carry no compatibility
obligation and are removed rather than deprecated. Their inline parameter type
was the only thing putting the retired filename on the public SDK surface.
* improve(wizard): warn about device-code phishing
The device-code prompt only warned against sharing the code, and only when an
expiry was known. Device-code phishing works the other way around: the attacker
starts the login and gets the victim to enter the attacker's code. Codes
delivered over a chat channel are the risky case and carry no expiry hint, so
the warning is now unconditional and covers received codes, matching the Codex
CLI prompt.
Also documents the Codex auth handoff: a subscription profile is installed as
in-memory external auth rather than persisted, and token refresh is inverted
so the refresh token stays in OpenClaw's store.
* fix(test): make transcript read-failure injection order-independent
server.sessions.compaction-read-errors.test.ts injected its failures with
mockRejectedValueOnce, which fails the NEXT call to loadTranscriptEvents
globally. Under --isolate=false a shard shares one worker, so any sibling
transcript read could consume the one-shot rejection before the compaction RPC
issued its own; compaction then ran against the real reader and returned ok,
failing three assertions. This shard was already red on main; a prior repair
fixed the mock's initialization order but left the call-order dependency.
Key the injection on the seeded sessionId instead, so unrelated readers cannot
consume it and the re-read case counts only its own session's reads.
Also updates two expectations invalidated by this branch: the duplicate-agentDir
remediation text, and the plugin SDK export ratchet, shrunk by the two retired
secret-plan exports.