From 0b4c8a9fc9ba2884d7a40613f6d3acd4f1794324 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 06:26:54 -0700 Subject: [PATCH] fix(state): preserve additive column order (#121062) Keep fresh shared-state table definitions aligned with SQLite ADD COLUMN ordering so current-version upgraded databases pass canonical schema validation.\n\nRefs #121061 --- src/state/openclaw-state-db.test.ts | 73 +++++++++++++++++------------ src/state/openclaw-state-schema.sql | 8 ++-- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/src/state/openclaw-state-db.test.ts b/src/state/openclaw-state-db.test.ts index 1b1ec9d417e5..f72504a65e96 100644 --- a/src/state/openclaw-state-db.test.ts +++ b/src/state/openclaw-state-db.test.ts @@ -1956,39 +1956,50 @@ INSERT INTO macos_port_guardian_records VALUES (4242, 18789, '/usr/bin/ssh', 're ).toEqual([{ task_id: "task-index-repair" }]); }); - it("repairs the same-version worktree cleanup column with physical index drift", () => { - const stateDir = createTempStateDir(); - const env = { OPENCLAW_STATE_DIR: stateDir }; - const databasePath = materializeCurrentStateDatabase(stateDir); + it.each([ + { columnName: "run_end_cleanup_json", tableName: "worktrees" }, + { columnName: "shared_host", tableName: "worker_environments" }, + ])( + "appends same-version $columnName to $tableName before schema validation", + ({ columnName, tableName }) => { + const stateDir = createTempStateDir(); + const env = { OPENCLAW_STATE_DIR: stateDir }; + const databasePath = materializeCurrentStateDatabase(stateDir); - const { DatabaseSync } = requireNodeSqlite(); - const shippedSchema = new DatabaseSync(databasePath); - try { - shippedSchema.exec("ALTER TABLE worktrees DROP COLUMN run_end_cleanup_json;"); - expect(readSqliteNumberPragma(shippedSchema, "user_version")).toBe( - OPENCLAW_STATE_SCHEMA_VERSION, - ); - } finally { - shippedSchema.close(); - } - createTaskRunStatusIndexPhysicalDrift(databasePath); + const { DatabaseSync } = requireNodeSqlite(); + const shippedSchema = new DatabaseSync(databasePath); + let canonicalColumnOrder: string[]; + try { + canonicalColumnOrder = ( + shippedSchema.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ name: string }> + ).map((column) => column.name); + shippedSchema.exec(`ALTER TABLE ${tableName} DROP COLUMN ${columnName};`); + expect(readSqliteNumberPragma(shippedSchema, "user_version")).toBe( + OPENCLAW_STATE_SCHEMA_VERSION, + ); + } finally { + shippedSchema.close(); + } + createTaskRunStatusIndexPhysicalDrift(databasePath); - const reopened = openOpenClawStateDatabase({ env }); - const columns = reopened.db.prepare("PRAGMA table_info(worktrees)").all() as Array<{ - name: string; - }>; - expect(columns.map((column) => column.name)).toContain("run_end_cleanup_json"); - expect(reopened.db.prepare("PRAGMA integrity_check").get()).toEqual({ - integrity_check: "ok", - }); - expect( - reopened.db - .prepare( - "SELECT task_id FROM task_runs INDEXED BY idx_task_runs_status WHERE status = 'running'", - ) - .all(), - ).toEqual([{ task_id: "task-index-repair" }]); - }); + const reopened = openOpenClawStateDatabase({ env }); + const columns = reopened.db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{ + name: string; + }>; + expect(columns.map((column) => column.name)).toEqual(canonicalColumnOrder); + expect(canonicalColumnOrder.at(-1)).toBe(columnName); + expect(reopened.db.prepare("PRAGMA integrity_check").get()).toEqual({ + integrity_check: "ok", + }); + expect( + reopened.db + .prepare( + "SELECT task_id FROM task_runs INDEXED BY idx_task_runs_status WHERE status = 'running'", + ) + .all(), + ).toEqual([{ task_id: "task-index-repair" }]); + }, + ); it("does not add Claw bootstrap columns before rejecting unrelated index corruption", () => { const stateDir = createTempStateDir(); diff --git a/src/state/openclaw-state-schema.sql b/src/state/openclaw-state-schema.sql index 218340e87ed0..9d2c9239587b 100644 --- a/src/state/openclaw-state-schema.sql +++ b/src/state/openclaw-state-schema.sql @@ -1801,10 +1801,10 @@ CREATE TABLE IF NOT EXISTS worktrees ( owner_id TEXT, snapshot_ref TEXT, provisioned_paths_json TEXT, - run_end_cleanup_json TEXT, created_at INTEGER NOT NULL, last_active_at INTEGER NOT NULL, - removed_at INTEGER + removed_at INTEGER, + run_end_cleanup_json TEXT ) STRICT; CREATE INDEX IF NOT EXISTS idx_worktrees_repo_fingerprint @@ -1846,7 +1846,6 @@ CREATE TABLE IF NOT EXISTS worker_environments ( profile_snapshot_json TEXT NOT NULL, provision_operation_id TEXT NOT NULL UNIQUE, lease_id TEXT, - shared_host INTEGER CHECK (shared_host IN (0, 1)), ssh_host TEXT, ssh_port INTEGER CHECK (ssh_port IS NULL OR (ssh_port >= 1 AND ssh_port <= 65535)), ssh_user TEXT, @@ -1878,7 +1877,8 @@ CREATE TABLE IF NOT EXISTS worker_environments ( state_changed_at_ms INTEGER NOT NULL, idle_since_at_ms INTEGER, destroy_requested_at_ms INTEGER, - last_error TEXT + last_error TEXT, + shared_host INTEGER CHECK (shared_host IN (0, 1)) ) STRICT; CREATE UNIQUE INDEX IF NOT EXISTS idx_worker_environments_provider_lease