diff --git a/apps/macos/Tests/OpenClawIPCTests/PortGuardianRecordStoreTests.swift b/apps/macos/Tests/OpenClawIPCTests/PortGuardianRecordStoreTests.swift index 22eb0b326c6c..518a1307e6cd 100644 --- a/apps/macos/Tests/OpenClawIPCTests/PortGuardianRecordStoreTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/PortGuardianRecordStoreTests.swift @@ -568,7 +568,7 @@ struct PortGuardianRecordStoreTests { let fixture = try Self.fixture() defer { fixture.cleanup() } - for version in [4, 5, 6, 7, 8, 9] { + for version in [4, 5, 6, 7, 8, 9, 10] { let databaseURL = fixture.root.appendingPathComponent("supported-v\(version).sqlite") try Self.seedVersionedPortGuardianDatabase(databaseURL, schemaVersion: version) let store = try PortGuardianRecordStore(databaseURL: databaseURL) @@ -580,7 +580,7 @@ struct PortGuardianRecordStoreTests { #expect(try store.records() == [record]) } - for version in [10, 99] { + for version in [11, 99] { let databaseURL = fixture.root.appendingPathComponent("newer-v\(version).sqlite") try Self.seedVersionedPortGuardianDatabase(databaseURL, schemaVersion: version) #expect(throws: PortGuardianStoreError.self) { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift b/apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift index 3f98dfb67cef..7e28cbbd17c8 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawNativeState/OpenClawNativeStateSQLite.swift @@ -37,7 +37,7 @@ public enum OpenClawNativeStateSQLiteValueType: Equatable, Sendable { /// One recursive connection lock serializes transactions and statement access. public final class OpenClawNativeStateSQLite: @unchecked Sendable { // Keep aligned with OPENCLAW_STATE_SCHEMA_VERSION. Native clients never upgrade this database. - private static let maximumSupportedSchemaVersion: Int64 = 9 + private static let maximumSupportedSchemaVersion: Int64 = 10 private static let defaultBusyTimeoutMilliseconds: Int32 = 5000 private struct SchemaObject: Hashable { diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index d02d76aeaf77..5a72fe73bf38 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -226,9 +226,9 @@ proceed with these assumptions: SQLite. Old `auth-profiles.json`, per-agent `auth.json`, and shared `credentials/oauth.json` files are doctor migration inputs, then removed after import. -- Generated model catalog state is database-backed. Runtime code must not write - `agents//agent/models.json`; existing `models.json` files are legacy - doctor inputs and are removed after import into `agent_model_catalogs`. +- Generated model catalogs have no runtime-owned SQLite store or `models.json` + doctor import. The unused `agent_model_catalogs` table was retired in state + schema v10. - Runtime must not migrate, normalize, or bridge transcript locators. Active transcript identity is `{agentId, sessionId}` in SQLite. File paths are legacy doctor inputs only, and `sqlite-transcript://...` must disappear from @@ -318,10 +318,10 @@ The branch already has a real shared SQLite base: transcript events. - Current shared tables include `agent_databases`, `auth_profile_stores`, `auth_profile_state`, - `plugin_state_entries`, `plugin_blob_entries`, `media_blobs`, + `plugin_state_entries`, `plugin_blob_entries`, `skill_uploads`, `capture_sessions`, `capture_events`, `capture_blobs`, `sandbox_registry_entries`, `cron_jobs`, - `delivery_queue_entries`, `model_capability_cache`, + `delivery_queue_entries`, `workspace_setup_state`, `workspace_path_aliases`, `workspace_attestations`, `workspace_generated_bootstrap_hashes`, `native_hook_relay_bridges`, `current_conversation_bindings`, `plugin_binding_approvals`, @@ -448,11 +448,11 @@ The branch already has a real shared SQLite base: - Legacy auth profile path helpers now live in doctor legacy code. Core auth profile path helpers expose SQLite auth-store identity and display locations, not `auth-profiles.json` or `auth-state.json` runtime paths. -- Subagent run recovery and OpenRouter model capability cache runtime modules - now keep SQLite snapshot readers/writers separate from doctor-only legacy JSON - import helpers. OpenRouter capabilities use the typed generic - `model_capability_cache` rows under `provider_id = "openrouter"` instead of - one opaque cache blob or a provider-specific host table. Subagent run +- Subagent run recovery keeps SQLite snapshot readers/writers separate from + doctor-only legacy JSON import helpers. OpenRouter capabilities use the + provider-local in-memory TTL cache in `extensions/openrouter/provider-catalog.ts`; + the unused `model_capability_cache` table was retired in state schema v10. + Subagent run `taskName` is stored in the typed `subagent_runs.task_name` column; the `payload_json` copy is replay/debug data, not the source for hot display or lookup fields. @@ -496,9 +496,9 @@ The branch already has a real shared SQLite base: - Android device identity and cached device auth remain app-local stores. They require a separate Android-owned migration; the host SQLite claims do not describe current Android behavior. -- Android notification recent-package history uses typed - `android_notification_recent_packages` rows. Runtime no longer migrates or - reads the old SharedPreferences CSV keys. +- Android notification recent-package history remains app-local Kotlin state; + the unused `android_notification_recent_packages` table was retired in state + schema v10. - Device identity creation fails closed when a legacy `identity/device.json` cannot be safely imported, when the SQLite identity row is invalid, or when the SQLite identity store cannot be opened. Gateway startup and Doctor both @@ -975,8 +975,9 @@ sessionId})`; create, branch, continue, list, and fork flows live in their opt-in JSONL artifacts. `OPENCLAW_CACHE_TRACE_FILE`, `OPENCLAW_ANTHROPIC_PAYLOAD_LOG_FILE`, `OPENCLAW_RAW_STREAM_PATH`, and `OPENCLAW_DIAGNOSTICS_TIMELINE_PATH` remain supported path controls. Gateway - stability bundles use typed SQLite `diagnostic_stability_bundles` rows and can - be materialized into an explicit support export. + stability bundles are JSON file exports owned by + `src/logging/diagnostic-stability-bundle.ts`; the unused + `diagnostic_stability_bundles` table was retired in state schema v10. - Cron persistence now reconciles SQLite `cron_jobs` rows instead of deleting/reinserting the whole job table on each save. Plugin target writebacks update matching cron rows directly and keep runtime cron state in @@ -1203,12 +1204,9 @@ sessionId})`; create, branch, continue, list, and fork flows live in their durable ACPX runtime records are the SQLite lease and gateway-instance rows; the old ACPX `stateDir` config surface is removed because no runtime state is written there anymore. -- Gateway media attachments now use the shared `media_blobs` SQLite table as - the canonical byte store. Local paths returned to channel and sandbox - compatibility surfaces are temp materializations of the database row, not the - durable media store. Runtime media allowlists no longer include legacy - `$OPENCLAW_STATE_DIR/media` or config-dir `media` roots; those directories are - doctor import sources only. +- Gateway media attachment bytes remain file-based in `src/media/store.ts`. + The unused `media_blobs` table was retired in state schema v10; Doctor does + not import media files into SQLite. - Shell completion no longer writes `$OPENCLAW_STATE_DIR/completions/*` cache files. Install, doctor, update, and release smoke paths use generated completion output or profile sourcing instead of durable completion cache @@ -1452,11 +1450,8 @@ create` validates the written archive by default; `--no-verify` is the - The global schema does not keep an unused `agents` registry table. Agent database discovery is the canonical `agent_databases` registry until runtime has a real agent-record owner. -- Generated model catalog config is stored in typed global SQLite - `agent_model_catalogs` rows keyed by agent directory. Runtime callers use - `ensureOpenClawModelCatalog`; there is no `models.json` compatibility API in - runtime code. The implementation writes SQLite and the embedded PI registry is - hydrated from that stored payload without creating a `models.json` file. +- Generated model catalogs have no runtime-owned SQLite store. The rebuildable + `agent_model_catalogs` cache table was retired in state schema v10. - QMD has no runtime export, collection, SDK, or lease surface. Doctor migrates configured QMD paths into builtin `memory.search.extraPaths` and can remove retired per-agent QMD indexes, model downloads, collection metadata, and @@ -1506,7 +1501,6 @@ plugin_binding_approvals(plugin_root, channel, account_id, plugin_id, plugin_nam tui_last_sessions(scope_key, session_key, updated_at) plugin_state_entries(plugin_id, namespace, entry_key, value_json, created_at, expires_at) plugin_blob_entries(plugin_id, namespace, entry_key, metadata_json, blob, created_at, expires_at) -media_blobs(subdir, id, content_type, size_bytes, blob, created_at, updated_at) skill_uploads(upload_id, kind, slug, force, size_bytes, sha256, actual_sha256, received_bytes, archive_blob, created_at, expires_at, committed, committed_at, idempotency_key_hash) skill_upload_chunks(upload_id, byte_offset, size_bytes, chunk_blob) web_push_subscriptions(endpoint_hash, subscription_id, endpoint, p256dh, auth, created_at_ms, updated_at_ms) @@ -1522,8 +1516,6 @@ workspace_path_aliases(alias_key, alias_path, workspace_key, workspace_path, upd workspace_attestations(workspace_key, attested_at_ms, updated_at_ms) workspace_generated_bootstrap_hashes(workspace_key, filename, sha256) native_hook_relay_bridges(relay_id, pid, hostname, port, token, expires_at_ms, updated_at_ms) -model_capability_cache(provider_id, model_id, name, input_text, input_image, reasoning, supports_tools, context_window, max_tokens, cost_input, cost_output, cost_cache_read, cost_cache_write, updated_at_ms) -agent_model_catalogs(catalog_key, agent_dir, raw_json, updated_at) managed_outgoing_image_records(attachment_id, session_key, agent_id, message_id, created_at, updated_at, retention_class, alt, original_media_id, original_media_subdir, original_content_type, original_width, original_height, original_size_bytes, original_filename, record_json, cleanup_pending) gateway_restart_sentinel(sentinel_key, version, kind, status, ts, session_key, thread_id, delivery_channel, delivery_to, delivery_account_id, message, continuation_json, doctor_hint, stats_json, payload_json, updated_at_ms) channel_pairing_requests(channel_key, account_id, request_id, code, created_at, last_seen_at, meta_json) @@ -1783,14 +1775,12 @@ Keep shared coordination state in `state/openclaw.sqlite`: - Plugin state - Sandbox container/browser registry - Cron/scheduler run history -- Pairing, device, push, update-check, TUI, OpenRouter/model caches, and other +- Pairing, device, push, update-check, TUI, and other small gateway-scoped runtime state - Backup and migration metadata -- Gateway media attachment bytes. Done for runtime writes; direct file paths - are temp materializations for compatibility with channel senders and sandbox - staging. Runtime allowlists accept SQLite materialization paths, not legacy - state/config media roots. Doctor imports legacy media files into - `media_blobs` and removes the source files after successful row writes. +- Gateway media attachment bytes remain file-based in `src/media/store.ts`; + the unused `media_blobs` table was retired in state schema v10 and has no + Doctor import. - Debug proxy capture sessions, events, and payload blobs. Done: captures live in the shared state DB and open through the shared state DB bootstrap, schema, WAL, and busy-timeout settings. Payload bytes are gzip-compressed in diff --git a/docs/reference/database-schemas.md b/docs/reference/database-schemas.md index eb0e791c55a0..d97823a24791 100644 --- a/docs/reference/database-schemas.md +++ b/docs/reference/database-schemas.md @@ -126,6 +126,7 @@ Version 3 was an unshipped development step folded into version 4. | 7 | Retired inferred-commitment storage removed | Unreleased | | 8 | Cloud-worker placement execution modes and mode-aware turn claims | Unreleased | | 9 | In-root agent database registry paths stored relative to the state directory | Unreleased | +| 10 | Six dead tables retired (agent_model_catalogs, android_notification_recent_packages, command_log_entries, diagnostic_stability_bundles, media_blobs, model_capability_cache) | Unreleased | ### State schema 9 @@ -182,6 +183,107 @@ The general procedure is: 3. Set `PRAGMA user_version` and `schema_meta.schema_version` to the target version. 4. Run the target release's full database verification before starting the Gateway. +### Example: state schema 10 to 9 + +Schema 10 removed six dead shared-state tables. A schema 9 build still requires those canonical tables and indexes, so a manual downgrade must recreate their exact empty schemas before lowering the version. + +Run equivalent SQL against the global state database after inspecting the exact schema that wrote it: + +```sql +BEGIN IMMEDIATE; + +CREATE TABLE IF NOT EXISTS agent_model_catalogs ( + catalog_key TEXT NOT NULL PRIMARY KEY, + agent_dir TEXT NOT NULL, + raw_json TEXT NOT NULL, + updated_at INTEGER NOT NULL +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_agent_model_catalogs_agent_dir + ON agent_model_catalogs(agent_dir, updated_at DESC); + +CREATE TABLE IF NOT EXISTS android_notification_recent_packages ( + package_name TEXT NOT NULL PRIMARY KEY, + sort_order INTEGER NOT NULL, + updated_at_ms INTEGER NOT NULL +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_android_notification_recent_packages_order + ON android_notification_recent_packages(sort_order, package_name); + +CREATE TABLE IF NOT EXISTS command_log_entries ( + id TEXT NOT NULL PRIMARY KEY, + timestamp_ms INTEGER NOT NULL, + action TEXT NOT NULL, + session_key TEXT NOT NULL, + sender_id TEXT NOT NULL, + source TEXT NOT NULL, + entry_json TEXT NOT NULL +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_command_log_entries_timestamp + ON command_log_entries(timestamp_ms DESC, id); + +CREATE INDEX IF NOT EXISTS idx_command_log_entries_session + ON command_log_entries(session_key, timestamp_ms DESC, id); + +CREATE TABLE IF NOT EXISTS diagnostic_stability_bundles ( + bundle_key TEXT NOT NULL PRIMARY KEY, + reason TEXT NOT NULL, + generated_at TEXT NOT NULL, + bundle_json TEXT NOT NULL, + created_at INTEGER NOT NULL +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_diagnostic_stability_bundles_created + ON diagnostic_stability_bundles(created_at DESC, bundle_key); + +CREATE TABLE IF NOT EXISTS media_blobs ( + subdir TEXT NOT NULL, + id TEXT NOT NULL, + content_type TEXT, + size_bytes INTEGER NOT NULL, + blob BLOB NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (subdir, id) +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_media_blobs_created + ON media_blobs(created_at); + +CREATE TABLE IF NOT EXISTS model_capability_cache ( + provider_id TEXT NOT NULL, + model_id TEXT NOT NULL, + name TEXT NOT NULL, + input_text INTEGER NOT NULL, + input_image INTEGER NOT NULL, + reasoning INTEGER NOT NULL, + supports_tools INTEGER, + context_window INTEGER NOT NULL, + max_tokens INTEGER NOT NULL, + cost_input REAL NOT NULL, + cost_output REAL NOT NULL, + cost_cache_read REAL NOT NULL, + cost_cache_write REAL NOT NULL, + updated_at_ms INTEGER NOT NULL, + PRIMARY KEY (provider_id, model_id) +) STRICT; + +CREATE INDEX IF NOT EXISTS idx_model_capability_cache_provider_updated + ON model_capability_cache(provider_id, updated_at_ms DESC, model_id); + +PRAGMA user_version = 9; +UPDATE schema_meta +SET schema_version = 9, + updated_at = unixepoch('now') * 1000 +WHERE meta_key = 'primary'; + +COMMIT; +``` + +The recreated tables start empty because schema 10 discarded only dead or rebuildable cache rows. A botched downgrade means restore from the verified backup. + ### Example: state schema 9 to 8 Schema 8 expects every `agent_databases.path` value to be absolute. Before lowering `user_version`, inspect each registry row on the same platform that wrote it. Leave absolute external paths unchanged; replace every relative path with its platform-native absolute form by resolving it against the state directory that owns `state/openclaw.sqlite`. Then set both `PRAGMA user_version` and `schema_meta.schema_version` to 8 in the same transaction. diff --git a/extensions/voice-call/doctor-contract-api.ts b/extensions/voice-call/doctor-contract-api.ts index dff745908b1b..a5bfed3d33d2 100644 --- a/extensions/voice-call/doctor-contract-api.ts +++ b/extensions/voice-call/doctor-contract-api.ts @@ -144,6 +144,8 @@ function describeVoiceCallSchemaMigration(migration: OpenClawStateDatabaseSchema return "audit event ledger -> versioned message lifecycle schema"; case "commitments-retirement-v7": return "retired commitments storage -> removed table and indexes"; + case "state-table-retirement-v10": + return "retired shared-state tables -> removed tables and indexes"; case "worker-placement-execution-mode-v8": return "cloud worker placements -> execution-mode claims"; case "operator-approvals-system-agent": diff --git a/package.json b/package.json index 90bae4f38227..6e125fd273dc 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "2026.8.1", "openclaw": { "schemaVersions": { - "state": 9, + "state": 10, "agent": 17 } }, diff --git a/src/agents/model-scan.ts b/src/agents/model-scan.ts index 5b0d3af88aec..f25ef26bb020 100644 --- a/src/agents/model-scan.ts +++ b/src/agents/model-scan.ts @@ -35,8 +35,7 @@ const DEFAULT_CONCURRENCY = 3; // The OpenRouter /models catalog is a provider-controlled, runtime-fetched body // (already >100 KB and growing). Read it under a byte cap before JSON.parse so a // faulty or hostile provider cannot stream an unbounded document and exhaust -// process memory. Keep this aligned with the runtime capability cache for the -// same endpoint so scan and runtime discovery fail at the same boundary. +// process memory. OpenRouter capability data is cached within its provider. const OPENROUTER_MODELS_BODY_MAX_BYTES = 16 * 1024 * 1024; const BASE_IMAGE_PNG = diff --git a/src/audit/message-delivery-progress-store.test.ts b/src/audit/message-delivery-progress-store.test.ts index 32d16a4a46e9..05b6e42c80ef 100644 --- a/src/audit/message-delivery-progress-store.test.ts +++ b/src/audit/message-delivery-progress-store.test.ts @@ -9,6 +9,7 @@ import { closeOpenClawStateDatabaseForTest, openOpenClawStateDatabase, } from "../state/openclaw-state-db.js"; +import { STATE_SCHEMA_10_TO_9_DOWNGRADE_SQL } from "../state/openclaw-state-schema-v10-retirement.test-support.js"; import { recordAuditEvent } from "./audit-event-store.js"; import type { OutboundMessageProgressInput } from "./audit-event-types.js"; import { @@ -137,10 +138,10 @@ describe("outbound message progress companion", () => { ); }); - it("stays absent through startup, reads, and terminal-only writes at schema v9", () => { + it("stays absent through startup, reads, and terminal-only writes at schema v10", () => { const database = databaseOptions(); const opened = openOpenClawStateDatabase(database); - expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(9); + expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(10); expect(tableExists(opened.db, "outbound_message_progress")).toBe(false); expect(tableExists(opened.db, "outbound_message_execution_bindings")).toBe(false); @@ -274,6 +275,10 @@ describe("outbound message progress companion", () => { openOpenClawStateDatabase(database).db.exec( "ALTER TABLE skill_workshop_proposals DROP COLUMN claim_released_time;", ); + // The pinned reader is a v9-era build, so it refuses v10 databases by the + // version contract. Project the file back to the exact v9 shape with the + // documented 10→9 downgrade so the C04 same-version reader proof survives. + openOpenClawStateDatabase(database).db.exec(STATE_SCHEMA_10_TO_9_DOWNGRADE_SQL); closeOpenClawStateDatabaseForTest(); const repositoryRoot = process.cwd(); diff --git a/src/gateway/worker-environments/placement-store.move.test.ts b/src/gateway/worker-environments/placement-store.move.test.ts index 387c961bb021..345074ab7289 100644 --- a/src/gateway/worker-environments/placement-store.move.test.ts +++ b/src/gateway/worker-environments/placement-store.move.test.ts @@ -156,7 +156,7 @@ describe("worker session placement moves", () => { }, }); expect(begun.intent.operationId).toMatch(/^move:v1:[A-Za-z0-9_-]{43}$/u); - expect(database.db.prepare("PRAGMA user_version").get()).toEqual({ user_version: 9 }); + expect(database.db.prepare("PRAGMA user_version").get()).toEqual({ user_version: 10 }); expect(store.getPlacementMove(SESSION.sessionId)).toEqual(begun.intent); expect(store.getPlacementMoves([SESSION.sessionId, "missing"])).toEqual( new Map([[SESSION.sessionId, begun.intent]]), diff --git a/src/infra/state-migrations.doctor.ts b/src/infra/state-migrations.doctor.ts index 4924e6c2ab27..94cb345adde5 100644 --- a/src/infra/state-migrations.doctor.ts +++ b/src/infra/state-migrations.doctor.ts @@ -191,6 +191,8 @@ function describeStateSchemaMigration(migration: OpenClawStateDatabaseSchemaMigr return "cloud worker placements → execution-mode claims"; case "agent-databases-relative-paths-v9": return "agent database registry paths → state-relative storage"; + case "state-table-retirement-v10": + return "retired shared-state tables → removed tables and indexes"; case "operator-approvals-system-agent": return "operator approvals → OpenClaw system changes"; case "session-watch-cursor-provenance-v4": diff --git a/src/node-host/node-worker-launch-store.test.ts b/src/node-host/node-worker-launch-store.test.ts index e1d4010bcc62..fb99609bf1af 100644 --- a/src/node-host/node-worker-launch-store.test.ts +++ b/src/node-host/node-worker-launch-store.test.ts @@ -312,7 +312,7 @@ describe("node worker launch store container identity", () => { expect(new NodeWorkerLaunchStore({ env }).get("container-launch")).toEqual(receipt); }); - it("lets the exact v9 predecessor read and write a populated candidate container journal before candidate reopen", () => { + it("lets the exact v10 predecessor read and write a populated candidate container journal before candidate reopen", () => { const { database, env, store } = fixture(); const databasePath = openOpenClawStateDatabase({ env }).path; const { planHash, supervisor } = claimLaunch(store, "candidate-container-launch"); @@ -330,7 +330,7 @@ describe("node worker launch store container identity", () => { nowMs: NOW_MS, }); expect(hasContainerIdentityTable(database)).toBe(true); - expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(9); + expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(10); closeOpenClawStateDatabaseForTest(); const companionStart = OPENCLAW_STATE_SCHEMA_SQL.indexOf( @@ -358,11 +358,11 @@ describe("node worker launch store container identity", () => { const predecessor = new DatabaseSync(databasePath); try { - expect(predecessor.prepare("PRAGMA user_version").get()).toEqual({ user_version: 9 }); + expect(predecessor.prepare("PRAGMA user_version").get()).toEqual({ user_version: 10 }); expect(() => assertSqliteSchemaContains( predecessor, - "predecessor v9 global schema", + "predecessor v10 global schema", predecessorSchema, predecessorCompatibility, ), diff --git a/src/state/openclaw-schema-retirements.json b/src/state/openclaw-schema-retirements.json index ec82c82db517..4165bb653412 100644 --- a/src/state/openclaw-schema-retirements.json +++ b/src/state/openclaw-schema-retirements.json @@ -7,6 +7,54 @@ "table": "state_leases", "indexes": ["idx_agent_state_leases_expiry", "idx_agent_state_leases_owner"], "note": "Removed from the canonical per-agent schema in version 17." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "agent_model_catalogs", + "indexes": ["idx_agent_model_catalogs_agent_dir"], + "note": "Rebuildable model-catalog cache contents were dropped in state schema 10." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "android_notification_recent_packages", + "indexes": ["idx_android_notification_recent_packages_order"], + "note": "Never had a shipped writer; retired in state schema 10." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "command_log_entries", + "indexes": ["idx_command_log_entries_timestamp", "idx_command_log_entries_session"], + "note": "Never had a shipped writer; retired in state schema 10." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "diagnostic_stability_bundles", + "indexes": ["idx_diagnostic_stability_bundles_created"], + "note": "Never had a shipped writer; retired in state schema 10." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "media_blobs", + "indexes": ["idx_media_blobs_created"], + "note": "Never had a shipped writer; retired in state schema 10." + }, + { + "database": "state", + "status": "completed", + "targetVersion": 10, + "table": "model_capability_cache", + "indexes": ["idx_model_capability_cache_provider_updated"], + "note": "Never had a shipped writer; retired in state schema 10." } ] } diff --git a/src/state/openclaw-state-db-contract.ts b/src/state/openclaw-state-db-contract.ts index b5302c615cbf..5128d8ed83f7 100644 --- a/src/state/openclaw-state-db-contract.ts +++ b/src/state/openclaw-state-db-contract.ts @@ -1,12 +1,13 @@ import type { DatabaseSync } from "node:sqlite"; import type { SqliteWalMaintenance } from "../infra/sqlite-wal.js"; +// v10 retires six dead tables that shipped without runtime owners. // v9 stores in-root agent database registry paths relative to the state dir. // v8 records cloud-placement execution mode and mode-aware turn claims. // v7 retires the inert shared commitments table. // v6 makes every committed shared-state table part of the canonical runtime schema. // v5 records durable cloud-worker result refs on pending workspace fences. -export const OPENCLAW_STATE_SCHEMA_VERSION = 9; +export const OPENCLAW_STATE_SCHEMA_VERSION = 10; export const OPENCLAW_STATE_STRICT_SCHEMA_VERSION = 3; // Privacy-sensitive feature tables remain absent even in fresh databases until // their feature-local first write. The canonical SQL still owns their shape. @@ -92,6 +93,7 @@ export type OpenClawStateDatabaseSchemaMigration = { | "commitments-retirement-v7" | "worker-placement-execution-mode-v8" | "agent-databases-relative-paths-v9" + | "state-table-retirement-v10" | "operator-approvals-system-agent" | "session-watch-cursor-provenance-v4" | "strict-tables-v3"; diff --git a/src/state/openclaw-state-db-maintenance.ts b/src/state/openclaw-state-db-maintenance.ts index 1e86fa676305..5088fc445553 100644 --- a/src/state/openclaw-state-db-maintenance.ts +++ b/src/state/openclaw-state-db-maintenance.ts @@ -14,6 +14,7 @@ import { OPENCLAW_STATE_SCHEMA_VERSION, type OpenClawStateDatabaseOptions, } from "./openclaw-state-db-contract.js"; +import { tableExists, tableHasColumn } from "./openclaw-state-db-schema-helpers.js"; import { resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths.js"; import { OPENCLAW_STATE_MAINTENANCE_SCHEMA_COMPATIBILITY } from "./openclaw-state-schema-compatibility.js"; import { OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-schema.js"; @@ -47,6 +48,7 @@ const STATE_MIGRATION_ALLOWED_MISSING_TABLES = { 6: STATE_V6_ADDITIVE_TABLES, 7: STATE_V6_ADDITIVE_TABLES, 8: STATE_V6_ADDITIVE_TABLES, + 9: STATE_V6_ADDITIVE_TABLES, } as const satisfies Record; type OpenClawStateMigrationVersion = keyof typeof STATE_MIGRATION_ALLOWED_MISSING_TABLES; @@ -196,6 +198,51 @@ export function assertOpenClawStateDatabaseV8ForMigration( assertOpenClawStateDatabaseVersionForMigration(database, { ...options, version: 8 }); } +/** Require every stable v9 table before the v10 retirement migration can run. */ +export function assertOpenClawStateDatabaseV9ForMigration( + database: DatabaseSync, + options: { pathname: string }, +): void { + assertOpenClawStateDatabaseVersionForMigration(database, { ...options, version: 9 }); +} + +export function markCurrentStateSchemaVersion( + db: DatabaseSync, + options: { createMetadataIfMissing?: boolean } = {}, +): void { + // Pre-v2 databases can legitimately predate the audit table. Leave their + // version untouched so normal open can create the complete v2 schema first. + if (!tableExists(db, "audit_events")) { + return; + } + db.exec(`PRAGMA user_version = ${OPENCLAW_STATE_SCHEMA_VERSION};`); + if ( + tableExists(db, "schema_meta") && + ["meta_key", "schema_version", "updated_at"].every((column) => + tableHasColumn(db, "schema_meta", column), + ) + ) { + const now = Date.now(); + if (options.createMetadataIfMissing) { + // Recognized pre-metadata schemas may acquire the global owner row during + // doctor migration. Conflicting existing ownership is preserved so the + // final maintenance assertion rejects and rolls back the repair. + db.prepare( + `INSERT INTO schema_meta ( + meta_key, role, schema_version, agent_id, app_version, created_at, updated_at + ) VALUES ('primary', 'global', ?, NULL, NULL, ?, ?) + ON CONFLICT(meta_key) DO UPDATE SET + schema_version = excluded.schema_version, + updated_at = excluded.updated_at`, + ).run(OPENCLAW_STATE_SCHEMA_VERSION, now, now); + return; + } + db.prepare( + "UPDATE schema_meta SET schema_version = ?, updated_at = ? WHERE meta_key = 'primary'", + ).run(OPENCLAW_STATE_SCHEMA_VERSION, now); + } +} + export function resolveDatabasePath(options: OpenClawStateDatabaseOptions = {}): string { return path.resolve(options.path ?? resolveOpenClawStateSqlitePath(options.env ?? process.env)); } diff --git a/src/state/openclaw-state-db-schema-repair.ts b/src/state/openclaw-state-db-schema-repair.ts index a8f996d0f919..bf2c52a8a4f1 100644 --- a/src/state/openclaw-state-db-schema-repair.ts +++ b/src/state/openclaw-state-db-schema-repair.ts @@ -36,6 +36,15 @@ import { } from "./openclaw-state-db.paths.js"; import { OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-schema.js"; +const RETIRED_DEAD_STATE_TABLES_V10 = [ + "agent_model_catalogs", + "android_notification_recent_packages", + "command_log_entries", + "diagnostic_stability_bundles", + "media_blobs", + "model_capability_cache", +] as const; + export function dropLegacyStateTables(db: DatabaseSync): void { // Unreleased transient history; drop, do not migrate. const transientHistoryTable = ["database", "verifications"].join("_"); @@ -364,6 +373,25 @@ export function migrateRetiredCommitmentsSchema( } } +export function migrateRetiredDeadStateTablesV10( + db: DatabaseSync, + previousVersion: number, +): boolean { + if (previousVersion >= 10) { + return false; + } + // These tables shipped without writers except for rebuildable model-catalog + // cache rows, so unlike commitments they need no shape or dependency proof. + let dropped = false; + for (const tableName of RETIRED_DEAD_STATE_TABLES_V10) { + if (tableExists(db, tableName)) { + db.exec(`DROP TABLE IF EXISTS ${tableName};`); + dropped = true; + } + } + return dropped; +} + export function migrateWorkerPlacementExecutionModeSchema( db: DatabaseSync, previousVersion: number, @@ -630,43 +658,6 @@ export function repairLegacyGatewayRestartHandoffsForStrictMigration(db: Databas `); } -export function markCurrentStateSchemaVersion( - db: DatabaseSync, - options: { createMetadataIfMissing?: boolean } = {}, -): void { - // Pre-v2 databases can legitimately predate the audit table. Leave their - // version untouched so normal open can create the complete v2 schema first. - if (!tableExists(db, "audit_events")) { - return; - } - db.exec(`PRAGMA user_version = ${OPENCLAW_STATE_SCHEMA_VERSION};`); - if ( - tableExists(db, "schema_meta") && - ["meta_key", "schema_version", "updated_at"].every((column) => - tableHasColumn(db, "schema_meta", column), - ) - ) { - const now = Date.now(); - if (options.createMetadataIfMissing) { - // Recognized pre-metadata schemas may acquire the global owner row during - // doctor migration. Conflicting existing ownership is preserved so the - // final maintenance assertion rejects and rolls back the repair. - db.prepare( - `INSERT INTO schema_meta ( - meta_key, role, schema_version, agent_id, app_version, created_at, updated_at - ) VALUES ('primary', 'global', ?, NULL, NULL, ?, ?) - ON CONFLICT(meta_key) DO UPDATE SET - schema_version = excluded.schema_version, - updated_at = excluded.updated_at`, - ).run(OPENCLAW_STATE_SCHEMA_VERSION, now, now); - return; - } - db.prepare( - "UPDATE schema_meta SET schema_version = ?, updated_at = ? WHERE meta_key = 'primary'", - ).run(OPENCLAW_STATE_SCHEMA_VERSION, now); - } -} - export function assertCanonicalStateSchemaShape(db: DatabaseSync, pathname: string): void { operatorApprovalMigration.assertCanonicalOperatorApprovalKinds(db, pathname); if (!hasCanonicalAgentDatabasesPrimaryKey(db)) { @@ -729,6 +720,12 @@ export function detectOpenClawStateDatabaseSchemaMigrationsFromDatabase( if (userVersion === 8 && tableExists(db, "agent_databases")) { migrations.push({ kind: "agent-databases-relative-paths-v9", path: pathname }); } + if ( + userVersion < 10 && + RETIRED_DEAD_STATE_TABLES_V10.some((tableName) => tableExists(db, tableName)) + ) { + migrations.push({ kind: "state-table-retirement-v10", path: pathname }); + } if (!hasCanonicalAgentDatabasesPrimaryKey(db)) { migrations.push({ kind: "agent-databases-composite-primary-key", path: pathname }); } diff --git a/src/state/openclaw-state-db.generated.d.ts b/src/state/openclaw-state-db.generated.d.ts index 401f8e670244..e85424aacd59 100644 --- a/src/state/openclaw-state-db.generated.d.ts +++ b/src/state/openclaw-state-db.generated.d.ts @@ -77,13 +77,6 @@ export interface AgentDeletionJournal { workspace_dir: string; } -export interface AgentModelCatalogs { - agent_dir: string; - catalog_key: string; - raw_json: string; - updated_at: number; -} - export interface AgentProvenance { agent_id: string; created_at_ms: number; @@ -91,12 +84,6 @@ export interface AgentProvenance { creator_agent_id: string | null; } -export interface AndroidNotificationRecentPackages { - package_name: string; - sort_order: number; - updated_at_ms: number; -} - export interface ApnsRegistrationTombstones { deleted_at_ms: number; node_id: string; @@ -366,16 +353,6 @@ export interface ClawhubPromotionsFeedState { updated_at_ms: number; } -export interface CommandLogEntries { - action: string; - entry_json: string; - id: string; - sender_id: string; - session_key: string; - source: string; - timestamp_ms: number; -} - export interface ConfigHealthEntries { config_path: string; last_known_good_json: string | null; @@ -651,14 +628,6 @@ export interface DiagnosticEvents { sequence: Generated; } -export interface DiagnosticStabilityBundles { - bundle_json: string; - bundle_key: string; - created_at: number; - generated_at: string; - reason: string; -} - export interface ExecApprovalsConfig { agent_count: number; allowlist_count: number; @@ -902,16 +871,6 @@ export interface McpOauthStores { updated_at: number; } -export interface MediaBlobs { - blob: Uint8Array; - content_type: string | null; - created_at: number; - id: string; - size_bytes: number; - subdir: string; - updated_at: number; -} - export interface MeetingTranscriptSessions { created_at_ms: number; export_key: string; @@ -976,23 +935,6 @@ export interface MigrationSources { target_table: string; } -export interface ModelCapabilityCache { - context_window: number; - cost_cache_read: number; - cost_cache_write: number; - cost_input: number; - cost_output: number; - input_image: number; - input_text: number; - max_tokens: number; - model_id: string; - name: string; - provider_id: string; - reasoning: number; - supports_tools: number | null; - updated_at_ms: number; -} - export interface ModelCatalogRemote { bundle_json: string; checked_at: number; @@ -1847,9 +1789,7 @@ export interface DB { agent_database_leases: AgentDatabaseLeases; agent_databases: AgentDatabases; agent_deletion_journal: AgentDeletionJournal; - agent_model_catalogs: AgentModelCatalogs; agent_provenance: AgentProvenance; - android_notification_recent_packages: AndroidNotificationRecentPackages; apns_registration_tombstones: ApnsRegistrationTombstones; apns_registrations: ApnsRegistrations; audit_events: AuditEvents; @@ -1870,7 +1810,6 @@ export interface DB { claw_workspace_files: ClawWorkspaceFiles; clawhub_promotion_claims: ClawhubPromotionClaims; clawhub_promotions_feed_state: ClawhubPromotionsFeedState; - command_log_entries: CommandLogEntries; config_health_entries: ConfigHealthEntries; config_machine_state: ConfigMachineState; config_revision_keys: ConfigRevisionKeys; @@ -1889,7 +1828,6 @@ export interface DB { device_pairing_paired: DevicePairingPaired; device_pairing_pending: DevicePairingPending; diagnostic_events: DiagnosticEvents; - diagnostic_stability_bundles: DiagnosticStabilityBundles; exec_approvals_config: ExecApprovalsConfig; execution_decision_facts: ExecutionDecisionFacts; execution_identity_contexts: ExecutionIdentityContexts; @@ -1907,13 +1845,11 @@ export interface DB { managed_outgoing_image_records: ManagedOutgoingImageRecords; mcp_oauth_pending_authorizations: McpOauthPendingAuthorizations; mcp_oauth_stores: McpOauthStores; - media_blobs: MediaBlobs; meeting_transcript_sessions: MeetingTranscriptSessions; meeting_transcript_summaries: MeetingTranscriptSummaries; meeting_transcript_utterances: MeetingTranscriptUtterances; migration_runs: MigrationRuns; migration_sources: MigrationSources; - model_capability_cache: ModelCapabilityCache; model_catalog_remote: ModelCatalogRemote; native_hook_relay_bridges: NativeHookRelayBridges; node_host_config: NodeHostConfig; diff --git a/src/state/openclaw-state-db.test.ts b/src/state/openclaw-state-db.test.ts index dca28bc92b3e..069911131784 100644 --- a/src/state/openclaw-state-db.test.ts +++ b/src/state/openclaw-state-db.test.ts @@ -51,6 +51,7 @@ import { } from "./openclaw-state-db.js"; import { resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths.js"; import { getOpenClawStateRuntimeSchema } from "./openclaw-state-schema-compatibility.js"; +import { STATE_SCHEMA_10_TO_9_DOWNGRADE_SQL } from "./openclaw-state-schema-v10-retirement.test-support.js"; import { OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-schema.js"; import { collectSqliteSchemaShape, @@ -220,6 +221,15 @@ const RETIRED_COMMITMENT_SCHEMA_OBJECTS = [ "idx_commitments_agent_sent", ] as const; +const RETIRED_STATE_TABLES_V10 = [ + "agent_model_catalogs", + "android_notification_recent_packages", + "command_log_entries", + "diagnostic_stability_bundles", + "media_blobs", + "model_capability_cache", +] as const; + function seedV6CommitmentSchema(database: DatabaseSync): void { database.exec(` CREATE TABLE IF NOT EXISTS commitments ( @@ -1692,7 +1702,7 @@ describe("openclaw state database", () => { warnings: [], }); const migrated = openOpenClawStateDatabase({ env }); - expect(readSqliteNumberPragma(migrated.db, "user_version")).toBe(9); + expect(readSqliteNumberPragma(migrated.db, "user_version")).toBe(OPENCLAW_STATE_SCHEMA_VERSION); expect( migrated.db.prepare("SELECT agent_id, path FROM agent_databases ORDER BY agent_id").all(), ).toEqual([ @@ -1720,6 +1730,53 @@ describe("openclaw state database", () => { ]); }); + it.each(["runtime open", "doctor repair"] as const)( + "retires six dead v9 shared-state tables through %s", + (migrationPath) => { + const stateDir = createTempStateDir(); + const options = { env: { OPENCLAW_STATE_DIR: stateDir } }; + const databasePath = materializeCurrentStateDatabase(stateDir); + const { DatabaseSync } = requireNodeSqlite(); + const legacy = new DatabaseSync(databasePath); + legacy.exec(STATE_SCHEMA_10_TO_9_DOWNGRADE_SQL); + legacy.exec(` + INSERT INTO agent_model_catalogs (catalog_key, agent_dir, raw_json, updated_at) + VALUES ('main', '/agents/main', '{"models":[]}', 1); + `); + legacy.close(); + + expect(detectOpenClawStateDatabaseSchemaMigrations(options)).toContainEqual({ + kind: "state-table-retirement-v10", + path: databasePath, + }); + if (migrationPath === "doctor repair") { + expect(repairOpenClawStateDatabaseSchema(options)).toEqual({ + changes: ["Retired six dead shared-state tables (v10)"], + warnings: [], + }); + } + + const migrated = openOpenClawStateDatabase(options); + expect(readSqliteNumberPragma(migrated.db, "user_version")).toBe(10); + expect( + migrated.db + .prepare("SELECT schema_version FROM schema_meta WHERE meta_key = 'primary'") + .get(), + ).toEqual({ schema_version: 10 }); + for (const tableName of RETIRED_STATE_TABLES_V10) { + expect( + migrated.db + .prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name = ?") + .get(tableName), + ).toBeUndefined(); + } + expect(detectOpenClawStateDatabaseSchemaMigrations(options)).not.toContainEqual({ + kind: "state-table-retirement-v10", + path: databasePath, + }); + }, + ); + it.each(["runtime open", "doctor repair"] as const)( "retires v6 commitments through %s while preserving shared leases", (migrationPath) => { diff --git a/src/state/openclaw-state-db.ts b/src/state/openclaw-state-db.ts index 2a45ab2e499e..90fe503702a3 100644 --- a/src/state/openclaw-state-db.ts +++ b/src/state/openclaw-state-db.ts @@ -59,7 +59,9 @@ import { assertOpenClawStateDatabaseV6ForMigration, assertOpenClawStateDatabaseV7ForMigration, assertOpenClawStateDatabaseV8ForMigration, + assertOpenClawStateDatabaseV9ForMigration, assertSupportedSchemaVersion, + markCurrentStateSchemaVersion, resolveDatabasePath, } from "./openclaw-state-db-maintenance.js"; import { openUnpublishedStateDatabase } from "./openclaw-state-db-open.js"; @@ -75,9 +77,9 @@ import { assertCanonicalStateSchemaShape, detectOpenClawStateDatabaseSchemaMigrationsFromDatabase, dropLegacyStateTables, - markCurrentStateSchemaVersion, migrateAgentDatabaseRelativePaths as migrateAgentPaths, migrateRetiredCommitmentsSchema, + migrateRetiredDeadStateTablesV10, migrateWorkerPlacementExecutionModeSchema, repairAgentDatabasesCompositePrimaryKey, repairLegacyGatewayRestartHandoffsForStrictMigration, @@ -95,12 +97,16 @@ import { getOpenClawStateRuntimeSchema } from "./openclaw-state-schema-compatibi import { OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-schema.js"; export { registerOpenClawStateDatabaseLifecycleListener } from "./openclaw-state-db-cache.js"; -const STATE_MIGRATION_ASSERTIONS = { - 5: assertOpenClawStateDatabaseV5ForMigration, - 6: assertOpenClawStateDatabaseV6ForMigration, - 7: assertOpenClawStateDatabaseV7ForMigration, - 8: assertOpenClawStateDatabaseV8ForMigration, -} as const; +const STATE_MIGRATION_ASSERTIONS = new Map< + number, + typeof assertOpenClawStateDatabaseV5ForMigration +>([ + [5, assertOpenClawStateDatabaseV5ForMigration], + [6, assertOpenClawStateDatabaseV6ForMigration], + [7, assertOpenClawStateDatabaseV7ForMigration], + [8, assertOpenClawStateDatabaseV8ForMigration], + [9, assertOpenClawStateDatabaseV9ForMigration], +]); export { OPENCLAW_DATABASE_SCHEMA_DOCS_URL, @@ -193,13 +199,8 @@ function repairOpenClawStateDatabaseSchemaWithWriteAccess( assertSqliteSchemaTablesPresent(db, pathname, OPENCLAW_STATE_SCHEMA_SQL, { allowedMissingTables: LAZY_ADDITIVE_STATE_TABLES, }); - } else if ( - previousVersion === 5 || - previousVersion === 6 || - previousVersion === 7 || - previousVersion === 8 - ) { - STATE_MIGRATION_ASSERTIONS[previousVersion](db, { pathname }); + } else { + STATE_MIGRATION_ASSERTIONS.get(previousVersion)?.(db, { pathname }); } if (rebuiltIndexNames.size === 0) { assertSqliteIntegrity(db, pathname); @@ -208,6 +209,9 @@ function repairOpenClawStateDatabaseSchemaWithWriteAccess( if (migrateRetiredCommitmentsSchema(db, previousVersion)) { applied.push("Retired shared state commitments table and indexes"); } + if (migrateRetiredDeadStateTablesV10(db, previousVersion)) { + applied.push("Retired six dead shared-state tables (v10)"); + } if (migrateWorkerPlacementExecutionModeSchema(db, previousVersion)) { applied.push("Migrated cloud worker placements to execution modes"); } @@ -408,16 +412,12 @@ function ensureSchema( }); ensureAdditiveStateColumns(db); assertCurrentStateRuntimeSchema(db, pathname); - } else if ( - previousVersion === 5 || - previousVersion === 6 || - previousVersion === 7 || - previousVersion === 8 - ) { - STATE_MIGRATION_ASSERTIONS[previousVersion](db, { pathname }); + } else { + STATE_MIGRATION_ASSERTIONS.get(previousVersion)?.(db, { pathname }); } dropLegacyStateTables(db); migrateRetiredCommitmentsSchema(db, previousVersion); + migrateRetiredDeadStateTablesV10(db, previousVersion); migrateWorkerPlacementExecutionModeSchema(db, previousVersion); const pathMigration: AgentPathSummary = migrateAgentPaths(db, previousVersion, pathname); ensureAdditiveStateColumns(db); diff --git a/src/state/openclaw-state-schema-v10-retirement.test-support.ts b/src/state/openclaw-state-schema-v10-retirement.test-support.ts new file mode 100644 index 000000000000..623ae457f179 --- /dev/null +++ b/src/state/openclaw-state-schema-v10-retirement.test-support.ts @@ -0,0 +1,86 @@ +// Exact schema of the six dead tables retired by state schema 10, matching the +// documented 10→9 downgrade recipe in docs/reference/database-schemas.md. +// Tests use it to rebuild v9-shaped databases: the v10 retirement regression +// seeds a pre-migration file, and the pinned pre-C04 audit reader projects a +// current database back to the v9 era it was built against. +const RETIRED_STATE_TABLES_V10_FIXTURE_SQL = ` +CREATE TABLE IF NOT EXISTS agent_model_catalogs ( + catalog_key TEXT NOT NULL PRIMARY KEY, + agent_dir TEXT NOT NULL, + raw_json TEXT NOT NULL, + updated_at INTEGER NOT NULL +) STRICT; +CREATE INDEX IF NOT EXISTS idx_agent_model_catalogs_agent_dir + ON agent_model_catalogs(agent_dir, updated_at DESC); + +CREATE TABLE IF NOT EXISTS android_notification_recent_packages ( + package_name TEXT NOT NULL PRIMARY KEY, + sort_order INTEGER NOT NULL, + updated_at_ms INTEGER NOT NULL +) STRICT; +CREATE INDEX IF NOT EXISTS idx_android_notification_recent_packages_order + ON android_notification_recent_packages(sort_order, package_name); + +CREATE TABLE IF NOT EXISTS command_log_entries ( + id TEXT NOT NULL PRIMARY KEY, + timestamp_ms INTEGER NOT NULL, + action TEXT NOT NULL, + session_key TEXT NOT NULL, + sender_id TEXT NOT NULL, + source TEXT NOT NULL, + entry_json TEXT NOT NULL +) STRICT; +CREATE INDEX IF NOT EXISTS idx_command_log_entries_timestamp + ON command_log_entries(timestamp_ms DESC, id); +CREATE INDEX IF NOT EXISTS idx_command_log_entries_session + ON command_log_entries(session_key, timestamp_ms DESC, id); + +CREATE TABLE IF NOT EXISTS diagnostic_stability_bundles ( + bundle_key TEXT NOT NULL PRIMARY KEY, + reason TEXT NOT NULL, + generated_at TEXT NOT NULL, + bundle_json TEXT NOT NULL, + created_at INTEGER NOT NULL +) STRICT; +CREATE INDEX IF NOT EXISTS idx_diagnostic_stability_bundles_created + ON diagnostic_stability_bundles(created_at DESC, bundle_key); + +CREATE TABLE IF NOT EXISTS media_blobs ( + subdir TEXT NOT NULL, + id TEXT NOT NULL, + content_type TEXT, + size_bytes INTEGER NOT NULL, + blob BLOB NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (subdir, id) +) STRICT; +CREATE INDEX IF NOT EXISTS idx_media_blobs_created + ON media_blobs(created_at); + +CREATE TABLE IF NOT EXISTS model_capability_cache ( + provider_id TEXT NOT NULL, + model_id TEXT NOT NULL, + name TEXT NOT NULL, + input_text INTEGER NOT NULL, + input_image INTEGER NOT NULL, + reasoning INTEGER NOT NULL, + supports_tools INTEGER, + context_window INTEGER NOT NULL, + max_tokens INTEGER NOT NULL, + cost_input REAL NOT NULL, + cost_output REAL NOT NULL, + cost_cache_read REAL NOT NULL, + cost_cache_write REAL NOT NULL, + updated_at_ms INTEGER NOT NULL, + PRIMARY KEY (provider_id, model_id) +) STRICT; +CREATE INDEX IF NOT EXISTS idx_model_capability_cache_provider_updated + ON model_capability_cache(provider_id, updated_at_ms DESC, model_id); +`; + +// The documented downgrade rewinds both version markers in the same batch. +export const STATE_SCHEMA_10_TO_9_DOWNGRADE_SQL = `${RETIRED_STATE_TABLES_V10_FIXTURE_SQL} +PRAGMA user_version = 9; +UPDATE schema_meta SET schema_version = 9 WHERE meta_key = 'primary'; +`; diff --git a/src/state/openclaw-state-schema.sql b/src/state/openclaw-state-schema.sql index 0d7de1191560..a53f98ed05b5 100644 --- a/src/state/openclaw-state-schema.sql +++ b/src/state/openclaw-state-schema.sql @@ -383,17 +383,6 @@ CREATE TABLE IF NOT EXISTS session_upstream_links ( CREATE INDEX IF NOT EXISTS idx_session_upstream_links_catalog_id ON session_upstream_links(catalog_id); -CREATE TABLE IF NOT EXISTS diagnostic_stability_bundles ( - bundle_key TEXT NOT NULL PRIMARY KEY, - reason TEXT NOT NULL, - generated_at TEXT NOT NULL, - bundle_json TEXT NOT NULL, - created_at INTEGER NOT NULL -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_diagnostic_stability_bundles_created - ON diagnostic_stability_bundles(created_at DESC, bundle_key); - CREATE TABLE IF NOT EXISTS state_leases ( scope TEXT NOT NULL, lease_key TEXT NOT NULL, @@ -725,15 +714,6 @@ CREATE TABLE IF NOT EXISTS gateway_origin_device_tokens ( PRIMARY KEY (gateway_scope, device_id, role) ) STRICT; -CREATE TABLE IF NOT EXISTS android_notification_recent_packages ( - package_name TEXT NOT NULL PRIMARY KEY, - sort_order INTEGER NOT NULL, - updated_at_ms INTEGER NOT NULL -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_android_notification_recent_packages_order - ON android_notification_recent_packages(sort_order, package_name); - CREATE TABLE IF NOT EXISTS macos_port_guardian_records ( pid INTEGER NOT NULL PRIMARY KEY, port INTEGER NOT NULL, @@ -807,37 +787,6 @@ CREATE TABLE IF NOT EXISTS native_hook_relay_bridges ( CREATE INDEX IF NOT EXISTS idx_native_hook_relay_bridges_expires ON native_hook_relay_bridges(expires_at_ms, relay_id); -CREATE TABLE IF NOT EXISTS model_capability_cache ( - provider_id TEXT NOT NULL, - model_id TEXT NOT NULL, - name TEXT NOT NULL, - input_text INTEGER NOT NULL, - input_image INTEGER NOT NULL, - reasoning INTEGER NOT NULL, - supports_tools INTEGER, - context_window INTEGER NOT NULL, - max_tokens INTEGER NOT NULL, - cost_input REAL NOT NULL, - cost_output REAL NOT NULL, - cost_cache_read REAL NOT NULL, - cost_cache_write REAL NOT NULL, - updated_at_ms INTEGER NOT NULL, - PRIMARY KEY (provider_id, model_id) -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_model_capability_cache_provider_updated - ON model_capability_cache(provider_id, updated_at_ms DESC, model_id); - -CREATE TABLE IF NOT EXISTS agent_model_catalogs ( - catalog_key TEXT NOT NULL PRIMARY KEY, - agent_dir TEXT NOT NULL, - raw_json TEXT NOT NULL, - updated_at INTEGER NOT NULL -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_agent_model_catalogs_agent_dir - ON agent_model_catalogs(agent_dir, updated_at DESC); - CREATE TABLE IF NOT EXISTS managed_outgoing_image_records ( attachment_id TEXT NOT NULL PRIMARY KEY, session_key TEXT NOT NULL, @@ -1394,20 +1343,6 @@ CREATE INDEX IF NOT EXISTS idx_plugin_blob_expiry CREATE INDEX IF NOT EXISTS idx_plugin_blob_listing ON plugin_blob_entries(plugin_id, namespace, created_at, entry_key); -CREATE TABLE IF NOT EXISTS media_blobs ( - subdir TEXT NOT NULL, - id TEXT NOT NULL, - content_type TEXT, - size_bytes INTEGER NOT NULL, - blob BLOB NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY (subdir, id) -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_media_blobs_created - ON media_blobs(created_at); - CREATE TABLE IF NOT EXISTS skill_uploads ( upload_id TEXT NOT NULL PRIMARY KEY, kind TEXT NOT NULL, @@ -1689,22 +1624,6 @@ CREATE TABLE IF NOT EXISTS cron_job_scratch ( CREATE INDEX IF NOT EXISTS idx_cron_job_scratch_store_updated ON cron_job_scratch(store_key, updated_at_ms DESC, job_id); -CREATE TABLE IF NOT EXISTS command_log_entries ( - id TEXT NOT NULL PRIMARY KEY, - timestamp_ms INTEGER NOT NULL, - action TEXT NOT NULL, - session_key TEXT NOT NULL, - sender_id TEXT NOT NULL, - source TEXT NOT NULL, - entry_json TEXT NOT NULL -) STRICT; - -CREATE INDEX IF NOT EXISTS idx_command_log_entries_timestamp - ON command_log_entries(timestamp_ms DESC, id); - -CREATE INDEX IF NOT EXISTS idx_command_log_entries_session - ON command_log_entries(session_key, timestamp_ms DESC, id); - CREATE TABLE IF NOT EXISTS delivery_queue_entries ( queue_name TEXT NOT NULL, id TEXT NOT NULL, diff --git a/src/state/user-profiles.test.ts b/src/state/user-profiles.test.ts index 7d95eb1cfc45..cd8fa6e66a6c 100644 --- a/src/state/user-profiles.test.ts +++ b/src/state/user-profiles.test.ts @@ -113,7 +113,7 @@ describe("user profiles", () => { expect( openOpenClawStateDatabase(options).db.prepare("PRAGMA user_version").get()?.user_version, ).toBe(versionBefore); - expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(9); + expect(OPENCLAW_STATE_SCHEMA_VERSION).toBe(10); expect(second).toEqual(first); expect(ensureProfileForEmail("ADA@example.com", options)).toEqual(first); expect(listProfiles(options)).toEqual([ diff --git a/test/scripts/check-native-state-schema-version.test.ts b/test/scripts/check-native-state-schema-version.test.ts index a6b6bf3b92ec..0e84c8a80eb2 100644 --- a/test/scripts/check-native-state-schema-version.test.ts +++ b/test/scripts/check-native-state-schema-version.test.ts @@ -6,7 +6,7 @@ import { describe("native state schema version guard", () => { it("keeps the checked-in Swift and TypeScript contracts aligned", () => { - expect(checkNativeStateSchemaVersion()).toBe(9); + expect(checkNativeStateSchemaVersion()).toBe(10); }); it("fails when a deliberate Swift fixture drifts behind TypeScript", () => {