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
This commit is contained in:
Peter Steinberger
2026-08-25 17:31:32 -07:00
committed by GitHub
parent 2671eff4bf
commit 6a246f70d1
22 changed files with 447 additions and 255 deletions
@@ -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) {
@@ -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 {
+25 -35
View File
@@ -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/<agentId>/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
+102
View File
@@ -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.
@@ -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":
+1 -1
View File
@@ -3,7 +3,7 @@
"version": "2026.8.1",
"openclaw": {
"schemaVersions": {
"state": 9,
"state": 10,
"agent": 17
}
},
+1 -2
View File
@@ -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 =
@@ -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();
@@ -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]]),
+2
View File
@@ -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":
@@ -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,
),
@@ -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."
}
]
}
+3 -1
View File
@@ -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";
@@ -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<number, readonly string[]>;
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));
}
+34 -37
View File
@@ -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 });
}
-64
View File
@@ -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<number>;
}
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;
+58 -1
View File
@@ -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) => {
+21 -21
View File
@@ -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);
@@ -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';
`;
-81
View File
@@ -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,
+1 -1
View File
@@ -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([
@@ -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", () => {