mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
refactor(skills): isolate workshop transition persistence
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
import { sha256Hex } from "../../infra/crypto-digest.js";
|
||||
|
||||
export function hashSkillProposalContent(content: string): string {
|
||||
return sha256Hex(content);
|
||||
}
|
||||
@@ -135,3 +135,40 @@ export async function readSkillProposalRollback(
|
||||
...(row.support_files_json ? { supportFiles: parseJson(row.support_files_json) } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearSkillProposalRollback(params: {
|
||||
proposalId: string;
|
||||
expectedRecordJson: string;
|
||||
store?: SkillWorkshopStoreOptions;
|
||||
}): Promise<boolean> {
|
||||
assertProposalId(params.proposalId);
|
||||
ensureSkillWorkshopSchema(params.store);
|
||||
return runOpenClawStateWriteTransaction(
|
||||
({ db }) => {
|
||||
const kysely = getNodeSqliteKysely<SkillWorkshopDatabase>(db);
|
||||
const proposal = executeSqliteQueryTakeFirstSync(
|
||||
db,
|
||||
kysely
|
||||
.selectFrom("skill_workshop_proposals")
|
||||
.select(["record_json", "status"])
|
||||
.where("proposal_id", "=", params.proposalId),
|
||||
);
|
||||
if (
|
||||
!proposal ||
|
||||
proposal.status !== "pending" ||
|
||||
proposal.record_json !== params.expectedRecordJson
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
kysely
|
||||
.deleteFrom("skill_workshop_proposal_rollbacks")
|
||||
.where("proposal_id", "=", params.proposalId),
|
||||
);
|
||||
return true;
|
||||
},
|
||||
databaseOptions(params.store),
|
||||
{ operationLabel: "skill-workshop.rollback.clear" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { executeSqliteQueryTakeFirstSync, getNodeSqliteKysely } from "../../infra/kysely-sync.js";
|
||||
import { runOpenClawStateWriteTransaction } from "../../state/openclaw-state-db.js";
|
||||
import { appendSkillProposalEvent, type NewSkillProposalEvent } from "./store-sqlite-event.js";
|
||||
import { parseSkillProposalRow, updateProposal } from "./store-sqlite-record.js";
|
||||
import {
|
||||
databaseOptions,
|
||||
ensureSkillWorkshopSchema,
|
||||
type SkillWorkshopDatabase,
|
||||
type SkillWorkshopStoreOptions,
|
||||
} from "./store-sqlite-schema.js";
|
||||
import type { SkillProposalEvent, SkillProposalRecord } from "./types.js";
|
||||
|
||||
export type PendingSkillProposalTransitionCommit =
|
||||
| { state: "committed"; event?: SkillProposalEvent }
|
||||
| { state: "conflict"; current?: SkillProposalRecord };
|
||||
|
||||
export function commitPendingSkillProposalTransition(params: {
|
||||
expected: SkillProposalRecord;
|
||||
record: SkillProposalRecord;
|
||||
event?: NewSkillProposalEvent;
|
||||
store?: SkillWorkshopStoreOptions;
|
||||
operationLabel: string;
|
||||
}): PendingSkillProposalTransitionCommit {
|
||||
ensureSkillWorkshopSchema(params.store);
|
||||
return runOpenClawStateWriteTransaction(
|
||||
({ db }) => {
|
||||
const kysely = getNodeSqliteKysely<SkillWorkshopDatabase>(db);
|
||||
const current = executeSqliteQueryTakeFirstSync(
|
||||
db,
|
||||
kysely
|
||||
.selectFrom("skill_workshop_proposals")
|
||||
.selectAll()
|
||||
.where("proposal_id", "=", params.expected.id),
|
||||
);
|
||||
const currentRecord = current ? parseSkillProposalRow(current) : null;
|
||||
if (
|
||||
!current ||
|
||||
!currentRecord ||
|
||||
currentRecord.status !== "pending" ||
|
||||
current.record_json !== JSON.stringify(params.expected)
|
||||
) {
|
||||
return {
|
||||
state: "conflict" as const,
|
||||
...(currentRecord ? { current: currentRecord } : {}),
|
||||
};
|
||||
}
|
||||
updateProposal(db, current, params.record);
|
||||
return {
|
||||
state: "committed" as const,
|
||||
...(params.event ? { event: appendSkillProposalEvent(db, params.event) } : {}),
|
||||
};
|
||||
},
|
||||
databaseOptions(params.store),
|
||||
{ operationLabel: params.operationLabel },
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { resolveStateDir } from "../../config/paths.js";
|
||||
import { sha256Hex } from "../../infra/crypto-digest.js";
|
||||
import { removePathWithinRoot } from "../../infra/fs-safe-remove.js";
|
||||
import { root } from "../../infra/fs-safe.js";
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
getNodeSqliteKysely,
|
||||
} from "../../infra/kysely-sync.js";
|
||||
import { runOpenClawStateWriteTransaction } from "../../state/openclaw-state-db.js";
|
||||
import { withOpenClawStateLease } from "../../state/openclaw-state-lease.js";
|
||||
import { normalizeSkillIndexName } from "../discovery/skill-index.js";
|
||||
import {
|
||||
assertInsideWorkspace,
|
||||
@@ -21,6 +19,7 @@ import {
|
||||
readWorkspaceSupportFile,
|
||||
} from "../lifecycle/workspace-skill-write.js";
|
||||
import { stripProposalFrontmatterForSkill } from "./frontmatter.js";
|
||||
import { hashSkillProposalContent } from "./proposal-hash.js";
|
||||
import { hashSkillProposalRevision } from "./revision-hash.js";
|
||||
import {
|
||||
assertProposalId,
|
||||
@@ -60,14 +59,18 @@ const WORKSHOP_REL_DIR = "skill-workshop";
|
||||
const PROPOSALS_REL_DIR = path.join(WORKSHOP_REL_DIR, "proposals");
|
||||
const MAX_PROPOSAL_BYTES = 1024 * 1024;
|
||||
const MAX_PROPOSAL_SUPPORT_FILES_TOTAL_BYTES = 2 * 1024 * 1024;
|
||||
const TARGET_LEASE_MS = 60_000;
|
||||
const TARGET_LEASE_WAIT_MS = 5_000;
|
||||
export {
|
||||
MAX_PROPOSAL_SUPPORT_FILES,
|
||||
validateSkillProposalRecord,
|
||||
validateSkillProposalRollback,
|
||||
} from "./store-record.js";
|
||||
export { readSkillProposalRollback, writeSkillProposalRollback } from "./store-sqlite-rollback.js";
|
||||
export { hashSkillProposalContent } from "./proposal-hash.js";
|
||||
export {
|
||||
clearSkillProposalRollback,
|
||||
readSkillProposalRollback,
|
||||
writeSkillProposalRollback,
|
||||
} from "./store-sqlite-rollback.js";
|
||||
export { withSkillProposalTargetLock } from "./target-lock.js";
|
||||
|
||||
type SkillProposalLookupScope = {
|
||||
agentId?: string;
|
||||
@@ -86,10 +89,6 @@ export function createSkillProposalId(name: string, now = new Date()): string {
|
||||
return `${normalized.slice(0, 60)}-${date}-${suffix}`;
|
||||
}
|
||||
|
||||
export function hashSkillProposalContent(content: string): string {
|
||||
return sha256Hex(content);
|
||||
}
|
||||
|
||||
function contentSizeBytes(content: string): number {
|
||||
return Buffer.byteLength(content, "utf8");
|
||||
}
|
||||
@@ -370,26 +369,6 @@ export async function updateSkillProposalRecord(params: {
|
||||
);
|
||||
}
|
||||
|
||||
export async function withSkillProposalTargetLock<T>(
|
||||
record: SkillProposalRecord,
|
||||
fn: () => Promise<T>,
|
||||
options: SkillWorkshopStoreOptions = {},
|
||||
): Promise<T> {
|
||||
ensureSkillWorkshopSchema(options);
|
||||
return await withOpenClawStateLease(
|
||||
{
|
||||
scope: "skill-workshop-target",
|
||||
key: hashSkillProposalContent(record.target.skillFile),
|
||||
database: { scope: "shared", options: databaseOptions(options) },
|
||||
leaseMs: TARGET_LEASE_MS,
|
||||
waitMs: TARGET_LEASE_WAIT_MS,
|
||||
leaseLabel: "Skill Workshop target lease",
|
||||
operationLabel: "skill-workshop.target-lease",
|
||||
},
|
||||
async () => await fn(),
|
||||
);
|
||||
}
|
||||
|
||||
function listStoredProposals(
|
||||
options: SkillWorkshopStoreOptions,
|
||||
scope: SkillProposalLookupScope,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { withOpenClawStateLease } from "../../state/openclaw-state-lease.js";
|
||||
import { hashSkillProposalContent } from "./proposal-hash.js";
|
||||
import {
|
||||
databaseOptions,
|
||||
ensureSkillWorkshopSchema,
|
||||
type SkillWorkshopStoreOptions,
|
||||
} from "./store-sqlite-schema.js";
|
||||
import type { SkillProposalRecord } from "./types.js";
|
||||
|
||||
const TARGET_LEASE_MS = 60_000;
|
||||
const TARGET_LEASE_WAIT_MS = 5_000;
|
||||
|
||||
export async function withSkillProposalTargetLock<T>(
|
||||
record: SkillProposalRecord,
|
||||
fn: () => Promise<T>,
|
||||
options: SkillWorkshopStoreOptions = {},
|
||||
): Promise<T> {
|
||||
ensureSkillWorkshopSchema(options);
|
||||
return await withOpenClawStateLease(
|
||||
{
|
||||
scope: "skill-workshop-target",
|
||||
key: hashSkillProposalContent(record.target.skillFile),
|
||||
database: { scope: "shared", options: databaseOptions(options) },
|
||||
leaseMs: TARGET_LEASE_MS,
|
||||
waitMs: TARGET_LEASE_WAIT_MS,
|
||||
leaseLabel: "Skill Workshop target lease",
|
||||
operationLabel: "skill-workshop.target-lease",
|
||||
},
|
||||
async () => await fn(),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user