mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
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:
committed by
GitHub
parent
2671eff4bf
commit
6a246f70d1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -3,7 +3,7 @@
|
||||
"version": "2026.8.1",
|
||||
"openclaw": {
|
||||
"schemaVersions": {
|
||||
"state": 9,
|
||||
"state": 10,
|
||||
"agent": 17
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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]]),
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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';
|
||||
`;
|
||||
@@ -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,
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user