openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
380 lines (379 loc) • 15.8 kB
JavaScript
import { l as normalizeOptionalString } from "./string-coerce-6kR-86XC.mjs";
import { A as readProposalFrontmatter, E as dispatchSkillProposalChanged, P as stripProposalFrontmatterForSkill, S as writeSkillProposalRollback, T as createSkillProposalEvent, _ as withSkillProposalTargetLock, b as clearSkillProposalRollback, g as withSkillProposalCommitLock, v as commitPendingSkillProposalTransition, w as readSkillProposalTargetTreeSha256, y as readCommittedSkillProposalTransition } from "./store-C0u12Mn-.mjs";
import { a as isWorkspaceSkillMutationApplied, c as prepareWorkspaceSkillMutation, f as restoreWorkspaceSkillMutation, n as applyWorkspaceSkillMutation, o as isWorkspaceSkillMutationRestored, r as assertInsideSkillsRoot, u as readWorkspaceSkillFile } from "./workspace-skill-write-CN7nTQwy.mjs";
import { i as snapshotCommittedSkillArtifactBestEffort, n as hasCommittedSkillChangeHooks, t as dispatchCommittedSkillChangeBestEffort } from "./skill-change-hook-nCwgL8Am.mjs";
import { t as bumpSkillsSnapshotVersion } from "./refresh-state-Cazo5oir.mjs";
import { n as hashSkillProposalRevision, t as hashSkillProposalContent } from "./proposal-hash-DFkV3A6F.mjs";
import { n as scanProposalBundle } from "./proposal-scan-CDjWnmRS.mjs";
import { t as resolveWorkshopSkillsDir } from "./skills-root-BZ-eiihY.mjs";
import { f as SKILL_WORKSHOP_ROLLBACK_SCHEMA } from "./store-record-DYY-eG78.mjs";
import { i as readStoredProposal, t as captureSkillWorkshopStoreOptions } from "./store-client-DQGJiPsl.mjs";
//#region src/skills/workshop/apply-transition.ts
var SkillProposalLifecycleError = class extends Error {
constructor(message, record, event) {
super(message);
this.record = record;
this.event = event;
}
};
async function applySkillProposalTransition(request, dependencies) {
const store = captureSkillWorkshopStoreOptions(storeOptions(request.env, request.agentId, request.config));
const input = {
...request,
env: store.env,
eventActor: structuredClone(request.eventActor)
};
const recoveryReadOptions = {
config: input.config,
store
};
const lockedReadOptions = {
config: input.config,
reconcile: false
};
const initial = await dependencies.readRequiredProposal(input.proposalId, input.env, input.agentId, recoveryReadOptions);
if (initial.record.status !== "pending") throw new Error(`Only pending proposals can be applied. Current status: ${initial.record.status}.`);
dependencies.assertExpectedRevisionHash(initial.revisionHash, input.expectedRevisionHash);
let evaluated;
try {
evaluated = await dependencies.evaluateSkillProposal({
workspaceDir: input.workspaceDir,
...input.agentId ? { agentId: input.agentId } : {},
config: input.config,
...input.eventActor ? { eventActor: input.eventActor } : {},
...input.env ? { env: input.env } : {},
proposalId: input.proposalId,
expectedRevisionHash: initial.revisionHash,
...input.correlationId ? { correlationId: input.correlationId } : {},
trigger: "apply"
}, store);
} catch (error) {
if (dependencies.isCreateTargetConflict(error)) await withSkillProposalLifecycleDispatch(input, withSkillProposalTargetLock(initial.record, async (lockedStore) => {
const current = await dependencies.readRequiredProposal(input.proposalId, input.env, input.agentId, {
...lockedReadOptions,
store: lockedStore
});
if (current.record.status === "pending" && current.record.kind === "create" && await readWorkspaceSkillFile(current.record.target.skillFile) !== null) await markSkillProposalStale({
store: lockedStore,
record: current.record,
reason: "Target skill was created after proposal creation.",
message: "Target skill was created after proposal creation; proposal marked stale.",
input
});
throw error;
}, store));
throw error;
}
const blocking = evaluated.evaluation.outcomes.find((outcome) => outcome.status === "completed" && outcome.result.decision === "block");
if (blocking?.status === "completed") throw new Error(blocking.result.decisionReason || `Skill proposal apply blocked by evaluator ${blocking.evaluatorId}.`);
const result = await withSkillProposalLifecycleDispatch(input, withSkillProposalCommitLock(evaluated.record, async (lockedStore) => {
const read = await dependencies.readRequiredProposal(input.proposalId, input.env, input.agentId, {
...lockedReadOptions,
store: lockedStore
});
const { record, content } = read;
if (record.status !== "pending") throw new Error(`Only pending proposals can be applied. Current status: ${record.status}.`);
dependencies.assertExpectedRevisionHash(read.revisionHash, evaluated.evaluation.revisionHash);
if (hashSkillProposalContent(content) !== record.draftHash) throw new Error("Proposal draft changed without updating proposal metadata.");
const supportFiles = read.supportFiles ?? [];
if (!readProposalFrontmatter(content)) throw new Error("Proposal draft must include proposal frontmatter.");
const scan = scanProposalBundle(content, supportFiles);
if (scan.state !== "clean") await quarantineSkillProposalAfterScan({
input,
record,
scan,
store: lockedStore
});
if (!input.agentId) throw new Error("Skill Workshop requires the active agent id.");
const skillsRoot = resolveWorkshopSkillsDir(input.config, input.agentId, input.env);
assertInsideSkillsRoot(skillsRoot, record.target.skillFile, "skill file");
assertInsideSkillsRoot(skillsRoot, record.target.skillDir, "skill directory");
if (record.evaluation?.id !== evaluated.evaluation.id) throw new Error("Skill proposal evaluation changed before apply; retry the operation.");
if (evaluated.evaluation.targetTreeSha256) {
let currentTargetTreeSha256;
try {
currentTargetTreeSha256 = await readSkillProposalTargetTreeSha256(record.target.skillDir);
} catch {
throw new Error("Skill target changed after evaluation; retry the operation.");
}
if (currentTargetTreeSha256 !== evaluated.evaluation.targetTreeSha256) throw new Error("Skill target changed after evaluation; retry the operation.");
}
const mutation = await prepareWorkspaceSkillMutation({
skillsRoot,
skillDir: record.target.skillDir,
skillFile: record.target.skillFile,
content: stripProposalFrontmatterForSkill(content),
supportFiles,
mode: record.kind
});
await assertApplyTargetUnchanged(record, mutation, input, lockedStore);
const shouldDispatchSkillChange = hasCommittedSkillChangeHooks();
const beforeSkill = shouldDispatchSkillChange && record.kind === "update" ? await snapshotCommittedSkillArtifactBestEffort({
skillDir: record.target.skillDir,
skillKey: record.target.skillKey,
source: "workshop"
}) : void 0;
const rollback = createSkillProposalRollbackFromMutation(record, mutation);
await writeSkillProposalRollback({
proposalId: record.id,
rollback,
store: lockedStore
});
try {
await applyWorkspaceSkillMutation(mutation);
} catch (error) {
if (await isWorkspaceSkillMutationRestored(mutation).catch(() => false)) await clearSkillProposalRollback({
proposalId: record.id,
expectedRecordJson: JSON.stringify(record),
store: lockedStore
}).catch(() => false);
throw error;
}
const afterSkill = shouldDispatchSkillChange ? await snapshotCommittedSkillArtifactBestEffort({
skillDir: record.target.skillDir,
skillKey: record.target.skillKey,
source: "workshop",
sourceVersion: record.proposedVersion
}) : void 0;
const now = (/* @__PURE__ */ new Date()).toISOString();
const applied = {
...record,
status: "applied",
updatedAt: now,
appliedAt: now,
statusReason: normalizeOptionalString(input.reason),
scan
};
const eventInput = createSkillProposalEvent({
record: applied,
type: "applied",
actor: input.eventActor,
...input.correlationId ? { correlationId: input.correlationId } : {},
occurredAt: now,
payload: { targetSkillFile: record.target.skillFile }
});
let commit;
try {
commit = await commitPendingSkillProposalTransition({
expected: record,
record: applied,
event: eventInput,
store: lockedStore,
operationLabel: "skill-workshop.apply.commit"
});
if (commit.state === "conflict") throw new Error("Skill proposal changed before apply status commit.");
} catch (error) {
const recoveredEvent = await recoverAfterApplyCommitFailure({
error,
store: lockedStore,
expected: record,
applied,
event: eventInput,
mutation
});
if (!recoveredEvent) throw error;
commit = {
state: "committed",
event: recoveredEvent
};
}
bumpSkillsSnapshotVersion({
reason: "workshop",
changedPath: record.target.skillFile
});
return {
result: {
record: applied,
targetSkillFile: record.target.skillFile
},
event: commit.event,
skillChange: shouldDispatchSkillChange ? {
before: beforeSkill,
after: afterSkill
} : void 0
};
}, store));
await dispatchSkillProposalChanged({
event: result.event,
record: result.result.record,
workspaceDir: input.workspaceDir,
...input.agentId ? { agentId: input.agentId } : {}
});
if (result.skillChange) await dispatchCommittedSkillChangeBestEffort({
action: result.result.record.kind === "create" ? "created" : "updated",
source: "workshop",
workspaceDir: input.workspaceDir,
before: result.skillChange.before,
after: result.skillChange.after,
proposal: {
id: result.result.record.id,
revision: result.result.record.proposedVersion,
revisionSha256: hashSkillProposalRevision(result.result.record)
}
});
return result.result;
}
async function withSkillProposalLifecycleDispatch(input, operation) {
try {
return await operation;
} catch (error) {
if (error instanceof SkillProposalLifecycleError) await dispatchSkillProposalChanged({
event: error.event,
record: error.record,
workspaceDir: input.workspaceDir,
...input.agentId ? { agentId: input.agentId } : {}
});
throw error;
}
}
async function assertSkillProposalSupportTargetUnchanged(params) {
const { record, file, currentContent } = params;
if (file.targetExisted === false ? currentContent !== null : file.targetExisted === true && (currentContent === null ? void 0 : hashSkillProposalContent(currentContent)) !== file.targetContentHash) await markSkillProposalStale({
store: params.store,
record,
reason: `Target support file changed after proposal creation: ${file.path}`,
message: "Target support file changed after proposal creation; proposal marked stale.",
input: params.input
});
}
async function transitionPendingSkillProposalToStale(params) {
const now = (/* @__PURE__ */ new Date()).toISOString();
const stale = {
...params.record,
status: "stale",
updatedAt: now,
staleAt: now,
statusReason: params.reason
};
const commit = await commitPendingSkillProposalTransition({
expected: params.record,
record: stale,
event: createSkillProposalEvent({
record: stale,
type: "stale",
actor: params.input.eventActor,
...params.input.correlationId ? { correlationId: params.input.correlationId } : {},
occurredAt: now
}),
store: params.store ?? storeOptions(params.input.env, params.input.agentId, params.input.config),
operationLabel: "skill-workshop.stale.commit"
});
if (commit.state !== "committed") throw new Error("Failed to record stale Skill Workshop proposal.");
return {
record: stale,
event: commit.event
};
}
async function markSkillProposalStale(params) {
const transition = await transitionPendingSkillProposalToStale(params);
throw new SkillProposalLifecycleError(params.message, transition.record, transition.event);
}
async function quarantineSkillProposalAfterScan(params) {
const now = (/* @__PURE__ */ new Date()).toISOString();
const updated = {
...params.record,
status: "quarantined",
updatedAt: now,
quarantinedAt: now,
scan: {
...params.scan,
state: "quarantined"
},
statusReason: "Proposal scan failed."
};
const commit = await commitPendingSkillProposalTransition({
expected: params.record,
record: updated,
event: createSkillProposalEvent({
record: updated,
type: "quarantined",
actor: params.input.eventActor,
...params.input.correlationId ? { correlationId: params.input.correlationId } : {},
occurredAt: now
}),
store: params.store ?? storeOptions(params.input.env, params.input.agentId, params.input.config),
operationLabel: "skill-workshop.quarantine.commit"
});
if (commit.state !== "committed") throw new Error("Failed to record quarantined Skill Workshop proposal.");
throw new SkillProposalLifecycleError("Proposal scan failed; proposal was quarantined.", updated, commit.event);
}
async function assertApplyTargetUnchanged(record, mutation, input, store) {
if (record.kind === "update" && record.target.currentContentHash && mutation.skillFile.previousContent !== null && hashSkillProposalContent(mutation.skillFile.previousContent) !== record.target.currentContentHash) await markSkillProposalStale({
store,
record,
reason: "Target skill changed after proposal creation.",
message: "Target skill changed after proposal creation; proposal marked stale.",
input
});
for (const file of mutation.supportFiles) {
const supportRecord = record.supportFiles?.find((entry) => entry.path === file.path);
if (record.kind === "update" && supportRecord) await assertSkillProposalSupportTargetUnchanged({
store,
record,
file: supportRecord,
currentContent: file.previousContent,
input
});
}
}
function createSkillProposalRollbackFromMutation(record, mutation) {
return {
schema: SKILL_WORKSHOP_ROLLBACK_SCHEMA,
proposalId: record.id,
writtenAt: (/* @__PURE__ */ new Date()).toISOString(),
targetSkillFile: record.target.skillFile,
action: record.kind,
...mutation.skillFile.previousContent !== null ? {
previousContent: mutation.skillFile.previousContent,
previousContentHash: hashSkillProposalContent(mutation.skillFile.previousContent)
} : {},
...mutation.supportFiles.length > 0 ? { supportFiles: mutation.supportFiles.map((file) => file.previousContent === null ? {
path: file.path,
existed: false
} : {
path: file.path,
existed: true,
previousContent: file.previousContent,
previousContentHash: hashSkillProposalContent(file.previousContent)
}) } : {}
};
}
async function recoverAfterApplyCommitFailure(params) {
const committed = await readCommittedSkillProposalTransition({
record: params.applied,
event: params.event,
store: params.store
});
if (committed) return committed.event;
if ((await readStoredProposal(params.expected.id, params.store))?.record.status === "applied") throw new Error("Applied Skill Workshop transition is missing its committed event.", { cause: params.error });
if (!await isWorkspaceSkillMutationApplied(params.mutation).catch(() => false)) return null;
try {
try {
await restoreWorkspaceSkillMutation(params.mutation);
} finally {
bumpSkillsSnapshotVersion({
reason: "workshop",
changedPath: params.expected.target.skillFile
});
}
} catch (restoreError) {
const failure = new Error("Skill proposal apply failed after filesystem mutation and requires reconciliation.", { cause: params.error });
Object.assign(failure, { restoreError });
throw failure;
}
await clearSkillProposalRollback({
proposalId: params.expected.id,
expectedRecordJson: JSON.stringify(params.expected),
store: params.store
}).catch(() => false);
return null;
}
function storeOptions(env, agentId, config) {
return {
...env ? { env } : {},
...agentId ? { agentId } : {},
config
};
}
//#endregion
export { withSkillProposalLifecycleDispatch as a, transitionPendingSkillProposalToStale as i, assertSkillProposalSupportTargetUnchanged as n, markSkillProposalStale as r, applySkillProposalTransition as t };