From 6792921deabe98d1ac2d85b480366c42748b0760 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 11 Aug 2026 09:59:11 -0700 Subject: [PATCH] refactor: finish commitments retirement bookkeeping (#122143) * refactor(commitments): remove remaining retired references * refactor(state): track schema retirements --- .../automation-cron-hooks-tasks-polling.md | 2 +- scripts/test-projects.test-support.mts | 4 - src/state/openclaw-schema-retirements.json | 26 ++++ src/state/openclaw-schema-retirements.test.ts | 133 ++++++++++++++++++ 4 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 src/state/openclaw-schema-retirements.json create mode 100644 src/state/openclaw-schema-retirements.test.ts diff --git a/.agents/skills/claw-score/references/completeness/automation-cron-hooks-tasks-polling.md b/.agents/skills/claw-score/references/completeness/automation-cron-hooks-tasks-polling.md index f4af28fbc38d..c88eb3aa6318 100644 --- a/.agents/skills/claw-score/references/completeness/automation-cron-hooks-tasks-polling.md +++ b/.agents/skills/claw-score/references/completeness/automation-cron-hooks-tasks-polling.md @@ -9,5 +9,5 @@ Use this rubric when assigning category Completeness scores for the - Event Ingress: Telegram long polling, Telegram webhook mode, Zalo polling/webhook mode, Polling stall diagnostics, iMessage watch fallback, Gmail setup wizard, Watcher start/serve, Tailscale/public routing, Push token validation, Gmail event routing, POST /hooks/wake, POST /hooks/agent, Mapped hooks, Hook auth policy, Async dispatch - Automation Hooks: HOOK.md authoring, Hook discovery, Hook CLI management, Hook packs, Lifecycle event dispatch, api.on registration, Tool-call policy hooks, Message hooks, Session/lifecycle hooks, Plugin approval requests, cron_changed - Background Tasks and Flows: Task list/show/cancel, Task notifications, Task audit and maintenance, Chat task board, Task pressure status, Managed flows, Mirrored flows, openclaw tasks flow, Flow audit and maintenance, Plugin managedFlows -- Heartbeat: Heartbeat scheduling, Active hours, Wake and cooldown handling, Due-only heartbeat tasks, Commitment check-ins +- Heartbeat: Heartbeat scheduling, Active hours, Wake and cooldown handling, Due-only heartbeat tasks - Polling Controls: openclaw message poll, Telegram polls, Teams polls, Poll flags, Channel capability gates, process poll, process log, Background process status, No-progress loop detection, Process input controls diff --git a/scripts/test-projects.test-support.mts b/scripts/test-projects.test-support.mts index a21477c3ad7b..05689d14ed6c 100644 --- a/scripts/test-projects.test-support.mts +++ b/scripts/test-projects.test-support.mts @@ -697,10 +697,6 @@ const SOURCE_TEST_TARGETS = new Map([ ], ], ["src/commands/doctor-memory-search.ts", ["src/commands/doctor-memory-search.test.ts"]], - [ - "src/commitments/model-selection.runtime.ts", - ["src/commitments/runtime.test.ts", "src/agents/model-selection.test.ts"], - ], [ "src/agents/test-helpers/live-model-turn-probes.ts", ["src/agents/live-model-turn-probes.test.ts"], diff --git a/src/state/openclaw-schema-retirements.json b/src/state/openclaw-schema-retirements.json new file mode 100644 index 000000000000..546ac6116c11 --- /dev/null +++ b/src/state/openclaw-schema-retirements.json @@ -0,0 +1,26 @@ +{ + "retirements": [ + { + "database": "state", + "status": "planned", + "targetVersion": 7, + "table": "commitments", + "indexes": [ + "idx_commitments_scope_due", + "idx_commitments_status_due", + "idx_commitments_scope_dedupe", + "idx_commitments_agent_due", + "idx_commitments_agent_sent" + ], + "note": "Remove the retired commitments feature in the next shared-state schema." + }, + { + "database": "agent", + "status": "completed", + "targetVersion": 17, + "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." + } + ] +} diff --git a/src/state/openclaw-schema-retirements.test.ts b/src/state/openclaw-schema-retirements.test.ts new file mode 100644 index 000000000000..9ea9bbcc5787 --- /dev/null +++ b/src/state/openclaw-schema-retirements.test.ts @@ -0,0 +1,133 @@ +import { readFileSync } from "node:fs"; +import { DatabaseSync } from "node:sqlite"; +import { isRecord } from "@openclaw/normalization-core/record-coerce"; +import { expect, it } from "vitest"; +import { OPENCLAW_AGENT_SCHEMA_VERSION } from "./openclaw-agent-db-contract.js"; +import retirementManifest from "./openclaw-schema-retirements.json" with { type: "json" }; +import { OPENCLAW_STATE_SCHEMA_VERSION } from "./openclaw-state-db-contract.js"; + +type DatabaseOwner = "state" | "agent"; +type RetirementStatus = "planned" | "completed"; + +type SchemaRetirement = { + database: DatabaseOwner; + status: RetirementStatus; + targetVersion: number; + table: string; + indexes: string[]; + note?: string; +}; + +function assertInvariant(condition: unknown, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } +} + +function parseString(value: unknown, field: string): string { + assertInvariant(typeof value === "string" && value.length > 0, `${field} must be a string`); + return value; +} + +function parseStringArray(value: unknown, field: string): string[] { + assertInvariant(Array.isArray(value), `${field} must be an array`); + return value.map((entry, index) => parseString(entry, `${field}[${index}]`)); +} + +function parseRetirement(value: unknown, index: number): SchemaRetirement { + assertInvariant(isRecord(value), `retirements[${index}] must be an object`); + assertInvariant( + value.database === "state" || value.database === "agent", + `retirements[${index}].database must name a database owner`, + ); + assertInvariant( + value.status === "planned" || value.status === "completed", + `retirements[${index}].status must be planned or completed`, + ); + assertInvariant( + Number.isInteger(value.targetVersion) && Number(value.targetVersion) >= 0, + `retirements[${index}].targetVersion must be a non-negative integer`, + ); + assertInvariant( + value.note === undefined || typeof value.note === "string", + `retirements[${index}].note must be a string when present`, + ); + + return { + database: value.database, + status: value.status, + targetVersion: Number(value.targetVersion), + table: parseString(value.table, `retirements[${index}].table`), + indexes: parseStringArray(value.indexes, `retirements[${index}].indexes`), + note: value.note, + }; +} + +function parseRetirementManifest(value: unknown): SchemaRetirement[] { + assertInvariant(isRecord(value), "retirement manifest must be an object"); + assertInvariant(Array.isArray(value.retirements), "retirements must be an array"); + return value.retirements.map(parseRetirement); +} + +function assertUniqueRetirements(retirements: readonly SchemaRetirement[]): void { + const seen = new Set(); + for (const retirement of retirements) { + const key = `${retirement.database}/${retirement.table}`; + assertInvariant(!seen.has(key), `duplicate schema retirement: ${key}`); + seen.add(key); + } +} + +function hasSchemaObject(database: DatabaseSync, type: "table" | "index", name: string): boolean { + return ( + database.prepare("SELECT 1 FROM sqlite_schema WHERE type = ? AND name = ?").get(type, name) !== + undefined + ); +} + +it("keeps the schema retirement ledger aligned with canonical database schemas", () => { + const retirements = parseRetirementManifest(retirementManifest); + assertUniqueRetirements(retirements); + + const databases = { + state: { + currentVersion: OPENCLAW_STATE_SCHEMA_VERSION, + schemaUrl: new URL("./openclaw-state-schema.sql", import.meta.url), + database: new DatabaseSync(":memory:"), + }, + agent: { + currentVersion: OPENCLAW_AGENT_SCHEMA_VERSION, + schemaUrl: new URL("./openclaw-agent-schema.sql", import.meta.url), + database: new DatabaseSync(":memory:"), + }, + } satisfies Record< + DatabaseOwner, + { currentVersion: number; schemaUrl: URL; database: DatabaseSync } + >; + + try { + for (const database of Object.values(databases)) { + database.database.exec(readFileSync(database.schemaUrl, "utf8")); + } + + for (const retirement of retirements) { + const { currentVersion, database } = databases[retirement.database]; + const shouldExist = retirement.status === "planned"; + + if (shouldExist) { + expect(retirement.targetVersion).toBe(currentVersion + 1); + } else { + expect(retirement.targetVersion).toBeLessThanOrEqual(currentVersion); + } + + expect(hasSchemaObject(database, "table", retirement.table)).toBe(shouldExist); + for (const index of retirement.indexes) { + expect(hasSchemaObject(database, "index", index)).toBe(shouldExist); + } + } + } finally { + for (const database of Object.values(databases)) { + database.database.close(); + } + } +});