Commit Graph

22 Commits

Author SHA1 Message Date
Peter Steinberger 1ea2640f54 refactor(state): consolidate wide rows, plugin index, workspace attestations, and shared auth singletons at schema v13 (#130466)
* 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>
2026-08-27 15:26:14 +08:00
Vincent Koc d62cb8aa07 fix(sqlite): safely discard retired commitments state (#130629)
* fix(sqlite): finalize commitments retirement

* fix(doctor): discard retired commitments json
2026-08-27 11:18:58 +08:00
Peter Steinberger 1fc29beba2 refactor(state): fold singleton tables into config_machine_state at schema v12 (#129876)
* 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.
2026-08-26 13:32:19 -07:00
Peter Steinberger 4bd8859126 fix(skills): record skill usage again and retire dead curator tables (#129769)
* 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.
2026-08-26 01:06:06 -07:00
Peter Steinberger 6a246f70d1 refactor(state): retire six dead shared-state tables at schema v10 (#129626)
* 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
2026-08-25 17:31:32 -07:00
Martin Cleary 7f24f54214 docs: require discussion for material SQLite changes (#128876) 2026-08-24 23:53:14 +01: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
Josh Avant 0b85966072 fix gateway conversation route ownership (#126424) 2026-08-21 12:20:21 -07:00
Peter Steinberger bbbd70542b feat(sessions): recover offline device placements (#126284)
* feat(sessions): recover offline device placements

* chore(protocol): refresh session placement models

* perf(ui): lazy-load session placement recovery

* test(ui): remove dropdown timing assertion

* test(ui): await committed cloud recovery route

* fix(sessions): complete explicit abandonment locally

* fix(sessions): fence lists by runner availability

* fix(ui): preserve canonical session freshness

* fix(sessions): preserve recovery contracts after rebase

* test(ui): await durable cloud recovery entry

* test(gateway): complete current runner fixtures

* fix(gateway): publish runner availability edges

* fix(ui): preserve shared session freshness

* fix(ui): preserve canonical sidebar session state

* fix(sessions): preserve abandoned partials and run-owned replies

* fix(sessions): resume durable abandonment retries

* test(gateway): compose provisioning replay with runner availability

* fix(sessions): publish recovered move transitions
2026-08-20 09:59:59 -07:00
Peter Steinberger 4bc37cd597 fix(state): copy-safe agent database registry via state-relative paths (schema v9) (#124728)
* fix(state): store in-root agent database registry paths relative to the state dir (schema v9)

Copied state directories retained stale absolute agent database registry rows. The combined gateway store then merged old and new copies, causing every sessions.list request to fail with SessionCanonicalKeyMigrationRequiredError.

Schema v9 stores in-root registry paths relative to the state directory; migration rewrites eligible rows, deletes stale duplicates, and preserves external paths.

* fix(voice-call): describe state-relative registry migration

* fix(state): preserve registry locator traversal

* test(macos): move PortGuardian schema-version boundary to v9

The store opens any state database up to maximumSupportedSchemaVersion,
which the schema v9 bump moved from 8 to 9. Shift the supported/newer
partition in the boundary test accordingly.

* fix(state): re-anchor copied default-layout registry rows instead of deleting them

Resolve the ClawSweeper P1 by preserving genuine external default-layout registrations, re-anchoring copied rows when an in-root counterpart exists, and deleting only dual default-layout conflicts.
2026-08-16 13:31:32 -07:00
Peter Steinberger b6548e509a refactor(state): retire commitments schema (#122176)
* refactor(state): retire commitments schema

* fix(state): complete commitments retirement safeguards

* test(state): expect doctor retirement report

* test(state): prove doctor v7 markers atomically

* fix(state): align schema support metadata

* fix(state): report actual commitments retirement

* fix(state): validate retired commitments schema

* fix(state): preserve early commitments upgrades

* fix(state): require exact commitments index set

* fix(state): reject non-exact retired schemas

* fix(state): recognize supported retirement layouts

* fix(state): expose commitments retirement to doctor

* fix(voice-call): describe commitments retirement

* test(state): align v7 rebase proof

* fix(state): protect commitments retirement dependencies

* fix(state): accept partial commitments layouts

* chore(docs): refresh v7 api baselines
2026-08-12 14:21:34 -07:00
Peter Steinberger 48253cde46 docs: correct database-first current-state claims (#122085)
Schema versions are global v6 and agent v17 (src/state/openclaw-state-db-contract.ts:6; src/state/openclaw-agent-db-contract.ts:22). State v5/v6 provenance comes from 7a7d6bb and 509a5f0, matching the current contract comments at src/state/openclaw-state-db-contract.ts:4.

QMD is retired and Doctor migrates its paths to builtin memory (src/commands/doctor/shared/legacy-config-migrations.runtime.retired-memory-qmd.ts:94). Device auth is SQLite-backed in TypeScript and Swift while Android remains SecurePrefs-backed (src/infra/device-auth-store.ts:115; apps/shared/OpenClawKit/Sources/OpenClawKit/DeviceAuthStore.swift:26; apps/android/app/src/main/java/ai/openclaw/app/gateway/DeviceAuthStore.kt:55).

Opt-in cache, payload, raw-stream, timeline, and macOS diagnostics remain JSONL artifacts (src/agents/cache-trace.ts:93; src/agents/anthropic-payload-log.ts:46; src/agents/embedded-agent-subscribe.raw-stream.ts:16; src/infra/diagnostics-timeline.ts:96; apps/macos/Sources/OpenClaw/DiagnosticsFileLog.swift:3).

The canonical agent schema currently has cache_entries but no VFS/tool/run artifact tables, and subagent attachments materialize in child workspaces (src/state/openclaw-agent-schema.sql:398; src/agents/subagents/spawn/subagent-attachments.ts:308). Current Doctor/session owners replace deleted prototype paths (src/infra/state-migrations.doctor.ts:66; src/config/sessions/session-accessor.sqlite-entry.ts:165; src/config/sessions/paths.ts:320).
2026-08-11 08:03:10 -07:00
Peter Steinberger 38fabc4e03 refactor(state): retire tenant-free agent lease schema (#121943)
* refactor(state): retire agent lease schema

* chore(plugin-sdk): refresh schema version closure

* test(state): preserve v17 migration fixtures

* docs(state): align lease schema inventory
2026-08-11 02:42:56 -07:00
Peter Steinberger f31d9d8fa9 fix: prevent externally supervised state schema drift (#121069)
* fix: prevent externally supervised state schema drift

* refactor: isolate schema ownership support code

* test: follow canonical additive column order

* test: keep older schema fixture valid

* fix: preserve additive schema compatibility

* chore: remove release-owned changelog entry

* test: follow schema compatibility owner

* style: format schema compatibility test

* refactor: split sqlite schema sql helpers

* fix: preserve desktop schema compatibility

* fix: preserve ownership gates across platforms

* fix: preserve detached updater long paths

* fix: close external state ownership races

* chore: refresh plugin sdk api baseline
2026-08-09 14:04:40 -07:00
Peter Steinberger a3e79236aa fix(state): preserve rollback across additive columns (#121075)
* fix(state): tolerate compatible additive columns

Preserve same-version rollback compatibility for bare nullable SQLite columns while continuing to reject constrained schema drift. Treat the historical shared_host CHECK definition as a named compatibility boundary.\n\nRefs #121061

* fix(state): keep additive registry internal

* test(state): keep constrained preflight fixture
2026-08-09 08:29:19 -07:00
Peter Steinberger 8b0735e89f refactor(memory)!: remove the QMD backend; builtin is the only memory engine (#120936)
* refactor(memory): remove qmd backend

Make builtin the sole memory-core engine, rename the retained session helper barrel, retire QMD config with doctor migrations, and remove QMD runtime/UI/policy surfaces.

* docs(memory): remove qmd backend guidance

Delete the QMD concept page, rewrite memory documentation for builtin retrieval, and remove QMD from navigation and taxonomy source.

* refactor(memory): remove qmd-only leftovers

* refactor(memory): finish qmd integration cleanup

* build(deps): align root string-width types

* build(deps): model root string-width tooling

* refactor(memory): align qmd removal ui and docs

* fix(memory): preserve qmd external paths in doctor

* test(memory): remove obsolete backend probe case

* test(plugin-sdk): refresh private type baseline
2026-08-09 03:05:47 -07:00
Peter Steinberger 40fb1ca27e fix: schema-version refusal blames a downgrade that never happened and cannot identify the stale install (#115232)
* fix(state): name the refusing install in schema and exec-approval refusals

The newer-schema refusal told operators not to downgrade and to upgrade
OpenClaw, neither of which is actionable when two builds share one release
version string. It now names the install root that refused, both schema
versions, and warns that a linked source checkout reports its git HEAD even
when its built dist is older.

The exec-approvals gate told operators to run `openclaw doctor --fix` without
naming the state directory, so a bare invocation repaired the default root
while the scoped install stayed blocked. Both the TypeScript gate and its
Swift sibling now scope the command to the blocked state directory.

Refs #115008

* fix(gateway): name the refusing install in the startup schema refusal

* fix(mac): keep the exec-approvals gate message buildable on iOS

* fix(exec-approvals): shell-quote the state directory in the repair command

* fix(exec-approvals): state the repair directory in prose so every shell can follow it
2026-07-28 12:21:43 -04:00
Peter Steinberger 608bb3b8ab refactor(sqlite): drop write-only verification history and share the terminal-open latch (#110532) 2026-07-18 09:18:40 +01:00
Peter Steinberger f8e417a774 docs: add database downgrade troubleshooting for the 2026.7.2 schema migration (#110487) 2026-07-18 08:03:16 +01:00
Peter Steinberger deddeb3dac feat(sqlite): move quarantine decisions to a dedicated store that survives primary database damage (#110453) 2026-07-18 07:13:43 +01:00
Peter Steinberger 442c166122 refactor(sessions): replace ambient watch sentinels with provenance (#110428)
* refactor(sessions): store watch provenance in cursors

* style(docs): align database schema table

* refactor(state): use Kysely for watch migration

* style(state): remove redundant timestamp casts
2026-07-18 06:55:29 +01:00
Peter Steinberger a89f01abf4 perf(sqlite): fast schema gates, background integrity verification, and downgrade protection (#110271)
* perf(sqlite): gate schema version before integrity scans and quarantine terminal open failures

* feat(gateway): refuse incompatible database schemas at boot and verify integrity in the background

* feat(update): refuse installing builds that cannot open the current databases

* docs: add database schema reference with version history and downgrade guidance

* test(update): tolerate optional runner options in git-mutation mocks

* refactor(update): split npm package-target metadata out of update-check

* chore: model verifier internals for knip and regenerate docs map

* chore(i18n): resync stale iOS localization catalog

* chore: allowlist read-only preflight and verifier raw SQLite access
2026-07-18 05:49:39 +01:00