refactor: finish commitments retirement bookkeeping (#122143)

* refactor(commitments): remove remaining retired references

* refactor(state): track schema retirements
This commit is contained in:
Peter Steinberger
2026-08-11 09:59:11 -07:00
committed by GitHub
parent 82ba647673
commit 6792921dea
4 changed files with 160 additions and 5 deletions
@@ -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 - 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 - 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 - 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 - 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
-4
View File
@@ -697,10 +697,6 @@ const SOURCE_TEST_TARGETS = new Map([
], ],
], ],
["src/commands/doctor-memory-search.ts", ["src/commands/doctor-memory-search.test.ts"]], ["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/test-helpers/live-model-turn-probes.ts",
["src/agents/live-model-turn-probes.test.ts"], ["src/agents/live-model-turn-probes.test.ts"],
@@ -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."
}
]
}
@@ -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<string>();
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();
}
}
});