refactor(state): register transcript, skill and runtime worker operations (#162524)

* refactor(state): register worker operations once per domain

Infer shared-state worker command contracts from lazy per-domain handler tables. Migrate Web Push, APNs, worktree registry operations, and fleet registry while preserving the existing broker and transaction owners.

* refactor(state): register transcript and skill worker operations

Infer shared-state operation contracts from lazy handler tables for transcripts, skills, ACP, auth profiles, and plugin runtime state. Preserve native transactions, worker admission, lease checks, receipts, and existing update behavior.

* fix(state): break registry type import cycles

Keep inferred worker contracts downstream of leaf ACP and lease data types. Separate host lease dispatch from native lifecycle storage while preserving captured admission and package replacement preloading.
This commit is contained in:
Peter Steinberger 2026-10-01 03:05:44 -07:00 • committed by GitHub
parent 21eeb7c932
commit 5b4fdfcb22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 771 additions and 911 deletions

View file

@ -1,7 +1,9 @@
/** Owns each admitted turn from actor admission through final settlement. */
import { createDeferredCore } from "../../shared/deferred.js";
import type { AcpSessionRuntimeLocator } from "../runtime/session-control-owner.js";
import type { AcpSessionControlConstraint } from "../runtime/session-meta-control.types.js";
import type {
AcpSessionRuntimeLocator,
AcpSessionControlConstraint,
} from "../runtime/session-meta-control.types.js";
import type {
AcpRunTurnInput,
ActiveTurnState,

View file

@ -7,8 +7,8 @@ import {
import {
matchesAcpSessionRuntimeLocator,
resolveAcpSessionControlOwner,
type AcpSessionRuntimeLocator,
} from "../runtime/session-control-owner.js";
import type { AcpSessionRuntimeLocator } from "../runtime/session-meta-control.types.js";
import type { AcceptedTurnState, AcceptedTurns } from "./manager.accepted-turns.js";
import type { ManagerRuntimeHandleCache } from "./manager.runtime-handle-cache.js";
import type {

View file

@ -17,8 +17,8 @@ import {
import {
matchesAcpSessionControlBinding,
resolveAcpSessionControlOwner,
type AcpSessionRuntimeLocator,
} from "../runtime/session-control-owner.js";
import type { AcpSessionRuntimeLocator } from "../runtime/session-meta-control.types.js";
import { assertAcpSessionMutationEntry } from "../runtime/session-meta-entry.kernel.js";
import type { ManagerRuntimeHandleCache } from "./manager.runtime-handle-cache.js";
import {

View file

@ -4,7 +4,7 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { logVerbose } from "../../globals.js";
import { formatErrorMessage, toErrorObject } from "../../infra/errors.js";
import type { AcpRuntimeError } from "../runtime/errors.js";
import type { AcpSessionControlBinding } from "../runtime/session-control-owner.js";
import type { AcpSessionControlBinding } from "../runtime/session-meta-control.types.js";
import type { ManagerRuntimeHandleCache } from "./manager.runtime-handle-cache.js";
import {
assertAcpRuntimeOwnerSupport,

View file

@ -21,9 +21,9 @@ import type { AcpRuntimeError } from "../runtime/errors.js";
import { getAcpRuntimeBackend, requireAcpRuntimeBackend } from "../runtime/registry.js";
import type {
AcpSessionControlBinding,
AcpSessionControlConstraint,
AcpSessionRuntimeLocator,
} from "../runtime/session-control-owner.js";
import type { AcpSessionControlConstraint } from "../runtime/session-meta-control.types.js";
} from "../runtime/session-meta-control.types.js";
import {
listAcpSessionEntries,
readAcpSessionEntry,

View file

@ -1,8 +1,8 @@
import type { SessionAcpMeta, SessionEntry } from "../../config/sessions/types.js";
export type AcpSessionRuntimeLocator = Readonly<
Pick<SessionAcpMeta, "backend" | "runtimeSessionName">
>;
import type { SessionEntry } from "../../config/sessions/types.js";
import type {
AcpSessionRuntimeLocator,
AcpSessionControlBinding,
} from "./session-meta-control.types.js";
/** Runtime names are opaque backend locators; ordinary metadata enrichment may continue. */
export function matchesAcpSessionRuntimeLocator(
@ -22,14 +22,6 @@ export function resolveAcpSessionControlOwner(
return entry?.spawnedBy?.trim() || entry?.parentSessionKey?.trim();
}
/** A cleanup target constraint; live task and actor authority remain separate. */
export type AcpSessionControlBinding = Readonly<{
sessionId: string;
lifecycleRevision?: string;
sessionStartedAt?: number;
ownerKey: string;
}>;
export function matchesAcpSessionControlBinding(
entry: SessionEntry | undefined,
expected: AcpSessionControlBinding,

View file

@ -7,9 +7,11 @@ import { captureOpenClawStateReadContext } from "../../state/openclaw-state-work
import {
matchesAcpSessionRuntimeLocator,
resolveAcpSessionControlOwner,
type AcpSessionRuntimeLocator,
} from "./session-control-owner.js";
import type { AcpSessionControlConstraint } from "./session-meta-control.types.js";
import type {
AcpSessionRuntimeLocator,
AcpSessionControlConstraint,
} from "./session-meta-control.types.js";
import {
assertAcpSessionMutationEntry,
captureAcpSessionEntryBinding,

View file

@ -1,10 +1,18 @@
import type { SessionEntry } from "../../config/sessions/types.js";
import type { SessionAcpMeta, SessionEntry } from "../../config/sessions/types.js";
import type { DatabasePathIdentity } from "../../infra/sqlite-worker-identity.js";
import type {
AcpSessionControlBinding,
AcpSessionRuntimeLocator,
} from "./session-control-owner.js";
import type { AcpSessionReadInput } from "./session-meta-keys.js";
import type { AcpSessionReadInput } from "./session-meta-read.types.js";
export type AcpSessionRuntimeLocator = Readonly<
Pick<SessionAcpMeta, "backend" | "runtimeSessionName">
>;
/** A cleanup target constraint; live task and actor authority remain separate. */
export type AcpSessionControlBinding = Readonly<{
sessionId: string;
lifecycleRevision?: string;
sessionStartedAt?: number;
ownerKey: string;
}>;
export type AcpSessionSourceReadInput = {
source: {

View file

@ -31,8 +31,8 @@ import {
selectAcpSessionRow,
selectLegacyFreeAcpSessionRows,
upsertAcpSessionMetaRow,
type AcpSessionRow,
} from "./session-meta-keys.js";
import type { AcpSessionRow } from "./session-meta-read.types.js";
import { rowToAcpSessionMeta } from "./session-meta-readonly.js";
import { resolveSessionStorePathForAcp } from "./session-meta-store.js";

View file

@ -1,8 +1,6 @@
import type { SessionEntry } from "../../config/sessions/types.js";
import {
matchesAcpSessionControlBinding,
type AcpSessionControlBinding,
} from "./session-control-owner.js";
import { matchesAcpSessionControlBinding } from "./session-control-owner.js";
import type { AcpSessionControlBinding } from "./session-meta-control.types.js";
export type AcpSessionEntryExpectation = Pick<
SessionEntry,

View file

@ -15,7 +15,7 @@ import type { SqliteWorkerOperationAdmission } from "../../infra/sqlite-worker-o
import type { RetainedWorkerTransactionAdmission } from "../../infra/sqlite-worker-operation-settlement.js";
import type { OpenClawAgentDatabaseOptions } from "../../state/openclaw-agent-db.js";
import type { OpenClawAgentDatabaseExecution } from "../../state/openclaw-agent-execution.js";
import type { AcpSessionControlBinding } from "./session-control-owner.js";
import type { AcpSessionControlBinding } from "./session-meta-control.types.js";
import {
captureAcpSessionEntryBinding,
type AcpSessionEntryExpectation,

View file

@ -1,6 +1,6 @@
import type { SessionEntryReplacementPublication } from "../../config/sessions/session-accessor.sqlite-entry-cache.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { AcpSessionControlBinding } from "./session-control-owner.js";
import type { AcpSessionControlBinding } from "./session-meta-control.types.js";
import type { AcpSessionEntryExpectation } from "./session-meta-entry.kernel.js";
export type AcpSessionEntryMutationInput = {

View file

@ -1,9 +1,8 @@
import type { DatabaseSync } from "node:sqlite";
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
import type { Insertable, Selectable } from "kysely";
import type { Insertable } from "kysely";
import { tryResolveLegacyDataOwnerAgentId } from "../../agents/agent-scope-config.js";
import { resolvePersistedSessionStoreOwnerForKey } from "../../config/sessions/session-store-owner.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
executeSqliteQuerySync,
@ -12,17 +11,14 @@ import {
} from "../../infra/kysely-sync.js";
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
import type { DB as OpenClawStateKyselyDatabase } from "../../state/openclaw-state-db.generated.js";
import type {
AcpSessionsTable,
AcpSessionRow,
AcpSessionEntryBinding,
AcpSessionReadInput,
} from "./session-meta-read.types.js";
export type AcpSessionsTable = OpenClawStateKyselyDatabase["acp_sessions"];
type AcpSessionMetaDatabase = Pick<OpenClawStateKyselyDatabase, "acp_sessions">;
export type AcpSessionRow = Selectable<AcpSessionsTable>;
export type AcpSessionEntryBinding = Pick<SessionEntry, "lifecycleRevision"> &
Partial<Pick<SessionEntry, "sessionId" | "sessionStartedAt">>;
export type AcpSessionReadInput = {
keys: readonly string[];
legacyKey?: string;
entry?: AcpSessionEntryBinding;
};
export function getAcpSessionKysely(db: DatabaseSync) {
return getNodeSqliteKysely<AcpSessionMetaDatabase>(db);

View file

@ -1,5 +1,5 @@
import { patchSessionEntryWithKey } from "../../config/sessions/session-accessor.js";
import type { AcpSessionControlBinding } from "./session-control-owner.js";
import type { AcpSessionControlBinding } from "./session-meta-control.types.js";
import {
assertAcpSessionMutationEntry,
type AcpSessionEntryExpectation,

View file

@ -0,0 +1,13 @@
import type { Selectable } from "kysely";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { DB as OpenClawStateKyselyDatabase } from "../../state/openclaw-state-db.generated.js";
export type AcpSessionsTable = OpenClawStateKyselyDatabase["acp_sessions"];
export type AcpSessionRow = Selectable<AcpSessionsTable>;
export type AcpSessionEntryBinding = Pick<SessionEntry, "lifecycleRevision"> &
Partial<Pick<SessionEntry, "sessionId" | "sessionStartedAt">>;
export type AcpSessionReadInput = {
keys: readonly string[];
legacyKey?: string;
entry?: AcpSessionEntryBinding;
};

View file

@ -11,14 +11,13 @@ import {
withExistingOpenClawStateDatabaseCurrentReadOnly,
} from "../../state/openclaw-state-db-readonly.js";
import {
type AcpSessionEntryBinding,
type AcpSessionRow,
buildAcpDatabaseSessionKey,
legacyAcpDatabaseSessionKeys,
resolveLegacyFreeAcpSessionKey,
resolveReadableAcpSessionRow,
selectAcpSessionRowForStoreEntry,
} from "./session-meta-keys.js";
import type { AcpSessionEntryBinding, AcpSessionRow } from "./session-meta-read.types.js";
/** Each result stays bound to the entry lifecycle captured by the row reader. */
export async function readAcpSessionMetaForEntries(

View file

@ -10,8 +10,8 @@ import {
selectAcpSessionRow,
selectLegacyFreeAcpSessionRows,
upsertAcpSessionMetaRow,
type AcpSessionsTable,
} from "./session-meta-keys.js";
import type { AcpSessionsTable } from "./session-meta-read.types.js";
import type { AcpSessionMutationCommit } from "./session-meta-write.types.js";
export function bindAcpSessionMeta(params: {

View file

@ -15,7 +15,7 @@ import {
} from "../../infra/legacy-acp-migration-source.js";
import { sessionChanges } from "../../sessions/session-row-changes.js";
import { runOpenClawStateWriteTransaction } from "../../state/openclaw-state-db.js";
import type { AcpSessionControlBinding } from "./session-control-owner.js";
import type { AcpSessionControlBinding } from "./session-meta-control.types.js";
import { assertAcpSessionMutationEntry } from "./session-meta-entry.kernel.js";
import { selectAcpSessionRowForStoreEntry } from "./session-meta-keys.js";
import { clearLegacyEmbeddedAcpMetadata } from "./session-meta-legacy-cleanup.js";

View file

@ -1,10 +1,10 @@
import type { SessionAcpMeta, SessionEntry } from "../../config/sessions/types.js";
import type { AcpSessionControlBinding } from "./session-control-owner.js";
import type {
AcpSessionControlBinding,
AcpSessionControlConstraint,
AcpSessionSourceReadInput,
} from "./session-meta-control.types.js";
import type { AcpSessionReadInput } from "./session-meta-keys.js";
import type { AcpSessionReadInput } from "./session-meta-read.types.js";
export type AcpSessionMutationDecision =
| { kind: "keep" }
@ -33,23 +33,14 @@ export type AcpSessionMutationCommit = {
control?: AcpSessionControlConstraint;
};
export type AcpSessionWriteOperations = {
"acp.prepareMutation": {
input: {
nonce: string;
read: AcpSessionReadInput;
entry?: SessionEntry;
updatedAt: number;
source: AcpSessionSourceReadInput["source"];
sessionKey: string;
agentId: string;
expectedControlBinding?: AcpSessionControlBinding;
control?: AcpSessionControlConstraint;
};
output: AcpSessionMutationPreparation;
};
"acp.commitMutation": {
input: AcpSessionMutationCommit & { nonce: string };
output: { nonce: string };
};
export type AcpSessionMutationPrepareInput = {
nonce: string;
read: AcpSessionReadInput;
entry?: SessionEntry;
updatedAt: number;
source: AcpSessionSourceReadInput["source"];
sessionKey: string;
agentId: string;
expectedControlBinding?: AcpSessionControlBinding;
control?: AcpSessionControlConstraint;
};

View file

@ -0,0 +1,4 @@
import type { WorkerOperations } from "../../state/worker-operation-registry.js";
import type { acpSessionOperations } from "./session-meta-write.worker.js";
export type AcpSessionWriteOperations = WorkerOperations<typeof acpSessionOperations>;

View file

@ -5,7 +5,6 @@ import {
legacyAcpMigrationBindingMatches,
recordLegacyAcpMigrationCompletion,
} from "../../infra/legacy-acp-migration-source.js";
import type { SqliteWorkerCommand } from "../../infra/sqlite-worker-contract.js";
import {
deferSqliteWorkerCommitReceipt,
requestSqliteWorkerOperationAdmission,
@ -15,6 +14,7 @@ import {
runOpenClawStateWriteTransaction,
type OpenClawStateDatabase,
} from "../../state/openclaw-state-db.js";
import type { WorkerOperationHandlers } from "../../state/worker-operation-registry.js";
import type { AcpSessionControlConstraint } from "./session-meta-control.types.js";
import { assertAcpSessionMutationEntry } from "./session-meta-entry.kernel.js";
import {
@ -33,17 +33,15 @@ import type {
AcpSessionMutationCommit,
AcpSessionMutationDecision,
AcpSessionMutationPreparation,
AcpSessionWriteOperations,
AcpSessionMutationPrepareInput,
} from "./session-meta-write.types.js";
export function executeAcpSessionMutationInWorker(
database: OpenClawStateDatabase,
command: SqliteWorkerCommand<AcpSessionWriteOperations>,
) {
return command.type === "acp.prepareMutation"
? prepareAcpSessionMutationInWorker(database, command.input)
: commitAcpSessionMutationInWorker(database, command.input);
}
export const acpSessionOperations = {
"acp.prepareMutation": (input: AcpSessionMutationPrepareInput, { open }) =>
prepareAcpSessionMutationInWorker(open(), input),
"acp.commitMutation": (input: AcpSessionMutationCommit & { nonce: string }, { open }) =>
commitAcpSessionMutationInWorker(open(), input),
} satisfies WorkerOperationHandlers;
function readControlledAcpSessionMutation(
database: OpenClawStateDatabase,
@ -67,7 +65,7 @@ function readControlledAcpSessionMutation(
function prepareAcpSessionMutationInWorker(
database: OpenClawStateDatabase,
input: AcpSessionWriteOperations["acp.prepareMutation"]["input"],
input: AcpSessionMutationPrepareInput,
): AcpSessionMutationPreparation {
return runOpenClawStateWriteTransaction(
({ db }) => {
@ -133,7 +131,7 @@ function consumeSources(database: OpenClawStateDatabase, input: AcpSessionMutati
function commitAcpSessionMutationInWorker(
database: OpenClawStateDatabase,
input: AcpSessionWriteOperations["acp.commitMutation"]["input"],
input: AcpSessionMutationCommit & { nonce: string },
) {
return runOpenClawStateWriteTransaction(
(current) => {

View file

@ -0,0 +1,4 @@
import type { WorkerOperations } from "../../state/worker-operation-registry.js";
import type { authProfileOperations } from "./store.worker.js";
export type AuthProfileWorkerOperations = WorkerOperations<typeof authProfileOperations>;

View file

@ -0,0 +1,51 @@
import { readConfigMachineState } from "../../state/config-machine-state.js";
import {
withArtifactPreservingStateReads,
withExistingOpenClawStateDatabaseReadOnly,
} from "../../state/openclaw-state-db-readonly.js";
import { readUserModelAuthProfile } from "../../state/user-model-accounts.js";
import type { WorkerOperationHandlers } from "../../state/worker-operation-registry.js";
import { readAuthProfileRows, SHARED_AUTH_STORE_STATE_KEY } from "./sqlite-json.js";
import { isMissingDatabasePath } from "./sqlite-read-pool.js";
import type { AuthProfileRowRead } from "./types.js";
export const authProfileOperations = {
"authProfiles.read": (input: { artifactPreserving: boolean }, { stateOptions }) => {
const read = (): AuthProfileRowRead => {
const options = stateOptions();
const missing: AuthProfileRowRead = {
store: { status: "missing", reason: "database" },
state: { status: "missing", reason: "database" },
cacheable: false,
};
try {
return (
withExistingOpenClawStateDatabaseReadOnly(
({ db }) => readAuthProfileRows(db, options.path, "shared-state"),
options,
) ?? missing
);
} catch {
return isMissingDatabasePath(options.path)
? missing
: {
store: { status: "unreadable" },
state: { status: "unreadable" },
cacheable: false,
};
}
};
return input.artifactPreserving ? withArtifactPreservingStateReads(read) : read();
},
"authProfiles.sharedOwnership": (input: { artifactPreserving: boolean }, { stateOptions }) => {
const read = () => readConfigMachineState(SHARED_AUTH_STORE_STATE_KEY, stateOptions());
return input.artifactPreserving ? withArtifactPreservingStateReads(read) : read();
},
"authProfiles.personal": (
input: { profileId: string; artifactPreserving: boolean },
{ stateOptions },
) => {
const read = () => readUserModelAuthProfile(input.profileId, stateOptions());
return input.artifactPreserving ? withArtifactPreservingStateReads(read) : read();
},
} satisfies WorkerOperationHandlers;

View file

@ -2,8 +2,8 @@
import { createSqliteWorkerWriteAdmission } from "../infra/sqlite-worker-store.js";
import { executeExistingOpenClawStateRead } from "../state/openclaw-state-db-readonly.js";
import type { OpenClawStateAsyncLeaseContext } from "../state/openclaw-state-lease-context.js";
import { runWithOpenClawStateLeaseWorker } from "../state/openclaw-state-lease-worker-operation.js";
import type { OpenClawStateLeaseWorkerAuthority } from "../state/openclaw-state-lease-worker-owner.js";
import { runWithOpenClawStateLeaseWorker } from "../state/openclaw-state-lease-worker-storage.js";
import { captureOpenClawStateWorkerContext } from "../state/openclaw-state-worker-context.js";
import type { OpenClawStateWorkerContext } from "../state/openclaw-state-worker-context.types.js";
import {

View file

@ -3,7 +3,7 @@ import type {
OAuthClientInformationMixed,
OAuthTokens,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease.types.js";
type McpOAuthAuthorizationChallenge = {
resourceMetadataUrl?: string;

View file

@ -211,7 +211,7 @@ export function readDeferredPluginMigrations(
/** Prime resolution in this module before the updater replaces its package. */
export async function prepareDeferredPluginMigrationRuntime(): Promise<void> {
const { prepareOpenClawStateLeaseWorkerRuntime } =
await import("../state/openclaw-state-lease-worker-storage.js");
await import("../state/openclaw-state-lease-worker-operation.js");
await Promise.all([
import("../state/openclaw-state-worker-store.js"),
import("../plugins/plugin-lifecycle-lease.js"),
@ -488,7 +488,7 @@ export async function recordDeferredPluginMigrations(
});
const { withPluginLifecycleLease } = await import("../plugins/plugin-lifecycle-lease.js");
const { runWithOpenClawStateLeaseWorker } =
await import("../state/openclaw-state-lease-worker-storage.js");
await import("../state/openclaw-state-lease-worker-operation.js");
return withPluginLifecycleLease({ env: params.env }, async (lease) => {
const context = captureOpenClawStateWorkerContext({
env: params.env,

View file

@ -1,55 +0,0 @@
import { ZodError } from "zod";
import {
runOpenClawStateWriteTransaction,
type OpenClawStateDatabaseOptions,
} from "../state/openclaw-state-db.js";
import { assertOpenClawStateLeaseWorkerOwnedInTransaction } from "../state/openclaw-state-lease-worker.js";
import type { OpenClawStateWorkerOperations } from "../state/openclaw-state-worker-contract.js";
import {
DeferredPluginMigrationConflictError,
recordDeferredPluginMigrationsInTransaction,
readDeferredPluginMigrationCompletions,
readDeferredPluginMigrations,
} from "./deferred-plugin-migrations.js";
import type { SqliteWorkerCommand } from "./sqlite-worker-contract.js";
export function readDeferredPluginMigrationsInWorker(
command: Extract<
SqliteWorkerCommand<OpenClawStateWorkerOperations>,
{ type: "plugins.deferredMigrations.read" | "plugins.deferredMigrations.completions.read" }
>,
options: { path: string; env: NodeJS.ProcessEnv },
) {
return command.type === "plugins.deferredMigrations.read"
? readDeferredPluginMigrations({
...options,
artifactPreservingReadOnly: command.input.artifactPreservingReadOnly,
})
: readDeferredPluginMigrationCompletions(options);
}
export function recordDeferredPluginMigrationsInWorker(
input: OpenClawStateWorkerOperations["plugins.deferredMigrations.record"]["input"],
options: OpenClawStateDatabaseOptions,
): OpenClawStateWorkerOperations["plugins.deferredMigrations.record"]["output"] {
try {
return runOpenClawStateWriteTransaction(
({ db }) => {
assertOpenClawStateLeaseWorkerOwnedInTransaction(db, input.identity);
const transitions = recordDeferredPluginMigrationsInTransaction(db, input);
assertOpenClawStateLeaseWorkerOwnedInTransaction(db, input.identity, "write", "commit");
return { kind: "recorded" as const, transitions };
},
options,
{ operationLabel: "state.plugin-migration-deferral" },
);
} catch (error) {
if (error instanceof DeferredPluginMigrationConflictError) {
return { kind: "conflict", pending: error.pending };
}
if (error instanceof ZodError) {
return { kind: "invalid", issues: error.issues };
}
throw error;
}
}

View file

@ -1,12 +0,0 @@
import type { HostedOfficialExternalPluginCatalogSnapshot } from "./official-external-plugin-catalog.types.js";
export type HostedCatalogSnapshotWorkerOperations = {
"plugins.catalogSnapshot.read": {
input: { url: string };
output: HostedOfficialExternalPluginCatalogSnapshot | null;
};
"plugins.catalogSnapshot.write": {
input: { snapshot: HostedOfficialExternalPluginCatalogSnapshot; now: number };
output: { ok: true } | { ok: false; message: string };
};
};

View file

@ -0,0 +1,4 @@
import type { WorkerOperations } from "../state/worker-operation-registry.js";
import type { pluginRuntimeOperations } from "./state.worker.js";
export type PluginRuntimeWorkerOperations = WorkerOperations<typeof pluginRuntimeOperations>;

104
src/plugins/state.worker.ts Normal file
View file

@ -0,0 +1,104 @@
import { ZodError } from "zod";
import {
DeferredPluginMigrationConflictError,
readDeferredPluginMigrationCompletions,
readDeferredPluginMigrations,
recordDeferredPluginMigrationsInTransaction,
type DeferredPluginMigrationRecordInput,
} from "../infra/deferred-plugin-migrations.js";
import { runOpenClawStateWriteTransaction } from "../state/openclaw-state-db.js";
import { assertOpenClawStateLeaseWorkerOwnedInTransaction } from "../state/openclaw-state-lease-worker.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease.types.js";
import type { WorkerOperationHandlers } from "../state/worker-operation-registry.js";
import {
readPluginBindingApprovalsInDatabase,
upsertPluginBindingApprovalInDatabase,
} from "./conversation-binding-state.kernel.js";
import type { PluginBindingApprovalEntry } from "./conversation-binding-state.types.js";
import { publishPluginSourceAdmissionInDatabase } from "./installed-plugin-index-store-write.js";
import {
readHostedCatalogSnapshotInDatabase,
writeHostedCatalogSnapshotInDatabase,
} from "./official-external-plugin-catalog-snapshot-store.kernel.js";
import { HostedCatalogSignedFeedMonotonicityError } from "./official-external-plugin-catalog-source.js";
import type { HostedOfficialExternalPluginCatalogSnapshot } from "./official-external-plugin-catalog.types.js";
import type { PluginSourceAdmissionPublication } from "./plugin-source-admission.types.js";
export const pluginRuntimeOperations = {
"plugins.conversationBindingApprovals.read": (_input: undefined, { open }) =>
readPluginBindingApprovalsInDatabase(open().db),
"plugins.conversationBindingApprovals.upsert": (
input: PluginBindingApprovalEntry,
{ open, stateOptions },
) =>
runOpenClawStateWriteTransaction(({ db }) => upsertPluginBindingApprovalInDatabase(db, input), {
database: open(),
...stateOptions(),
}),
"plugins.catalogSnapshot.read": (input: { url: string }, { open }) =>
readHostedCatalogSnapshotInDatabase(open().db, input.url),
"plugins.catalogSnapshot.write": (
input: { snapshot: HostedOfficialExternalPluginCatalogSnapshot; now: number },
{ open, stateOptions },
) => {
const options = { database: open(), ...stateOptions() };
try {
runOpenClawStateWriteTransaction(
({ db }) => writeHostedCatalogSnapshotInDatabase(db, input.snapshot, input.now),
options,
);
return { ok: true as const };
} catch (error) {
if (error instanceof HostedCatalogSignedFeedMonotonicityError) {
return { ok: false as const, message: error.message };
}
throw error;
}
},
"plugins.metadata.sourceAdmission.publish": (
input: PluginSourceAdmissionPublication,
{ open, stateOptions },
) =>
runOpenClawStateWriteTransaction(
({ db }) => publishPluginSourceAdmissionInDatabase(db, input),
{ database: open(), ...stateOptions() },
),
"plugins.deferredMigrations.record": (
input: Omit<DeferredPluginMigrationRecordInput, "env"> & {
identity: OpenClawStateLeaseIdentity;
},
{ open, stateOptions },
) => {
const options = { database: open(), ...stateOptions() };
try {
return runOpenClawStateWriteTransaction(
({ db }) => {
assertOpenClawStateLeaseWorkerOwnedInTransaction(db, input.identity);
const transitions = recordDeferredPluginMigrationsInTransaction(db, input);
assertOpenClawStateLeaseWorkerOwnedInTransaction(db, input.identity, "write", "commit");
return { kind: "recorded" as const, transitions };
},
options,
{ operationLabel: "state.plugin-migration-deferral" },
);
} catch (error) {
if (error instanceof DeferredPluginMigrationConflictError) {
return { kind: "conflict" as const, pending: error.pending };
}
if (error instanceof ZodError) {
return { kind: "invalid" as const, issues: error.issues };
}
throw error;
}
},
"plugins.deferredMigrations.read": (
input: { artifactPreservingReadOnly: boolean },
{ stateOptions },
) =>
readDeferredPluginMigrations({
...stateOptions(),
artifactPreservingReadOnly: input.artifactPreservingReadOnly,
}),
"plugins.deferredMigrations.completions.read": (_input: undefined, { stateOptions }) =>
readDeferredPluginMigrationCompletions(stateOptions()),
} satisfies WorkerOperationHandlers;

View file

@ -59,7 +59,7 @@ vi.mock("./project-clone-runtime.js", () => ({
cloneProjectCheckout: fixture.clone,
}));
vi.mock("../state/openclaw-state-lease-worker-storage.js", () => ({
vi.mock("../state/openclaw-state-lease-worker-operation.js", () => ({
runWithOpenClawStateLeaseWorker: async (
_lease: OpenClawStateLeaseContext,
_context: unknown,

View file

@ -50,7 +50,7 @@ vi.mock("../state/openclaw-state-lease.js", () => ({
vi.mock("../state/openclaw-state-worker-store.js", () => ({
executeOpenClawStateWorker: mocks.execute,
}));
vi.mock("../state/openclaw-state-lease-worker-storage.js", () => ({
vi.mock("../state/openclaw-state-lease-worker-operation.js", () => ({
runWithOpenClawStateLeaseWorker: async (
_lease: unknown,
context: unknown,

View file

@ -54,7 +54,7 @@ export async function registerPreparedProjectRegistry(
);
}
const { runWithOpenClawStateLeaseWorker } =
await import("../state/openclaw-state-lease-worker-storage.js");
await import("../state/openclaw-state-lease-worker-operation.js");
return await runWithOpenClawStateLeaseWorker(lease, context, async (scope, identity) => {
const project = await scope.execute({
type: "projects.insert",

View file

@ -243,7 +243,7 @@ export async function resolveProjectCloneRefreshOwner(
context: OpenClawStateWorkerContext,
): Promise<ProjectRegistryRecord | undefined> {
const { runWithOpenClawStateLeaseWorker } =
await import("../state/openclaw-state-lease-worker-storage.js");
await import("../state/openclaw-state-lease-worker-operation.js");
return await runWithOpenClawStateLeaseWorker(lease, context, (scope, identity) =>
scope.execute({
type: "projects.resolveRefreshOwner",
@ -285,7 +285,7 @@ export async function removeProjectRegistry(
{ path: context.admission.databasePath, env },
async (lease) => {
const { runWithOpenClawStateLeaseWorker } =
await import("../state/openclaw-state-lease-worker-storage.js");
await import("../state/openclaw-state-lease-worker-operation.js");
return await runWithOpenClawStateLeaseWorker(lease, context, (scope, identity) =>
scope.execute({
type: "projects.remove",

View file

@ -1,4 +1,4 @@
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease.types.js";
import type {
ProjectRegistryIdentity,
ProjectRegistryInsert,

View file

@ -18,7 +18,7 @@ import {
resolveSkillUploadDatabaseOptions,
type SkillUploadMetadataRow,
} from "./upload-store.sqlite.js";
import type { SkillUploadWorkerOperations } from "./upload-store.worker.js";
import type { SkillUploadWorkerOperations } from "./upload-store.worker-contract.js";
type SkillUploadScope = Pick<SqliteWorkerStore<SkillUploadWorkerOperations>, "execute">;
/** Time window in which uploaded skill archive chunks may be committed. */

View file

@ -0,0 +1,4 @@
import type { WorkerOperations } from "../../state/worker-operation-registry.js";
import type { skillUploadOperations } from "./upload-store.worker.js";
export type SkillUploadWorkerOperations = WorkerOperations<typeof skillUploadOperations>;

View file

@ -1,9 +1,5 @@
import type { SqliteWorkerCommand } from "../../infra/sqlite-worker-contract.js";
import { requestSqliteWorkerOperationAdmission } from "../../infra/sqlite-worker-operation-admission.js";
import type {
OpenClawStateDatabase,
OpenClawStateDatabaseOptions,
} from "../../state/openclaw-state-db.js";
import type { WorkerOperationHandlers } from "../../state/worker-operation-registry.js";
import { commitSkillUploadInDatabase } from "./upload-store-commit.js";
import {
appendSkillUploadChunkInDatabase,
@ -15,69 +11,42 @@ import {
} from "./upload-store.kernel.js";
import { deleteOwnedSkillUpload, renewSkillUploadInstallLease } from "./upload-store.sqlite.js";
type Operation<F extends (...args: never[]) => unknown> = {
input: Parameters<F>[0];
output: ReturnType<F>;
};
export type SkillUploadWorkerOperations = {
"skillUploads.begin": Operation<typeof beginSkillUploadInDatabase>;
"skillUploads.chunk": Operation<typeof appendSkillUploadChunkInDatabase>;
"skillUploads.commit": Operation<typeof commitSkillUploadInDatabase>;
"skillUploads.expired": Operation<typeof listExpiredSkillUploadsInDatabase>;
"skillUploads.deleteExpired": Operation<typeof deleteExpiredSkillUploadInDatabase>;
"skillUploads.claim": Operation<typeof claimSkillUploadInDatabase>;
"skillUploads.renew": {
input: Omit<Parameters<typeof renewSkillUploadInstallLease>[0], "options">;
output: boolean;
};
"skillUploads.consume": {
input: { uploadId: string; owner: string };
output: ReturnType<typeof deleteOwnedSkillUpload>;
};
"skillUploads.release": Operation<typeof releaseSkillUploadInDatabase>;
};
const admit = (stage: "transaction" | "commit") =>
requestSqliteWorkerOperationAdmission({ stage, facts: undefined });
export function isSkillUploadCommand(command: {
type: string;
input: unknown;
}): command is SqliteWorkerCommand<SkillUploadWorkerOperations> {
return (
command.type === "skillUploads.begin" ||
command.type === "skillUploads.chunk" ||
command.type === "skillUploads.commit" ||
command.type === "skillUploads.expired" ||
command.type === "skillUploads.deleteExpired" ||
command.type === "skillUploads.claim" ||
command.type === "skillUploads.renew" ||
command.type === "skillUploads.consume" ||
command.type === "skillUploads.release"
);
}
export function executeSkillUploadCommand(
command: SqliteWorkerCommand<SkillUploadWorkerOperations>,
options: OpenClawStateDatabaseOptions & { database: OpenClawStateDatabase },
): SkillUploadWorkerOperations[keyof SkillUploadWorkerOperations]["output"] {
const admit = (stage: "transaction" | "commit") =>
requestSqliteWorkerOperationAdmission({ stage, facts: undefined });
switch (command.type) {
case "skillUploads.begin":
return beginSkillUploadInDatabase(command.input, options, admit);
case "skillUploads.chunk":
return appendSkillUploadChunkInDatabase(command.input, options, admit);
case "skillUploads.commit":
return commitSkillUploadInDatabase(command.input, options, admit);
case "skillUploads.expired":
return listExpiredSkillUploadsInDatabase(command.input, options);
case "skillUploads.deleteExpired":
return deleteExpiredSkillUploadInDatabase(command.input, options);
case "skillUploads.claim":
return claimSkillUploadInDatabase(command.input, options);
case "skillUploads.renew":
return renewSkillUploadInstallLease({ ...command.input, options });
case "skillUploads.consume":
return deleteOwnedSkillUpload(command.input.uploadId, command.input.owner, options);
case "skillUploads.release":
return releaseSkillUploadInDatabase(command.input, options);
}
}
export const skillUploadOperations = {
"skillUploads.begin": (
input: Parameters<typeof beginSkillUploadInDatabase>[0],
{ open, stateOptions },
) => beginSkillUploadInDatabase(input, { database: open(), ...stateOptions() }, admit),
"skillUploads.chunk": (
input: Parameters<typeof appendSkillUploadChunkInDatabase>[0],
{ open, stateOptions },
) => appendSkillUploadChunkInDatabase(input, { database: open(), ...stateOptions() }, admit),
"skillUploads.commit": (
input: Parameters<typeof commitSkillUploadInDatabase>[0],
{ open, stateOptions },
) => commitSkillUploadInDatabase(input, { database: open(), ...stateOptions() }, admit),
"skillUploads.expired": (
input: Parameters<typeof listExpiredSkillUploadsInDatabase>[0],
{ open, stateOptions },
) => listExpiredSkillUploadsInDatabase(input, { database: open(), ...stateOptions() }),
"skillUploads.deleteExpired": (
input: Parameters<typeof deleteExpiredSkillUploadInDatabase>[0],
{ open, stateOptions },
) => deleteExpiredSkillUploadInDatabase(input, { database: open(), ...stateOptions() }),
"skillUploads.claim": (
input: Parameters<typeof claimSkillUploadInDatabase>[0],
{ open, stateOptions },
) => claimSkillUploadInDatabase(input, { database: open(), ...stateOptions() }),
"skillUploads.renew": (
input: Omit<Parameters<typeof renewSkillUploadInstallLease>[0], "options">,
{ open, stateOptions },
) => renewSkillUploadInstallLease({ ...input, options: { database: open(), ...stateOptions() } }),
"skillUploads.consume": (input: { uploadId: string; owner: string }, { open, stateOptions }) =>
deleteOwnedSkillUpload(input.uploadId, input.owner, { database: open(), ...stateOptions() }),
"skillUploads.release": (
input: Parameters<typeof releaseSkillUploadInDatabase>[0],
{ open, stateOptions },
) => releaseSkillUploadInDatabase(input, { database: open(), ...stateOptions() }),
} satisfies WorkerOperationHandlers;

View file

@ -1,8 +1,8 @@
import { cloneEnvWithPlatformSemantics } from "../../config/config-env-vars.js";
import type { SqliteWorkerStore } from "../../infra/sqlite-worker-contract.js";
import { createSqliteWorkerWriteAdmission } from "../../infra/sqlite-worker-store.js";
import type { OpenClawStateLeaseIdentity } from "../../state/openclaw-state-lease-store.js";
import { runWithOpenClawStateLeasesWorker } from "../../state/openclaw-state-lease-worker-storage.js";
import { runWithOpenClawStateLeasesWorker } from "../../state/openclaw-state-lease-worker-operation.js";
import type { OpenClawStateLeaseIdentity } from "../../state/openclaw-state-lease.types.js";
import { captureOpenClawStateWorkerContext } from "../../state/openclaw-state-worker-context.js";
import { runOpenClawStateWorkerOperation } from "../../state/openclaw-state-worker-store.js";
import { resolveSkillWorkshopStateDir } from "./proposal-generation.js";

View file

@ -1,69 +1,10 @@
import type { OpenClawStateLeaseIdentity } from "../../state/openclaw-state-lease-store.js";
import type {
RecordSkillExperienceReviewOutcomeInput,
ReadSkillCollectionBackupDropsInput,
SkillCollectionReviewOutcome,
} from "./collection-review.kernel.js";
import type { RecordSkillProposalEvaluationInput } from "./store-evaluation.kernel.js";
import type {
CreateSkillProposalInput,
ImportLegacySkillProposalInput,
ListStoredSkillProposalsInput,
UpdateSkillProposalRecordInput,
} from "./store-proposal.kernel.js";
import type { StoredSkillProposal } from "./store-sqlite-record.js";
import type {
ClearSkillProposalRollbackInput,
WriteSkillProposalRollbackInput,
} from "./store-sqlite-rollback.js";
import type {
CommitPendingSkillProposalTransitionInput,
PendingSkillProposalTransitionCommit,
ReadCommittedSkillProposalTransitionInput,
} from "./store-sqlite-transition.js";
import type { SkillProposalEvent, SkillProposalRecord, SkillProposalRollback } from "./types.js";
import type { WorkerOperations } from "../../state/worker-operation-registry.js";
import type { skillWorkshopOperations, skillCuratorOperations } from "./store.worker.js";
type WorkshopOperation<Input, Output> = {
input: {
value: Input;
agentId?: string;
leaseIdentities?: readonly OpenClawStateLeaseIdentity[];
};
output: Output;
};
export type SkillWorkshopWorkerOperations = WorkerOperations<typeof skillWorkshopOperations>;
export type SkillWorkshopExecutionOperations = Omit<
SkillWorkshopWorkerOperations,
"workshop.events.list"
>;
export type SkillWorkshopExecutionOperations = {
"workshop.schema.ensure": WorkshopOperation<undefined, void>;
"workshop.proposal.read": WorkshopOperation<string, StoredSkillProposal | null>;
"workshop.proposals.list": WorkshopOperation<
ListStoredSkillProposalsInput,
StoredSkillProposal[]
>;
"workshop.proposal.create": WorkshopOperation<CreateSkillProposalInput, SkillProposalEvent>;
"workshop.proposal.update": WorkshopOperation<
UpdateSkillProposalRecordInput,
SkillProposalEvent | undefined
>;
"workshop.proposal.import": WorkshopOperation<
ImportLegacySkillProposalInput,
"imported" | "already-imported"
>;
"workshop.proposal.evaluate": WorkshopOperation<
RecordSkillProposalEvaluationInput,
{ record: SkillProposalRecord; event: SkillProposalEvent }
>;
"workshop.transition.commit": WorkshopOperation<
CommitPendingSkillProposalTransitionInput & { operationLabel: string },
PendingSkillProposalTransitionCommit
>;
"workshop.transition.committed": WorkshopOperation<
ReadCommittedSkillProposalTransitionInput,
Extract<PendingSkillProposalTransitionCommit, { state: "committed" }> | null
>;
"workshop.rollback.read": WorkshopOperation<string, SkillProposalRollback | null>;
"workshop.rollback.write": WorkshopOperation<WriteSkillProposalRollbackInput, void>;
"workshop.rollback.clear": WorkshopOperation<ClearSkillProposalRollbackInput, boolean>;
"workshop.collection.list": WorkshopOperation<string, SkillCollectionReviewOutcome[]>;
"workshop.collection.drops": WorkshopOperation<ReadSkillCollectionBackupDropsInput, Set<string>>;
"workshop.experience.record": WorkshopOperation<RecordSkillExperienceReviewOutcomeInput, void>;
};
export type SkillCuratorOperations = WorkerOperations<typeof skillCuratorOperations>;

View file

@ -1,11 +1,13 @@
import type { DatabaseSync } from "node:sqlite";
import type { SqliteWorkerCommand } from "../../infra/sqlite-worker-contract.js";
import { requestSqliteWorkerOperationAdmission } from "../../infra/sqlite-worker-operation-admission.js";
import { getSqliteWorkerStateContext } from "../../infra/sqlite-worker-state-context.js";
import type { OpenClawStateDatabase } from "../../state/openclaw-state-db-contract.js";
import { runOpenClawStateWriteTransaction } from "../../state/openclaw-state-db.js";
import { assertOpenClawStateLeasesWorkerOwnedInTransaction } from "../../state/openclaw-state-lease-worker.js";
import type { OpenClawStateWorkerOperations } from "../../state/openclaw-state-worker-contract.js";
import type { OpenClawStateLeaseIdentity } from "../../state/openclaw-state-lease.types.js";
import type {
WorkerOperationContext,
WorkerOperationHandlers,
} from "../../state/worker-operation-registry.js";
import {
listSkillCollectionReviewOutcomesInDatabase,
readSkillCollectionBackupDropsInDatabase,
@ -32,162 +34,197 @@ import {
commitPendingSkillProposalTransitionInDatabase,
readCommittedSkillProposalTransitionInDatabase,
} from "./store-sqlite-transition.js";
import type { SkillWorkshopExecutionOperations } from "./store.worker-contract.js";
type Operations = SkillWorkshopExecutionOperations &
Pick<
OpenClawStateWorkerOperations,
"skills.curator.read" | "skills.usage.record" | "workshop.events.list"
>;
type WorkshopInput<Value> = {
value: Value;
agentId?: string;
leaseIdentities?: readonly OpenClawStateLeaseIdentity[];
};
export function isSkillWorkshopCommand(command: {
type: string;
input: unknown;
}): command is SqliteWorkerCommand<Operations> {
switch (command.type) {
case "skills.curator.read":
case "skills.usage.record":
case "workshop.events.list":
case "workshop.schema.ensure":
case "workshop.proposal.read":
case "workshop.proposals.list":
case "workshop.proposal.create":
case "workshop.proposal.update":
case "workshop.proposal.import":
case "workshop.proposal.evaluate":
case "workshop.transition.commit":
case "workshop.transition.committed":
case "workshop.rollback.read":
case "workshop.rollback.write":
case "workshop.rollback.clear":
case "workshop.collection.list":
case "workshop.collection.drops":
case "workshop.experience.record":
return true;
default:
return false;
function assertWrite(
db: DatabaseSync,
input: WorkshopInput<unknown>,
stage: "transaction" | "commit",
) {
if (input.leaseIdentities) {
assertOpenClawStateLeasesWorkerOwnedInTransaction(db, input.leaseIdentities, stage);
} else {
requestSqliteWorkerOperationAdmission({ stage, facts: undefined });
}
}
export function executeSkillWorkshopCommand(
command: SqliteWorkerCommand<Operations>,
database: OpenClawStateDatabase,
databasePath: string,
): Operations[keyof Operations]["output"] {
if (command.type === "skills.curator.read") {
return readSkillCuratorStateInDatabase(database, command.input.skillFiles);
}
const options = {
database,
path: databasePath,
env: getSqliteWorkerStateContext().environment,
};
if (command.type === "skills.usage.record") {
return runOpenClawStateWriteTransaction(
(current) => recordSkillUsageInDatabase(current, command.input),
options,
);
}
if (command.type === "workshop.events.list") {
ensureSkillWorkshopSchemaInDatabase(database, options);
return listStoredSkillProposalEventsInDatabase(database.db, command.input);
}
const { leaseIdentities } = command.input;
const assertWrite = (db: DatabaseSync, stage: "transaction" | "commit") => {
if (leaseIdentities) {
assertOpenClawStateLeasesWorkerOwnedInTransaction(db, leaseIdentities, stage);
} else {
requestSqliteWorkerOperationAdmission({ stage, facts: undefined });
}
};
if (command.type === "workshop.schema.ensure") {
return ensureSkillWorkshopSchemaInDatabase(database, options, assertWrite);
}
const write = <T>(operationLabel: string, operation: () => T, proposalId?: string): T =>
runOpenClawStateWriteTransaction(
({ db }) => {
assertWrite(db, "transaction");
if (leaseIdentities) {
const stored = proposalId ? readStoredProposalInDatabase(db, proposalId) : null;
const agentId = command.input.agentId;
for (const identity of leaseIdentities) {
if (
!agentId ||
(identity.scope === "skill-collection"
? identity.key !== agentId
: identity.scope !== "skill-workshop-target" ||
!stored ||
identity.key !==
`${agentId}:${hashSkillProposalContent(stored.record.target.skillFile)}`) ||
(stored !== null &&
stored.row.owner_agent_id !== null &&
stored.row.owner_agent_id !== agentId)
) {
throw new Error("Skill Workshop lease does not match its proposal target.");
}
function write<T>(
input: WorkshopInput<unknown>,
{ open, stateOptions }: WorkerOperationContext,
operationLabel: string,
operation: (database: OpenClawStateDatabase) => T,
proposalId?: string,
): T {
const database = open();
return runOpenClawStateWriteTransaction(
({ db }) => {
assertWrite(db, input, "transaction");
if (input.leaseIdentities) {
const stored = proposalId ? readStoredProposalInDatabase(db, proposalId) : null;
const { agentId } = input;
for (const identity of input.leaseIdentities) {
if (
!agentId ||
(identity.scope === "skill-collection"
? identity.key !== agentId
: identity.scope !== "skill-workshop-target" ||
!stored ||
identity.key !==
`${agentId}:${hashSkillProposalContent(stored.record.target.skillFile)}`) ||
(stored !== null &&
stored.row.owner_agent_id !== null &&
stored.row.owner_agent_id !== agentId)
) {
throw new Error("Skill Workshop lease does not match its proposal target.");
}
}
const result = operation();
assertWrite(db, "commit");
return result;
},
options,
{ operationLabel },
);
switch (command.type) {
case "workshop.proposal.read":
return readStoredProposalInDatabase(database.db, command.input.value);
case "workshop.proposals.list":
return listStoredSkillProposalsInDatabase(database.db, command.input.value);
case "workshop.proposal.create":
return write("skill-workshop.proposal.create", () =>
createSkillProposalInDatabase(database.db, command.input.value),
);
case "workshop.proposal.update":
return write(
"skill-workshop.proposal.update",
() => updateSkillProposalRecordInDatabase(database.db, command.input.value),
command.input.value.record.id,
);
case "workshop.proposal.import":
return write("doctor.skill-workshop.import", () =>
importLegacySkillProposalInDatabase(database.db, command.input.value),
);
case "workshop.proposal.evaluate":
return write(
"skill-workshop.proposal.evaluate",
() => recordSkillProposalEvaluationInDatabase(database.db, command.input.value),
command.input.value.proposalId,
);
case "workshop.transition.commit":
return write(
command.input.value.operationLabel,
() => commitPendingSkillProposalTransitionInDatabase(database.db, command.input.value),
command.input.value.expected.id,
);
case "workshop.transition.committed":
return readCommittedSkillProposalTransitionInDatabase(database.db, command.input.value);
case "workshop.rollback.read":
return readSkillProposalRollbackInDatabase(database.db, command.input.value);
case "workshop.rollback.write":
return write(
"skill-workshop.rollback.write",
() => writeSkillProposalRollbackInDatabase(database.db, command.input.value),
command.input.value.proposalId,
);
case "workshop.rollback.clear":
return write(
"skill-workshop.rollback.clear",
() => clearSkillProposalRollbackInDatabase(database.db, command.input.value),
command.input.value.proposalId,
);
case "workshop.collection.list":
return listSkillCollectionReviewOutcomesInDatabase(database.db, command.input.value);
case "workshop.collection.drops":
return readSkillCollectionBackupDropsInDatabase(database.db, command.input.value);
case "workshop.experience.record":
return write("skill-workshop.experience.record", () =>
recordSkillExperienceReviewOutcomeInDatabase(database, command.input.value),
);
}
}
const result = operation(database);
assertWrite(db, input, "commit");
return result;
},
{ database, ...stateOptions() },
{ operationLabel },
);
}
export const skillCuratorOperations = {
"skills.curator.read": (input: { skillFiles: readonly string[] }, { open }) =>
readSkillCuratorStateInDatabase(open(), input.skillFiles),
"skills.usage.record": (
input: Parameters<typeof recordSkillUsageInDatabase>[1],
{ open, stateOptions },
) =>
runOpenClawStateWriteTransaction((current) => recordSkillUsageInDatabase(current, input), {
database: open(),
...stateOptions(),
}),
} satisfies WorkerOperationHandlers;
export const skillWorkshopOperations = {
"workshop.events.list": (
input: Parameters<typeof listStoredSkillProposalEventsInDatabase>[1],
{ open, stateOptions },
) => {
const database = open();
ensureSkillWorkshopSchemaInDatabase(database, { database, ...stateOptions() });
return listStoredSkillProposalEventsInDatabase(database.db, input);
},
"workshop.schema.ensure": (input: WorkshopInput<undefined>, { open, stateOptions }) => {
const database = open();
return ensureSkillWorkshopSchemaInDatabase(
database,
{ database, ...stateOptions() },
(db, stage) => assertWrite(db, input, stage),
);
},
"workshop.proposal.read": (
input: WorkshopInput<Parameters<typeof readStoredProposalInDatabase>[1]>,
{ open },
) => readStoredProposalInDatabase(open().db, input.value),
"workshop.proposals.list": (
input: WorkshopInput<Parameters<typeof listStoredSkillProposalsInDatabase>[1]>,
{ open },
) => listStoredSkillProposalsInDatabase(open().db, input.value),
"workshop.proposal.create": (
input: WorkshopInput<Parameters<typeof createSkillProposalInDatabase>[1]>,
context,
) =>
write(input, context, "skill-workshop.proposal.create", (database) =>
createSkillProposalInDatabase(database.db, input.value),
),
"workshop.proposal.update": (
input: WorkshopInput<Parameters<typeof updateSkillProposalRecordInDatabase>[1]>,
context,
) =>
write(
input,
context,
"skill-workshop.proposal.update",
(database) => updateSkillProposalRecordInDatabase(database.db, input.value),
input.value.record.id,
),
"workshop.proposal.import": (
input: WorkshopInput<Parameters<typeof importLegacySkillProposalInDatabase>[1]>,
context,
) =>
write(input, context, "doctor.skill-workshop.import", (database) =>
importLegacySkillProposalInDatabase(database.db, input.value),
),
"workshop.proposal.evaluate": (
input: WorkshopInput<Parameters<typeof recordSkillProposalEvaluationInDatabase>[1]>,
context,
) =>
write(
input,
context,
"skill-workshop.proposal.evaluate",
(database) => recordSkillProposalEvaluationInDatabase(database.db, input.value),
input.value.proposalId,
),
"workshop.transition.commit": (
input: WorkshopInput<
Parameters<typeof commitPendingSkillProposalTransitionInDatabase>[1] & {
operationLabel: string;
}
>,
context,
) =>
write(
input,
context,
input.value.operationLabel,
(database) => commitPendingSkillProposalTransitionInDatabase(database.db, input.value),
input.value.expected.id,
),
"workshop.transition.committed": (
input: WorkshopInput<Parameters<typeof readCommittedSkillProposalTransitionInDatabase>[1]>,
{ open },
) => readCommittedSkillProposalTransitionInDatabase(open().db, input.value),
"workshop.rollback.read": (
input: WorkshopInput<Parameters<typeof readSkillProposalRollbackInDatabase>[1]>,
{ open },
) => readSkillProposalRollbackInDatabase(open().db, input.value),
"workshop.rollback.write": (
input: WorkshopInput<Parameters<typeof writeSkillProposalRollbackInDatabase>[1]>,
context,
) =>
write(
input,
context,
"skill-workshop.rollback.write",
(database) => writeSkillProposalRollbackInDatabase(database.db, input.value),
input.value.proposalId,
),
"workshop.rollback.clear": (
input: WorkshopInput<Parameters<typeof clearSkillProposalRollbackInDatabase>[1]>,
context,
) =>
write(
input,
context,
"skill-workshop.rollback.clear",
(database) => clearSkillProposalRollbackInDatabase(database.db, input.value),
input.value.proposalId,
),
"workshop.collection.list": (
input: WorkshopInput<Parameters<typeof listSkillCollectionReviewOutcomesInDatabase>[1]>,
{ open },
) => listSkillCollectionReviewOutcomesInDatabase(open().db, input.value),
"workshop.collection.drops": (
input: WorkshopInput<Parameters<typeof readSkillCollectionBackupDropsInDatabase>[1]>,
{ open },
) => readSkillCollectionBackupDropsInDatabase(open().db, input.value),
"workshop.experience.record": (
input: WorkshopInput<Parameters<typeof recordSkillExperienceReviewOutcomeInDatabase>[1]>,
context,
) =>
write(input, context, "skill-workshop.experience.record", (database) =>
recordSkillExperienceReviewOutcomeInDatabase(database, input.value),
),
} satisfies WorkerOperationHandlers;

View file

@ -11,7 +11,7 @@ import {
OpenClawStateLeaseError,
} from "./openclaw-state-lease-error.js";
import { STATE_LEASE_WRITE_BACKOFF } from "./openclaw-state-lease-storage.js";
import type { OpenClawStateLeaseAcquisition } from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseAcquisition } from "./openclaw-state-lease.types.js";
const log = createSubsystemLogger("state/lease");

View file

@ -7,9 +7,9 @@ import {
} from "./openclaw-state-db-async-lifecycle.js";
import type { OpenClawStateAsyncLeaseContext } from "./openclaw-state-lease-context.js";
import type { LeaseHeartbeatCleanup } from "./openclaw-state-lease-heartbeat.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import { withOpenClawStateLeaseWorkerAdmission } from "./openclaw-state-lease-worker-owner.js";
import { withOpenClawStateLeaseAsync } from "./openclaw-state-lease.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerContext } from "./openclaw-state-worker-context.types.js";
type CreateStorage =

View file

@ -2,7 +2,7 @@ import type { DatabaseSync } from "node:sqlite";
import type {
OpenClawStateLeaseAcquisition,
OpenClawStateLeaseIdentity,
} from "./openclaw-state-lease-store.js";
} from "./openclaw-state-lease.types.js";
export type OpenClawStateLeaseContext = {
signal: AbortSignal;

View file

@ -1,5 +1,5 @@
import type { StateLeaseProcessOwner } from "../infra/state-lease-process-owner.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerErrorPayload } from "./openclaw-state-worker-error.js";
// Allow headroom over observed 38 s cold Gateway boots under load; committed lease expiry still bounds startup.

View file

@ -25,10 +25,9 @@ import {
releaseOpenClawStateLeaseInTransaction,
renewOpenClawStateLeaseInTransaction,
} from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import { OPENCLAW_STATE_SCHEMA_SQL } from "./openclaw-state-schema.js";
import { captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context.js";
import { runOpenClawStateWorkerOperation } from "./openclaw-state-worker-store.js";
export type OpenClawStateLeaseDatabase = {
scope: "shared";
@ -98,6 +97,8 @@ export async function acquireLease(
});
}
};
const { runOpenClawStateWorkerOperation } = await import("./openclaw-state-worker-store.js");
assertAdmission();
const result = await runOpenClawStateWorkerOperation(
context,
(scope) =>

View file

@ -11,11 +11,11 @@ import {
type StateLeaseProcessOwner,
} from "../infra/state-lease-process-owner.js";
import type { DB } from "./openclaw-state-db.generated.js";
import type {
OpenClawStateLeaseIdentity,
OpenClawStateLeaseAcquisition,
} from "./openclaw-state-lease.types.js";
export type OpenClawStateLeaseIdentity = { scope: string; key: string; owner: string };
export type OpenClawStateLeaseAcquisition =
| { kind: "acquired"; expiresAt: number }
| { kind: "held"; holder: { owner: string; epoch: number; expiresAt: number | null } };
type LeaseDatabase = Pick<DB, "state_leases">;
/** The caller owns the write transaction; only absent or expired leases can be acquired. */

View file

@ -5,13 +5,13 @@ import type { SqliteWorkerOperationSettlement } from "../infra/sqlite-worker-ope
import { createDeferredCore } from "../shared/deferred.js";
import { createOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle.js";
import type { OpenClawStateAsyncLeaseContext } from "./openclaw-state-lease-context.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import { runWithOpenClawStateLeasesWorker } from "./openclaw-state-lease-worker-operation.js";
import {
createOpenClawStateLeaseWorkerOwner,
withOpenClawStateLeaseWorkerAdmission,
withOpenClawStateLeasesWorkerAdmission,
} from "./openclaw-state-lease-worker-owner.js";
import { runWithOpenClawStateLeasesWorker } from "./openclaw-state-lease-worker-storage.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerContext } from "./openclaw-state-worker-context.types.js";
const runWorkerOperation = vi.hoisted(() => vi.fn());

View file

@ -0,0 +1,66 @@
import type { OpenClawStateWorkerLeaseContext } from "./openclaw-state-lease-context.js";
import {
withOpenClawStateLeaseWorkerAdmission,
withOpenClawStateLeasesWorkerAdmission,
type OpenClawStateLeaseWorkerAuthority,
} from "./openclaw-state-lease-worker-owner.js";
import { prepareOpenClawStateLeaseStorageRuntime } from "./openclaw-state-lease-worker-storage.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerContext } from "./openclaw-state-worker-context.types.js";
import type { DomainScope } from "./openclaw-state-worker-store.types.js";
/** Both importers must resolve their transport before package replacement. */
export async function prepareOpenClawStateLeaseWorkerRuntime(): Promise<void> {
await Promise.all([
prepareOpenClawStateLeaseStorageRuntime(),
import("./openclaw-state-worker-store.js"),
]);
}
/** Retain the actual lease until every admitted worker transaction has settled. */
export function runWithOpenClawStateLeaseWorker<T>(
lease: OpenClawStateWorkerLeaseContext,
context: OpenClawStateWorkerContext,
operation: (scope: DomainScope, identity: OpenClawStateLeaseIdentity) => Promise<T>,
authority?: OpenClawStateLeaseWorkerAuthority,
): Promise<T> {
return withOpenClawStateLeaseWorkerAdmission(
lease,
context.admission.databasePath,
async (admission) => {
const { runOpenClawStateWorkerOperation } = await import("./openclaw-state-worker-store.js");
return runOpenClawStateWorkerOperation(
context,
(scope) => operation(scope, admission.identity),
{ assertCurrent: admission.assertCurrent, createAdmission: admission.createAdmission },
);
},
authority,
);
}
/** Share one actor operation while every original lease retains its native settlement. */
export function runWithOpenClawStateLeasesWorker<T>(
leases: readonly OpenClawStateWorkerLeaseContext[],
context: OpenClawStateWorkerContext,
operation: (scope: DomainScope, identities: readonly OpenClawStateLeaseIdentity[]) => Promise<T>,
authority?: OpenClawStateLeaseWorkerAuthority,
): Promise<T> {
return withOpenClawStateLeasesWorkerAdmission(
leases,
context,
async (admission) => {
const { runOpenClawStateWorkerOperation } = await import("./openclaw-state-worker-store.js");
admission.assertCurrent();
return runOpenClawStateWorkerOperation(
context,
(scope) => operation(scope, admission.identities),
{
assertCurrent: admission.assertCurrent,
createAdmission: admission.createAdmission,
},
);
},
authority,
);
}

View file

@ -16,7 +16,7 @@ import { createDeferredCore } from "../shared/deferred.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
import type { OpenClawStateWorkerLeaseContext } from "./openclaw-state-lease-context.js";
import { OpenClawStateLeaseError } from "./openclaw-state-lease-error.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerContext } from "./openclaw-state-worker-context.types.js";
export type OpenClawStateLeaseWorkerPurpose = "write" | "acquire" | "verify" | "renew" | "release";

View file

@ -1,31 +1,27 @@
import { throwSqliteLifecycleErrors } from "../infra/sqlite-lifecycle-errors.js";
import type { SqliteWorkerStore } from "../infra/sqlite-worker-store.js";
import type { OpenClawStateWorkerLeaseContext } from "./openclaw-state-lease-context.js";
import type { OpenClawStateLeaseLifecycleOperations } from "./openclaw-state-lease-context.js";
import { OpenClawStateLeaseError } from "./openclaw-state-lease-error.js";
import { leaseHeartbeatState } from "./openclaw-state-lease-heartbeat-shared.js";
import { startOpenClawStateLeaseTimer } from "./openclaw-state-lease-heartbeat.js";
import type {
createOpenClawStateLeaseWorkerOwner,
WorkerLeaseScope,
} from "./openclaw-state-lease-worker-owner.js";
import type {
OpenClawStateLeaseAcquisition,
OpenClawStateLeaseIdentity,
} from "./openclaw-state-lease-store.js";
import {
withOpenClawStateLeaseWorkerAdmission,
withOpenClawStateLeasesWorkerAdmission,
type createOpenClawStateLeaseWorkerOwner,
type OpenClawStateLeaseWorkerAuthority,
type WorkerLeaseScope,
} from "./openclaw-state-lease-worker-owner.js";
} from "./openclaw-state-lease.types.js";
import type { OpenClawStateWorkerContext } from "./openclaw-state-worker-context.types.js";
import type { OpenClawStateWorkerOperations } from "./openclaw-state-worker-contract.js";
type LeaseWorkerOwner = ReturnType<typeof createOpenClawStateLeaseWorkerOwner>;
type LeaseWorkerOperation<T> = (
scope: Pick<SqliteWorkerStore<OpenClawStateWorkerOperations>, "execute">,
scope: Pick<SqliteWorkerStore<OpenClawStateLeaseLifecycleOperations>, "execute">,
identity: OpenClawStateLeaseIdentity,
) => Promise<T>;
/** Keep this importer's admission and release code available across package replacement. */
export async function prepareOpenClawStateLeaseWorkerRuntime(): Promise<void> {
export async function prepareOpenClawStateLeaseStorageRuntime(): Promise<void> {
await Promise.all([
import("./openclaw-state-worker-store.js"),
import("../infra/sqlite-worker-identity.js"),
@ -221,47 +217,3 @@ export function createOpenClawStateLeaseWorkerStorage(
};
return storage;
}
/** Retain the actual lease until every admitted worker transaction has settled. */
export function runWithOpenClawStateLeaseWorker<T>(
lease: OpenClawStateWorkerLeaseContext,
context: OpenClawStateWorkerContext,
operation: LeaseWorkerOperation<T>,
authority?: OpenClawStateLeaseWorkerAuthority,
): Promise<T> {
return withOpenClawStateLeaseWorkerAdmission(
lease,
context.admission.databasePath,
admittedWorkerOperation(context, operation),
authority,
);
}
/** Share one actor operation while every original lease retains its native settlement. */
export function runWithOpenClawStateLeasesWorker<T>(
leases: readonly OpenClawStateWorkerLeaseContext[],
context: OpenClawStateWorkerContext,
operation: (
scope: Pick<SqliteWorkerStore<OpenClawStateWorkerOperations>, "execute">,
identities: readonly OpenClawStateLeaseIdentity[],
) => Promise<T>,
authority?: OpenClawStateLeaseWorkerAuthority,
): Promise<T> {
return withOpenClawStateLeasesWorkerAdmission(
leases,
context,
async (admission) => {
const { runOpenClawStateWorkerOperation } = await import("./openclaw-state-worker-store.js");
admission.assertCurrent();
return runOpenClawStateWorkerOperation(
context,
(scope) => operation(scope, admission.identities),
{
assertCurrent: admission.assertCurrent,
createAdmission: admission.createAdmission,
},
);
},
authority,
);
}

View file

@ -2,8 +2,8 @@ import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it, vi } from "vitest";
import { disposeNodeSqliteDependents } from "../infra/kysely-sync-cache-state.js";
import { requestSqliteWorkerOperationAdmission } from "../infra/sqlite-worker-operation-admission.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import { assertOpenClawStateLeasesWorkerOwnedInTransaction } from "./openclaw-state-lease-worker.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
vi.mock("../infra/sqlite-worker-operation-admission.js", () => ({
requestSqliteWorkerOperationAdmission: vi.fn(),

View file

@ -32,8 +32,8 @@ import {
reclaimDeadOpenClawStateLeaseInTransaction,
releaseOpenClawStateLeaseInTransaction,
renewOpenClawStateLeaseInTransaction,
type OpenClawStateLeaseIdentity,
} from "./openclaw-state-lease-store.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease.types.js";
function takeLeaseExpiryObservation(identity: OpenClawStateLeaseIdentity): BigInt64Array {
const attachment = takeSqliteWorkerOperationAdmissionAttachment();

View file

@ -0,0 +1,4 @@
export type OpenClawStateLeaseIdentity = { scope: string; key: string; owner: string };
export type OpenClawStateLeaseAcquisition =
| { kind: "acquired"; expiresAt: number }
| { kind: "held"; holder: { owner: string; epoch: number; expiresAt: number | null } };

View file

@ -2,7 +2,7 @@
// oxfmt-ignore
import { emptyReply, queueTask, source } from "./openclaw-state-read-worker.test-harness.js";
import { expect, it } from "vitest";
import type { AcpSessionReadInput } from "../acp/runtime/session-meta-keys.js";
import type { AcpSessionReadInput } from "../acp/runtime/session-meta-read.types.js";
import { createDeferredCore } from "../shared/deferred.js";
import { captureOpenClawStateReadSource } from "./openclaw-state-read-worker.js";
import { captureOpenClawStateWorkerContext } from "./openclaw-state-worker-context.js";

View file

@ -1,6 +1,6 @@
import type { DatabaseSync } from "node:sqlite";
import type { Selectable } from "kysely";
import type { AcpSessionReadInput, AcpSessionRow } from "../acp/runtime/session-meta-keys.js";
import type { AcpSessionReadInput, AcpSessionRow } from "../acp/runtime/session-meta-read.types.js";
import type { McpOAuthReadOnlyOperations } from "../agents/mcp-oauth-store.kernel.js";
import type {
SandboxBrowserRegistryEntry,

View file

@ -1,6 +1,3 @@
import type { ZodIssue } from "zod";
import type { AcpSessionWriteOperations } from "../acp/runtime/session-meta-write.types.js";
import type { AuthProfileRowRead, UserModelAuthProfile } from "../agents/auth-profiles/types.js";
import type { NativeHookRelayStoreWorkerOperations } from "../agents/harness/native-hook-relay-store.worker-contract.js";
import type { McpOAuthReadOperations } from "../agents/mcp-oauth-store.kernel.js";
import type { McpOAuthWriteOperations } from "../agents/mcp-oauth-store.types.js";
@ -41,12 +38,6 @@ import type { PlacementSessionToolWorkerOperations } from "../gateway/worker-env
import type { PlacementTurnClaimWorkerOperations } from "../gateway/worker-environments/placement-turn-claims.worker-contract.js";
import type { WorkspaceJournalWorkerOperations } from "../gateway/worker-environments/placement-workspace-journal.worker-contract.js";
import type { WorkerEnvironmentWorkerOperations } from "../gateway/worker-environments/store-worker-contract.js";
import type {
DeferredPluginMigration,
DeferredPluginMigrationRecordInput,
recordDeferredPluginMigrationsInTransaction,
readDeferredPluginMigrationCompletions,
} from "../infra/deferred-plugin-migrations.js";
import type { DeliveryQueueWorkerOperations } from "../infra/delivery-queue.worker-contract.js";
import type * as deviceAuth from "../infra/device-auth-store.kernel.js";
import type { DeviceIdentity } from "../infra/device-identity-store.js";
@ -75,10 +66,7 @@ import type { readRemoteModelCatalog } from "../model-catalog/remote-store.js";
import type { NodeWorkerJournalWorkerOperations } from "../node-host/node-worker-journal.worker-contract.js";
import type { PluginBlobWorkerOperations } from "../plugin-state/plugin-blob-worker-contract.js";
import type { PluginStateWorkerOperations } from "../plugin-state/plugin-state-worker-contract.js";
import type { PluginBindingApprovalEntry } from "../plugins/conversation-binding-state.types.js";
import type { PluginMetadataStateSelector } from "../plugins/installed-plugin-index-row.js";
import type { HostedCatalogSnapshotWorkerOperations } from "../plugins/official-external-plugin-catalog-snapshot-store.worker-contract.js";
import type { PluginSourceAdmissionPublication } from "../plugins/plugin-source-admission.types.js";
import type { ProjectRegistryWorkerOperations } from "../projects/project-registry.worker-contract.js";
import type { CaptureWorkerOperations } from "../proxy-capture/store.worker-contract.js";
import type { SecretStoreConfigRefWrite } from "../secrets/store/secret-store-config-ref.kernel.js";
@ -87,15 +75,9 @@ import type { SessionStateWorkerOperations } from "../sessions/session-state-eve
import type { SessionUpstreamLink } from "../sessions/session-upstream-links.kernel.js";
import type { SessionUpstreamWorkerOperations } from "../sessions/session-upstream-links.worker-contract.js";
import type { DeviceAuthEntry } from "../shared/device-auth.js";
import type { SkillUploadWorkerOperations } from "../skills/lifecycle/upload-store.worker.js";
import type * as curator from "../skills/workshop/curator.kernel.js";
import type { listStoredSkillProposalEventsInDatabase } from "../skills/workshop/store-sqlite-event.js";
import type { SkillWorkshopExecutionOperations } from "../skills/workshop/store.worker-contract.js";
import type { SkillUploadWorkerOperations } from "../skills/lifecycle/upload-store.worker-contract.js";
import type { SkillProposalEvent, SkillProposalRecord } from "../skills/workshop/types.js";
import type {
TranscriptReadOperations,
TranscriptWriteOperations,
} from "../transcripts/store-worker-contract.js";
import type { TranscriptReadOperations } from "../transcripts/store-worker-contract.js";
import type { TuiLastSessionWorkerOperations } from "../tui/tui-last-session.contract.js";
import type { AgentProvenance } from "./agent-provenance.types.js";
import type { PreparedBackupRunRecord } from "./backup-run-records.kernel.js";
@ -106,7 +88,6 @@ import type {
import type { OnboardingRecommendationWriteOperations } from "./onboarding-recommendations.contract.js";
import type { OpenClawAgentDatabaseWorkerLeaseReceipt } from "./openclaw-agent-db-lease.js";
import type { OpenClawStateLeaseLifecycleOperations } from "./openclaw-state-lease-context.js";
import type { OpenClawStateLeaseIdentity } from "./openclaw-state-lease-store.js";
import type { RegisteredStateWorkerOperations } from "./openclaw-state-worker-registry.js";
import type { RepositoryWorkspaceWorkerOperations } from "./session-repository-workspaces.types.js";
import type { UserPreferenceWorkerOperations } from "./user-preferences.types.js";
@ -121,11 +102,9 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
RepositoryWorkspaceWorkerOperations &
CaptureWorkerOperations &
TuiLastSessionWorkerOperations &
AcpSessionWriteOperations &
SessionStateWorkerOperations &
SessionUpstreamWorkerOperations &
McpOAuthReadOperations &
SkillWorkshopExecutionOperations &
CurrentConversationBindingWorkerOperations &
McpOAuthWriteOperations &
LegacyMcpOAuthWorkerOperations &
@ -135,7 +114,6 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
AuditWriterOperations &
NativeHookRelayStoreWorkerOperations &
TelemetryWorkerOperations &
HostedCatalogSnapshotWorkerOperations &
PluginStateWorkerOperations &
PluginBlobWorkerOperations &
UserPreferenceWorkerOperations &
@ -153,9 +131,7 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
SessionDeliveryWorkerOperations &
DeliveryQueueWorkerOperations &
TranscriptReadOperations &
TranscriptWriteOperations &
NodeWorkerJournalWorkerOperations &
SkillUploadWorkerOperations &
OpenClawStateLeaseLifecycleOperations &
ManagedImageRecordWorkerOperations & {
"database.walMaintenance": { input: SqliteWalPeriodicRequest; output: SqliteWalPeriodicResult };
@ -224,12 +200,6 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
output: ReturnType<typeof deviceAuth.clearOriginDeviceTokenInDatabase>;
};
"authProfiles.read": { input: { artifactPreserving: boolean }; output: AuthProfileRowRead };
"authProfiles.sharedOwnership": { input: { artifactPreserving: boolean }; output: unknown };
"authProfiles.personal": {
input: { profileId: string; artifactPreserving: boolean };
output: UserModelAuthProfile | undefined;
};
"agentProvenance.readBatch": {
input: { agentIds: readonly string[] };
output: AgentProvenance[];
@ -253,15 +223,6 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
input: SessionGroupCatalogMutation;
output: SessionGroupCatalogMutationResult;
};
"skills.curator.read": {
input: { skillFiles: readonly string[] };
output: ReturnType<typeof curator.readSkillCuratorStateInDatabase>;
};
"skills.usage.record": { input: curator.PreparedSkillUsage; output: void };
"workshop.events.list": {
input: Parameters<typeof listStoredSkillProposalEventsInDatabase>[1];
output: ReturnType<typeof listStoredSkillProposalEventsInDatabase>;
};
"doctor.workshopMigrationRecords.read": {
input: { includeEvents: boolean };
output:
@ -275,42 +236,10 @@ export type OpenClawStateWorkerOperations = RegisteredStateWorkerOperations &
input: { artifactPreservingReadOnly: boolean };
output: ReturnType<typeof readRemoteModelCatalog>;
};
"plugins.conversationBindingApprovals.read": {
input: undefined;
output: PluginBindingApprovalEntry[];
};
"plugins.conversationBindingApprovals.upsert": {
input: PluginBindingApprovalEntry;
output: void;
};
"plugins.metadata.read": {
input: { selector: PluginMetadataStateSelector; artifactPreservingReadOnly?: boolean };
output: { value_json: string } | undefined;
};
"plugins.metadata.sourceAdmission.publish": {
input: PluginSourceAdmissionPublication;
output: boolean;
};
"plugins.deferredMigrations.record": {
input: Omit<DeferredPluginMigrationRecordInput, "env"> & {
identity: OpenClawStateLeaseIdentity;
};
output:
| {
kind: "recorded";
transitions: ReturnType<typeof recordDeferredPluginMigrationsInTransaction>;
}
| { kind: "conflict"; pending: readonly DeferredPluginMigration[] }
| { kind: "invalid"; issues: ZodIssue[] };
};
"plugins.deferredMigrations.read": {
input: { artifactPreservingReadOnly: boolean };
output: readonly DeferredPluginMigration[];
};
"plugins.deferredMigrations.completions.read": {
input: undefined;
output: ReturnType<typeof readDeferredPluginMigrationCompletions>;
};
"claws.install-schema-versions": {
input: { artifactPreservingReadOnly: boolean };
output: ClawInstallSchemaVersionRow[] | undefined;

View file

@ -1,15 +1,43 @@
import type { AcpSessionWriteOperations } from "../acp/runtime/session-meta-write.worker-contract.js";
import type { AuthProfileWorkerOperations } from "../agents/auth-profiles/store.worker-contract.js";
import type { WorktreeWorkerOperations } from "../agents/worktrees/dispatch.worker.js";
import type { FleetRegistryWriteOperations } from "../fleet/registry.worker-contract.js";
import type { ApnsRegistrationWorkerOperations } from "../infra/push-apns-store.worker-contract.js";
import type { WebPushWorkerOperations } from "../infra/push-web-store.worker-contract.js";
import type { PluginRuntimeWorkerOperations } from "../plugins/state.worker-contract.js";
import type { SkillUploadWorkerOperations } from "../skills/lifecycle/upload-store.worker-contract.js";
import type {
SkillWorkshopWorkerOperations,
SkillCuratorOperations,
} from "../skills/workshop/store.worker-contract.js";
import type { TranscriptWriteOperations } from "../transcripts/store-write.worker-contract.js";
import { createWorkerOperationRegistry } from "./worker-operation-registry.js";
export type RegisteredStateWorkerOperations = WebPushWorkerOperations &
ApnsRegistrationWorkerOperations &
WorktreeWorkerOperations &
FleetRegistryWriteOperations;
FleetRegistryWriteOperations &
AcpSessionWriteOperations &
SkillUploadWorkerOperations &
SkillWorkshopWorkerOperations &
SkillCuratorOperations &
TranscriptWriteOperations &
AuthProfileWorkerOperations &
PluginRuntimeWorkerOperations;
export const stateWorkerRegistry = createWorkerOperationRegistry<RegisteredStateWorkerOperations>({
authProfiles: () =>
import("../agents/auth-profiles/store.worker.js").then((m) => m.authProfileOperations),
plugins: () => import("../plugins/state.worker.js").then((m) => m.pluginRuntimeOperations),
acp: () =>
import("../acp/runtime/session-meta-write.worker.js").then((m) => m.acpSessionOperations),
skillUploads: () =>
import("../skills/lifecycle/upload-store.worker.js").then((m) => m.skillUploadOperations),
workshop: () =>
import("../skills/workshop/store.worker.js").then((m) => m.skillWorkshopOperations),
skills: () => import("../skills/workshop/store.worker.js").then((m) => m.skillCuratorOperations),
transcripts: () =>
import("../transcripts/store-worker-write.js").then((m) => m.transcriptWriteOperations),
webPush: () => import("../infra/push-web-store.worker.js").then((m) => m.webPushOperations),
apns: () => import("../infra/push-apns-store.worker.js").then((m) => m.apnsOperations),
worktrees: () =>

View file

@ -1,10 +1,3 @@
import { executeAcpSessionMutationInWorker } from "../acp/runtime/session-meta-write.worker.js";
import {
readAuthProfileRows,
SHARED_AUTH_STORE_STATE_KEY,
} from "../agents/auth-profiles/sqlite-json.js";
import { isMissingDatabasePath } from "../agents/auth-profiles/sqlite-read-pool.js";
import type { AuthProfileRowRead } from "../agents/auth-profiles/types.js";
import {
readNativeHookRelayBridgeSnapshotFromDatabase,
listNativeHookRelayBridgeSnapshotsInDatabase,
@ -58,10 +51,6 @@ import { isWorkspaceJournalWriteCommand } from "../gateway/worker-environments/p
import { executeWorkspaceJournalCommand } from "../gateway/worker-environments/placement-workspace-journal.worker.js";
import { isWorkerEnvironmentCommand } from "../gateway/worker-environments/store-worker-contract.js";
import { executeWorkerEnvironmentCommand } from "../gateway/worker-environments/store.worker.js";
import {
readDeferredPluginMigrationsInWorker,
recordDeferredPluginMigrationsInWorker,
} from "../infra/deferred-plugin-migrations.worker.js";
import * as deliveryQueue from "../infra/delivery-queue.worker.js";
import * as deviceAuth from "../infra/device-auth-store.kernel.js";
import { executeDevicePairingMutationInWorker } from "../infra/device-pairing-dispatch.worker.js";
@ -95,15 +84,6 @@ import { isNodeWorkerJournalCommand } from "../node-host/node-worker-journal.wor
import { executeNodeWorkerJournalCommand } from "../node-host/node-worker-journal.worker.js";
import { executePluginBlobCommand } from "../plugin-state/plugin-blob-store.worker.js";
import { isPluginBlobWorkerCommand } from "../plugin-state/plugin-blob-worker-contract.js";
import {
readPluginBindingApprovalsInDatabase,
upsertPluginBindingApprovalInDatabase,
} from "../plugins/conversation-binding-state.kernel.js";
import {
readHostedCatalogSnapshotInDatabase,
writeHostedCatalogSnapshotInDatabase,
} from "../plugins/official-external-plugin-catalog-snapshot-store.kernel.js";
import { HostedCatalogSignedFeedMonotonicityError } from "../plugins/official-external-plugin-catalog-source.js";
import {
executeProjectRegistryCommand,
isProjectRegistryCommand,
@ -113,17 +93,7 @@ import { purgeExpiredSecretStoreEntriesInDatabase } from "../secrets/store/secre
import { executeSessionStateCommand } from "../sessions/session-state-events.worker.js";
import { listWatchedSessionUpstreamLinksInDatabase } from "../sessions/session-upstream-links.kernel.js";
import { executeSessionUpstreamCommand } from "../sessions/session-upstream-links.worker.js";
import { createLazyRuntimeModule } from "../shared/lazy-runtime.js";
import {
isSkillUploadCommand,
executeSkillUploadCommand,
} from "../skills/lifecycle/upload-store.worker.js";
import * as skillWorkshop from "../skills/workshop/store.worker.js";
import { executeTranscriptRead } from "../transcripts/store-worker-read.js";
import {
executeTranscriptWrite,
isTranscriptWriteCommand,
} from "../transcripts/store-worker-write.js";
import { clearRetiredTuiPointers } from "../tui/tui-last-session.kernel.js";
import {
listAgentProvenanceInDatabase,
@ -132,7 +102,6 @@ import {
import { ensureAgentProvenanceSchema } from "./agent-provenance.schema.js";
import { recordBackupRunInDatabase } from "./backup-run-records.kernel.js";
import { writeConfigMachineState } from "./config-machine-state-write.js";
import { readConfigMachineState } from "./config-machine-state.js";
import {
deletePersonalGitHubSessionReceiptsInDatabase,
readSessionReceiptDeletionIdentitiesInDatabase,
@ -157,23 +126,12 @@ import {
executeRepositoryWorkspaceCommand,
isRepositoryWorkspaceCommand,
} from "./session-repository-workspaces.worker.js";
import { readUserModelAuthProfile } from "./user-model-accounts.js";
import { executeUserPreferenceCommand } from "./user-preferences.worker.js";
import { executeUserProfileCommand, isUserProfileCommand } from "./user-profiles.worker.js";
const log = createSubsystemLogger("state/worker");
const loadPluginIndexWriter = createLazyRuntimeModule(
() => import("../plugins/installed-plugin-index-store-write.js"),
);
let pluginIndexWriter: Awaited<ReturnType<typeof loadPluginIndexWriter>> | undefined;
export function prepareSharedStateCommand(type: PropertyKey): Promise<void> | undefined {
if (type === "plugins.metadata.sourceAdmission.publish" && !pluginIndexWriter) {
return loadPluginIndexWriter().then((loaded) => {
pluginIndexWriter = loaded;
});
}
return stateWorkerRegistry.prepare(type) ?? prepareCronStateWorkerCommand(type);
}
@ -238,43 +196,6 @@ export function executeSharedStateCommand(
if (command.type === "audit.writer.process" || command.type === "audit.writer.prune") {
return executeAuditWriterCommand(command, stateOptions(), open);
}
if (
command.type === "authProfiles.read" ||
command.type === "authProfiles.sharedOwnership" ||
command.type === "authProfiles.personal"
) {
const read = () => {
const options = stateOptions();
if (command.type === "authProfiles.sharedOwnership") {
return readConfigMachineState(SHARED_AUTH_STORE_STATE_KEY, options);
}
if (command.type === "authProfiles.personal") {
return readUserModelAuthProfile(command.input.profileId, options);
}
const missing: AuthProfileRowRead = {
store: { status: "missing", reason: "database" },
state: { status: "missing", reason: "database" },
cacheable: false,
};
try {
return (
withExistingOpenClawStateDatabaseReadOnly(
({ db }) => readAuthProfileRows(db, context.databasePath, "shared-state"),
options,
) ?? missing
);
} catch {
return isMissingDatabasePath(context.databasePath)
? missing
: {
store: { status: "unreadable" as const },
state: { status: "unreadable" as const },
cacheable: false,
};
}
};
return command.input.artifactPreserving ? withArtifactPreservingStateReads(read) : read();
}
if (command.type === "promotions.markNotified" || command.type === "promotions.recordClaim") {
return executePromotionCommand(command, stateOptions(), open);
}
@ -314,19 +235,6 @@ export function executeSharedStateCommand(
? withArtifactPreservingStateReads(read)
: read();
}
if (command.type === "acp.prepareMutation" || command.type === "acp.commitMutation") {
return executeAcpSessionMutationInWorker(open(), command);
}
if (command.type === "plugins.conversationBindingApprovals.read") {
return readPluginBindingApprovalsInDatabase(open().db);
}
if (command.type === "plugins.conversationBindingApprovals.upsert") {
const database = open();
return runOpenClawStateWriteTransaction(
({ db }) => upsertPluginBindingApprovalInDatabase(db, command.input),
{ database, path: context.databasePath, env: getSqliteWorkerStateContext().environment },
);
}
if (command.type === "updateRuns.recordStep" || command.type === "updateRuns.recordPhase") {
return recordUpdateRunMutationInWorker(command, stateOptions(), (stage) =>
requestSqliteWorkerOperationAdmission({ stage, facts: undefined }),
@ -342,12 +250,6 @@ export function executeSharedStateCommand(
requestSqliteWorkerOperationAdmission({ stage, facts: undefined }),
);
}
if (
command.type === "plugins.deferredMigrations.read" ||
command.type === "plugins.deferredMigrations.completions.read"
) {
return readDeferredPluginMigrationsInWorker(command, stateOptions());
}
if (command.type === "claws.install-schema-versions") {
const read = command.input.artifactPreservingReadOnly
? withExistingOpenClawStateDatabaseArtifactPreservingReadOnly
@ -431,15 +333,9 @@ export function executeSharedStateCommand(
if (command.type === "githubRepository.personalPending") {
return readPendingRepositoryGitHubPublicationInDatabase(database.db, command.input);
}
if (skillWorkshop.isSkillWorkshopCommand(command)) {
return skillWorkshop.executeSkillWorkshopCommand(command, database, context.databasePath);
}
if (command.type === "deviceAuth.list") {
return deviceAuth.readDeviceAuthTokensFromDatabase(database.db, command.input);
}
if (isTranscriptWriteCommand(command)) {
return executeTranscriptWrite(command, { database, path: context.databasePath });
}
switch (command.type) {
case "transcripts.canonicalSessionRow":
case "transcripts.readEntries":
@ -467,9 +363,6 @@ export function executeSharedStateCommand(
if (isManagedImageRecordCommand(command)) {
return executeManagedImageRecordCommand(command, database);
}
if (command.type === "plugins.catalogSnapshot.read") {
return readHostedCatalogSnapshotInDatabase(database.db, command.input.url);
}
if (command.type === "nativeHookRelay.listSnapshots") {
return listNativeHookRelayBridgeSnapshotsInDatabase(database);
}
@ -486,9 +379,6 @@ export function executeSharedStateCommand(
database,
...stateOptions(),
};
if (command.type === "plugins.deferredMigrations.record") {
return recordDeferredPluginMigrationsInWorker(command.input, writeOptions);
}
if (
command.type === "nativeHookRelay.write" ||
command.type === "nativeHookRelay.renew" ||
@ -538,9 +428,6 @@ export function executeSharedStateCommand(
if (deliveryQueue.isDeliveryQueueCommand(command)) {
return deliveryQueue.executeDeliveryQueueCommand(command, writeOptions);
}
if (isSkillUploadCommand(command)) {
return executeSkillUploadCommand(command, writeOptions);
}
if (
command.type === "deviceAuth.store" ||
command.type === "deviceAuth.storeOrigin" ||
@ -581,31 +468,6 @@ export function executeSharedStateCommand(
if (command.type === "sessionState.record" || command.type === "sessionState.prune") {
return executeSessionStateCommand(command, writeOptions);
}
if (command.type === "plugins.catalogSnapshot.write") {
try {
runOpenClawStateWriteTransaction(
({ db }) =>
writeHostedCatalogSnapshotInDatabase(db, command.input.snapshot, command.input.now),
writeOptions,
);
return { ok: true };
} catch (error) {
if (error instanceof HostedCatalogSignedFeedMonotonicityError) {
return { ok: false, message: error.message };
}
throw error;
}
}
if (command.type === "plugins.metadata.sourceAdmission.publish") {
if (!pluginIndexWriter) {
throw new Error("Plugin source admission writer is not prepared");
}
const { publishPluginSourceAdmissionInDatabase } = pluginIndexWriter;
return runOpenClawStateWriteTransaction(
({ db }) => publishPluginSourceAdmissionInDatabase(db, command.input),
writeOptions,
);
}
if (command.type === "subagents.persistChanges") {
const { writeId, values, deleteRunIds } = command.input;
let committed = false;

View file

@ -1,5 +1,5 @@
import { createDeferredCore } from "../shared/deferred.js";
import type { TranscriptAppendScheduler } from "./store-worker-contract.js";
import type { TranscriptAppendScheduler } from "./store-worker.types.js";
type AppendOutcome = { ok: true } | { ok: false; error: unknown };

View file

@ -1,17 +1,15 @@
import { createSqliteWorkerWriteAdmission } from "../infra/sqlite-worker-store.js";
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
import type { OpenClawStateLeaseContext } from "../state/openclaw-state-lease-context.js";
import { runWithOpenClawStateLeaseWorker } from "../state/openclaw-state-lease-worker-storage.js";
import { runWithOpenClawStateLeaseWorker } from "../state/openclaw-state-lease-worker-operation.js";
import { captureOpenClawStateWorkerContext } from "../state/openclaw-state-worker-context.js";
import type { OpenClawStateWorkerOperations } from "../state/openclaw-state-worker-contract.js";
import { runOpenClawStateWorkerOperation } from "../state/openclaw-state-worker-store.js";
import { prepareTranscriptDateReader } from "./store-date-preparation.js";
import { TranscriptLibraryError } from "./store-read.js";
import type {
TranscriptExportWriteKey,
TranscriptReadRequests,
TranscriptWriteOperations,
} from "./store-worker-contract.js";
import type { TranscriptReadRequests } from "./store-worker-contract.js";
import type { TranscriptExportWriteKey } from "./store-worker.types.js";
import type { TranscriptWriteOperations } from "./store-write.worker-contract.js";
/** One captured database generation spans planning, filesystem work, and persistence. */
export function createTranscriptStoreOperation(

View file

@ -1,5 +1,4 @@
import type { SqliteWorkerCommand } from "../infra/sqlite-worker-contract.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease-store.js";
import type { TranscriptSessionDescriptor, TranscriptSourceLocator } from "./provider-types.js";
import type {
queryTranscriptReadEntries,
@ -24,68 +23,12 @@ import type {
readTranscriptJsonlDigest,
} from "./store-sqlite-read.js";
import type {
writeMeetingTranscriptSessionInDatabase,
writeMeetingTranscriptSummaryInDatabase,
} from "./store-sqlite-write.js";
import type {
appendMeetingTranscriptUtterance,
readRecentStoppedTranscriptSession,
readTranscriptSummaryInputRevision,
} from "./store-sqlite.js";
type SessionIdentity = Pick<TranscriptSessionDescriptor, "sessionId" | "startedAt">;
/** Host-only capture scheduling; functions never cross the worker boundary. */
export type TranscriptAppendScheduler = (
write: (assertCurrent: () => void) => Promise<void>,
) => Promise<void>;
export type TranscriptWriteOperations = {
"transcripts.writeSession": {
input: Parameters<typeof writeMeetingTranscriptSessionInDatabase>[1] & { readOnly?: boolean };
output: { ok: true } | { ok: false; reason: "changed" | "conflict" };
};
"transcripts.markPendingExports": {
input: {
session: SessionIdentity;
fileNames: string[];
readOnly?: boolean;
lease?: OpenClawStateLeaseIdentity;
};
output: void;
};
"transcripts.recordExportManifest": {
input: {
session: SessionIdentity;
exportedHashes: Record<string, string>;
removedExports: string[];
readOnly?: boolean;
lease?: OpenClawStateLeaseIdentity;
};
output: void;
};
"transcripts.append": {
input: Omit<Parameters<typeof appendMeetingTranscriptUtterance>[0], "database"> & {
readOnly?: boolean;
};
output: void;
};
"transcripts.writeSummary": {
input: {
session: SessionIdentity;
summaryValues: Parameters<typeof writeMeetingTranscriptSummaryInDatabase>[2];
guard?: Parameters<typeof writeMeetingTranscriptSummaryInDatabase>[3];
readOnly?: boolean;
};
output: { ok: true } | { ok: false; reason: "changed" };
};
};
export type TranscriptWriteCommand = SqliteWorkerCommand<TranscriptWriteOperations>;
export type TranscriptExportWriteKey =
| "transcripts.markPendingExports"
| "transcripts.recordExportManifest";
export type TranscriptReadRequests = {
"transcripts.canonicalSessionRow": {
input: { selector: string };

View file

@ -1,10 +1,13 @@
import type { DatabaseSync } from "node:sqlite";
import { requestSqliteWorkerOperationAdmission } from "../infra/sqlite-worker-operation-admission.js";
import { getSqliteWorkerStateContext } from "../infra/sqlite-worker-state-context.js";
import {
runOpenClawStateWriteTransaction,
type OpenClawStateDatabase,
} from "../state/openclaw-state-db.js";
import { runOpenClawStateWriteTransaction } from "../state/openclaw-state-db.js";
import { assertOpenClawStateLeaseWorkerOwnedInTransaction } from "../state/openclaw-state-lease-worker.js";
import type { OpenClawStateLeaseIdentity } from "../state/openclaw-state-lease.types.js";
import type {
WorkerOperationContext,
WorkerOperationHandlers,
} from "../state/worker-operation-registry.js";
import type { TranscriptSessionDescriptor } from "./provider-types.js";
import { ensureMeetingTranscriptsSchema } from "./sqlite-schema.js";
import { transcriptSessionExportKey } from "./store-artifacts.js";
import { TranscriptSessionConflictError, TranscriptsSummaryChangedError } from "./store-errors.js";
@ -15,112 +18,126 @@ import {
writeMeetingTranscriptSummaryInDatabase,
} from "./store-sqlite-write.js";
import { appendMeetingTranscriptUtterance } from "./store-sqlite.js";
import type { TranscriptWriteCommand, TranscriptWriteOperations } from "./store-worker-contract.js";
const operationLabels: Record<TranscriptWriteCommand["type"], string> = {
"transcripts.append": "meeting-transcripts.utterance.append",
"transcripts.writeSummary": "meeting-transcripts.summary.write",
"transcripts.writeSession": "meeting-transcripts.session.write",
"transcripts.markPendingExports": "meeting-transcripts.export.pending",
"transcripts.recordExportManifest": "meeting-transcripts.export.record",
type SessionIdentity = Pick<TranscriptSessionDescriptor, "sessionId" | "startedAt">;
type ExportInput = {
session: SessionIdentity;
lease?: OpenClawStateLeaseIdentity;
readOnly?: boolean;
};
export function isTranscriptWriteCommand(command: {
type: string;
}): command is TranscriptWriteCommand {
return Object.hasOwn(operationLabels, command.type);
}
export function executeTranscriptWrite(
command: TranscriptWriteCommand,
target: { database: OpenClawStateDatabase; path: string },
): TranscriptWriteOperations[keyof TranscriptWriteOperations]["output"] {
const options = {
...target,
env: getSqliteWorkerStateContext().environment,
readOnly: command.input.readOnly,
};
function writeTranscript(
input: { readOnly?: boolean },
{ open, stateOptions }: WorkerOperationContext,
operationLabel: string,
write: (db: DatabaseSync) => void,
exportInput?: ExportInput,
) {
const options = { database: open(), ...stateOptions(), readOnly: input.readOnly };
ensureMeetingTranscriptsSchema(options);
try {
runOpenClawStateWriteTransaction(
({ db }) => {
const lease =
command.type === "transcripts.markPendingExports" ||
command.type === "transcripts.recordExportManifest"
? command.input.lease
: undefined;
const assertLease = () => {
if (!lease) {
return;
}
runOpenClawStateWriteTransaction(
({ db }) => {
const assertWrite = (stage: "transaction" | "commit") => {
if (exportInput?.lease) {
const { lease, session } = exportInput;
if (
lease.scope !== "meeting-transcript.export" ||
lease.key !== transcriptSessionExportKey(command.input.session)
lease.key !== transcriptSessionExportKey(session)
) {
throw new Error("Transcript export lease does not match its session");
}
assertOpenClawStateLeaseWorkerOwnedInTransaction(db, lease);
};
if (lease) {
assertLease();
} else {
requestSqliteWorkerOperationAdmission({ stage: "transaction", facts: undefined });
requestSqliteWorkerOperationAdmission({ stage, facts: undefined });
}
switch (command.type) {
case "transcripts.append":
appendMeetingTranscriptUtterance({ ...command.input, database: db });
break;
case "transcripts.writeSummary": {
const { session, summaryValues, guard } = command.input;
writeMeetingTranscriptSummaryInDatabase(db, session, summaryValues, guard);
break;
}
case "transcripts.writeSession":
writeMeetingTranscriptSessionInDatabase(db, command.input);
break;
case "transcripts.markPendingExports":
markMeetingTranscriptPendingExportsInDatabase(
db,
command.input.session,
command.input.fileNames,
);
break;
case "transcripts.recordExportManifest":
updateMeetingTranscriptExportManifestInDatabase(
db,
command.input.session,
command.input.exportedHashes,
new Set(command.input.removedExports),
);
break;
}
if (lease) {
assertLease();
} else {
requestSqliteWorkerOperationAdmission({ stage: "commit", facts: undefined });
}
},
options,
{ operationLabel: operationLabels[command.type] },
);
return command.type === "transcripts.writeSummary" ||
command.type === "transcripts.writeSession"
? { ok: true }
: undefined;
} catch (error) {
if (
(command.type === "transcripts.writeSummary" ||
command.type === "transcripts.writeSession") &&
error instanceof TranscriptsSummaryChangedError
) {
return { ok: false, reason: "changed" };
}
if (
command.type === "transcripts.writeSession" &&
error instanceof TranscriptSessionConflictError
) {
return { ok: false, reason: "conflict" };
}
throw error;
}
};
assertWrite("transaction");
write(db);
assertWrite("commit");
},
options,
{ operationLabel },
);
}
export const transcriptWriteOperations = {
"transcripts.append": (
input: Omit<Parameters<typeof appendMeetingTranscriptUtterance>[0], "database"> & {
readOnly?: boolean;
},
context,
) =>
writeTranscript(input, context, "meeting-transcripts.utterance.append", (db) =>
appendMeetingTranscriptUtterance({ ...input, database: db }),
),
"transcripts.writeSummary": (
input: {
session: SessionIdentity;
summaryValues: Parameters<typeof writeMeetingTranscriptSummaryInDatabase>[2];
guard?: Parameters<typeof writeMeetingTranscriptSummaryInDatabase>[3];
readOnly?: boolean;
},
context,
) => {
try {
writeTranscript(input, context, "meeting-transcripts.summary.write", (db) =>
writeMeetingTranscriptSummaryInDatabase(
db,
input.session,
input.summaryValues,
input.guard,
),
);
return { ok: true } as const;
} catch (error) {
if (error instanceof TranscriptsSummaryChangedError) {
return { ok: false, reason: "changed" } as const;
}
throw error;
}
},
"transcripts.writeSession": (
input: Parameters<typeof writeMeetingTranscriptSessionInDatabase>[1] & { readOnly?: boolean },
context,
) => {
try {
writeTranscript(input, context, "meeting-transcripts.session.write", (db) =>
writeMeetingTranscriptSessionInDatabase(db, input),
);
return { ok: true } as const;
} catch (error) {
if (error instanceof TranscriptsSummaryChangedError) {
return { ok: false, reason: "changed" } as const;
}
if (error instanceof TranscriptSessionConflictError) {
return { ok: false, reason: "conflict" } as const;
}
throw error;
}
},
"transcripts.markPendingExports": (input: ExportInput & { fileNames: string[] }, context) =>
writeTranscript(
input,
context,
"meeting-transcripts.export.pending",
(db) => markMeetingTranscriptPendingExportsInDatabase(db, input.session, input.fileNames),
input,
),
"transcripts.recordExportManifest": (
input: ExportInput & { exportedHashes: Record<string, string>; removedExports: string[] },
context,
) =>
writeTranscript(
input,
context,
"meeting-transcripts.export.record",
(db) =>
updateMeetingTranscriptExportManifestInDatabase(
db,
input.session,
input.exportedHashes,
new Set(input.removedExports),
),
input,
),
} satisfies WorkerOperationHandlers;

View file

@ -0,0 +1,8 @@
/** Host-only capture scheduling; functions never cross the worker boundary. */
export type TranscriptAppendScheduler = (
write: (assertCurrent: () => void) => Promise<void>,
) => Promise<void>;
export type TranscriptExportWriteKey =
| "transcripts.markPendingExports"
| "transcripts.recordExportManifest";

View file

@ -0,0 +1,4 @@
import type { WorkerOperations } from "../state/worker-operation-registry.js";
import type { transcriptWriteOperations } from "./store-worker-write.js";
export type TranscriptWriteOperations = WorkerOperations<typeof transcriptWriteOperations>;

View file

@ -1,4 +1,3 @@
// Stores meeting-capture transcripts in the shared SQLite state database.
import fs from "node:fs/promises";
import path from "node:path";
import type { TranscriptUtterance as ProjectedTranscriptUtterance } from "../../packages/gateway-protocol/src/schema/transcripts.js";
@ -50,11 +49,10 @@ import {
createTranscriptStoreOperation,
type TranscriptStoreOperation,
} from "./store-worker-client.js";
import type {
TranscriptAppendScheduler,
TranscriptReadRequests,
TranscriptWriteOperations,
} from "./store-worker-contract.js";
import type { TranscriptReadRequests } from "./store-worker-contract.js";
import type { TranscriptAppendScheduler } from "./store-worker.types.js";
// Stores meeting-capture transcripts in the shared SQLite state database.
import type { TranscriptWriteOperations } from "./store-write.worker-contract.js";
import type { TranscriptsSummary } from "./summary.js";
import { renderTranscriptsMarkdown } from "./summary.js";