mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 06:11:24 +00:00
refactor: move tasks into bundled plugin
This commit is contained in:
@@ -39,6 +39,7 @@ export type BuildPluginApiParams = {
|
||||
| "registerMemoryFlushPlan"
|
||||
| "registerMemoryRuntime"
|
||||
| "registerMemoryEmbeddingProvider"
|
||||
| "registerOperationsRuntime"
|
||||
| "on"
|
||||
>
|
||||
>;
|
||||
@@ -69,6 +70,7 @@ const noopRegisterMemoryFlushPlan: OpenClawPluginApi["registerMemoryFlushPlan"]
|
||||
const noopRegisterMemoryRuntime: OpenClawPluginApi["registerMemoryRuntime"] = () => {};
|
||||
const noopRegisterMemoryEmbeddingProvider: OpenClawPluginApi["registerMemoryEmbeddingProvider"] =
|
||||
() => {};
|
||||
const noopRegisterOperationsRuntime: OpenClawPluginApi["registerOperationsRuntime"] = () => {};
|
||||
const noopOn: OpenClawPluginApi["on"] = () => {};
|
||||
|
||||
export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi {
|
||||
@@ -112,6 +114,7 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
|
||||
registerMemoryRuntime: handlers.registerMemoryRuntime ?? noopRegisterMemoryRuntime,
|
||||
registerMemoryEmbeddingProvider:
|
||||
handlers.registerMemoryEmbeddingProvider ?? noopRegisterMemoryEmbeddingProvider,
|
||||
registerOperationsRuntime: handlers.registerOperationsRuntime ?? noopRegisterOperationsRuntime,
|
||||
resolvePath: params.resolvePath,
|
||||
on: handlers.on ?? noopOn,
|
||||
};
|
||||
|
||||
@@ -48,5 +48,6 @@ describe("captured plugin registration", () => {
|
||||
expect(captured.tools.map((tool) => tool.name)).toEqual(["captured-tool"]);
|
||||
expect(captured.providers.map((provider) => provider.id)).toEqual(["captured-provider"]);
|
||||
expect(captured.api.registerMemoryEmbeddingProvider).toBeTypeOf("function");
|
||||
expect(captured.api.registerOperationsRuntime).toBeTypeOf("function");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,10 @@ import {
|
||||
registerMemoryRuntime,
|
||||
resolveMemoryFlushPlan,
|
||||
} from "./memory-state.js";
|
||||
import {
|
||||
getRegisteredOperationsRuntime,
|
||||
registerOperationsRuntimeForOwner,
|
||||
} from "./operations-state.js";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
import {
|
||||
getActivePluginRegistry,
|
||||
@@ -1461,6 +1465,181 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
||||
expect(listMemoryEmbeddingProviders()).toEqual([]);
|
||||
});
|
||||
|
||||
it("restores the active operations runtime during snapshot loads", () => {
|
||||
const activeRuntime = {
|
||||
async dispatch() {
|
||||
return { matched: true, created: true, record: null };
|
||||
},
|
||||
async getById() {
|
||||
return null;
|
||||
},
|
||||
async findByRunId() {
|
||||
return null;
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
},
|
||||
async summarize() {
|
||||
return {
|
||||
total: 0,
|
||||
active: 0,
|
||||
terminal: 0,
|
||||
failures: 0,
|
||||
byNamespace: { active: 0 },
|
||||
byKind: {},
|
||||
byStatus: {},
|
||||
};
|
||||
},
|
||||
async audit() {
|
||||
return [];
|
||||
},
|
||||
async maintenance() {
|
||||
return {
|
||||
reconciled: 0,
|
||||
cleanupStamped: 0,
|
||||
pruned: 0,
|
||||
};
|
||||
},
|
||||
async cancel() {
|
||||
return { found: false, cancelled: false, reason: "active" };
|
||||
},
|
||||
};
|
||||
registerOperationsRuntimeForOwner(activeRuntime, "active-operations");
|
||||
const plugin = writePlugin({
|
||||
id: "snapshot-operations",
|
||||
filename: "snapshot-operations.cjs",
|
||||
body: `module.exports = {
|
||||
id: "snapshot-operations",
|
||||
register(api) {
|
||||
api.registerOperationsRuntime({
|
||||
async dispatch() {
|
||||
return { matched: true, created: true, record: null };
|
||||
},
|
||||
async getById() {
|
||||
return null;
|
||||
},
|
||||
async findByRunId() {
|
||||
return null;
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
},
|
||||
async summarize() {
|
||||
return {
|
||||
total: 1,
|
||||
active: 1,
|
||||
terminal: 0,
|
||||
failures: 0,
|
||||
byNamespace: { snapshot: 1 },
|
||||
byKind: { snapshot: 1 },
|
||||
byStatus: { queued: 1 },
|
||||
};
|
||||
},
|
||||
async audit() {
|
||||
return [];
|
||||
},
|
||||
async maintenance() {
|
||||
return {
|
||||
reconciled: 0,
|
||||
cleanupStamped: 0,
|
||||
pruned: 0,
|
||||
};
|
||||
},
|
||||
async cancel() {
|
||||
return { found: false, cancelled: false, reason: "snapshot" };
|
||||
},
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const scoped = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
activate: false,
|
||||
workspaceDir: plugin.dir,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["snapshot-operations"],
|
||||
},
|
||||
},
|
||||
onlyPluginIds: ["snapshot-operations"],
|
||||
});
|
||||
|
||||
expect(scoped.plugins.find((entry) => entry.id === "snapshot-operations")?.status).toBe(
|
||||
"loaded",
|
||||
);
|
||||
expect(getRegisteredOperationsRuntime()).toBe(activeRuntime);
|
||||
});
|
||||
|
||||
it("clears newly-registered operations runtime when plugin register fails", () => {
|
||||
const plugin = writePlugin({
|
||||
id: "failing-operations",
|
||||
filename: "failing-operations.cjs",
|
||||
body: `module.exports = {
|
||||
id: "failing-operations",
|
||||
register(api) {
|
||||
api.registerOperationsRuntime({
|
||||
async dispatch() {
|
||||
return { matched: true, created: true, record: null };
|
||||
},
|
||||
async getById() {
|
||||
return null;
|
||||
},
|
||||
async findByRunId() {
|
||||
return null;
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
},
|
||||
async summarize() {
|
||||
return {
|
||||
total: 1,
|
||||
active: 1,
|
||||
terminal: 0,
|
||||
failures: 0,
|
||||
byNamespace: { failing: 1 },
|
||||
byKind: { failing: 1 },
|
||||
byStatus: { queued: 1 },
|
||||
};
|
||||
},
|
||||
async audit() {
|
||||
return [];
|
||||
},
|
||||
async maintenance() {
|
||||
return {
|
||||
reconciled: 0,
|
||||
cleanupStamped: 0,
|
||||
pruned: 0,
|
||||
};
|
||||
},
|
||||
async cancel() {
|
||||
return { found: false, cancelled: false, reason: "failing" };
|
||||
},
|
||||
});
|
||||
throw new Error("operations register failed");
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
workspaceDir: plugin.dir,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["failing-operations"],
|
||||
},
|
||||
},
|
||||
onlyPluginIds: ["failing-operations"],
|
||||
});
|
||||
|
||||
expect(registry.plugins.find((entry) => entry.id === "failing-operations")?.status).toBe(
|
||||
"error",
|
||||
);
|
||||
expect(getRegisteredOperationsRuntime()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws when activate:false is used without cache:false", () => {
|
||||
expect(() => loadOpenClawPlugins({ activate: false })).toThrow(
|
||||
"activate:false requires cache:false",
|
||||
|
||||
@@ -35,6 +35,12 @@ import {
|
||||
getMemoryRuntime,
|
||||
restoreMemoryPluginState,
|
||||
} from "./memory-state.js";
|
||||
import {
|
||||
clearOperationsRuntimeState,
|
||||
getRegisteredOperationsRuntime,
|
||||
getRegisteredOperationsRuntimeOwner,
|
||||
restoreOperationsRuntimeState,
|
||||
} from "./operations-state.js";
|
||||
import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { createPluginRegistry, type PluginRecord, type PluginRegistry } from "./registry.js";
|
||||
import { resolvePluginCacheInputs } from "./roots.js";
|
||||
@@ -116,6 +122,8 @@ type CachedPluginState = {
|
||||
memoryFlushPlanResolver: ReturnType<typeof getMemoryFlushPlanResolver>;
|
||||
memoryPromptBuilder: ReturnType<typeof getMemoryPromptSectionBuilder>;
|
||||
memoryRuntime: ReturnType<typeof getMemoryRuntime>;
|
||||
operationsRuntime: ReturnType<typeof getRegisteredOperationsRuntime>;
|
||||
operationsRuntimeOwner: ReturnType<typeof getRegisteredOperationsRuntimeOwner>;
|
||||
};
|
||||
|
||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128;
|
||||
@@ -136,6 +144,7 @@ const LAZY_RUNTIME_REFLECTION_KEYS = [
|
||||
"logging",
|
||||
"state",
|
||||
"modelAuth",
|
||||
"operations",
|
||||
] as const satisfies readonly (keyof PluginRuntime)[];
|
||||
|
||||
export function clearPluginLoaderCache(): void {
|
||||
@@ -143,6 +152,7 @@ export function clearPluginLoaderCache(): void {
|
||||
openAllowlistWarningCache.clear();
|
||||
clearMemoryEmbeddingProviders();
|
||||
clearMemoryPluginState();
|
||||
clearOperationsRuntimeState();
|
||||
}
|
||||
|
||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||
@@ -843,6 +853,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
flushPlanResolver: cached.memoryFlushPlanResolver,
|
||||
runtime: cached.memoryRuntime,
|
||||
});
|
||||
restoreOperationsRuntimeState({
|
||||
runtime: cached.operationsRuntime,
|
||||
ownerPluginId: cached.operationsRuntimeOwner,
|
||||
});
|
||||
if (shouldActivate) {
|
||||
activatePluginRegistry(cached.registry, cacheKey, runtimeSubagentMode);
|
||||
}
|
||||
@@ -1336,6 +1350,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const previousMemoryFlushPlanResolver = getMemoryFlushPlanResolver();
|
||||
const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder();
|
||||
const previousMemoryRuntime = getMemoryRuntime();
|
||||
const previousOperationsRuntime = getRegisteredOperationsRuntime();
|
||||
const previousOperationsRuntimeOwner = getRegisteredOperationsRuntimeOwner();
|
||||
|
||||
try {
|
||||
const result = register(api);
|
||||
@@ -1355,6 +1371,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
flushPlanResolver: previousMemoryFlushPlanResolver,
|
||||
runtime: previousMemoryRuntime,
|
||||
});
|
||||
restoreOperationsRuntimeState({
|
||||
runtime: previousOperationsRuntime,
|
||||
ownerPluginId: previousOperationsRuntimeOwner,
|
||||
});
|
||||
}
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
@@ -1365,6 +1385,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
flushPlanResolver: previousMemoryFlushPlanResolver,
|
||||
runtime: previousMemoryRuntime,
|
||||
});
|
||||
restoreOperationsRuntimeState({
|
||||
runtime: previousOperationsRuntime,
|
||||
ownerPluginId: previousOperationsRuntimeOwner,
|
||||
});
|
||||
recordPluginError({
|
||||
logger,
|
||||
registry,
|
||||
@@ -1404,6 +1428,8 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
memoryFlushPlanResolver: getMemoryFlushPlanResolver(),
|
||||
memoryPromptBuilder: getMemoryPromptSectionBuilder(),
|
||||
memoryRuntime: getMemoryRuntime(),
|
||||
operationsRuntime: getRegisteredOperationsRuntime(),
|
||||
operationsRuntimeOwner: getRegisteredOperationsRuntimeOwner(),
|
||||
});
|
||||
}
|
||||
if (shouldActivate) {
|
||||
|
||||
134
src/plugins/operations-state.test.ts
Normal file
134
src/plugins/operations-state.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearOperationsRuntimeState,
|
||||
getRegisteredOperationsRuntime,
|
||||
getRegisteredOperationsRuntimeOwner,
|
||||
registerOperationsRuntimeForOwner,
|
||||
restoreOperationsRuntimeState,
|
||||
summarizeOperationRecords,
|
||||
type PluginOperationsRuntime,
|
||||
} from "./operations-state.js";
|
||||
|
||||
function createRuntime(label: string): PluginOperationsRuntime {
|
||||
return {
|
||||
async dispatch() {
|
||||
return { matched: true, created: true, record: null };
|
||||
},
|
||||
async getById() {
|
||||
return null;
|
||||
},
|
||||
async findByRunId() {
|
||||
return null;
|
||||
},
|
||||
async list() {
|
||||
return [];
|
||||
},
|
||||
async summarize() {
|
||||
return {
|
||||
total: 0,
|
||||
active: 0,
|
||||
terminal: 0,
|
||||
failures: 0,
|
||||
byNamespace: { [label]: 0 },
|
||||
byKind: {},
|
||||
byStatus: {},
|
||||
};
|
||||
},
|
||||
async audit() {
|
||||
return [];
|
||||
},
|
||||
async maintenance() {
|
||||
return {
|
||||
reconciled: 0,
|
||||
cleanupStamped: 0,
|
||||
pruned: 0,
|
||||
};
|
||||
},
|
||||
async cancel() {
|
||||
return { found: false, cancelled: false, reason: label };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("operations-state", () => {
|
||||
it("registers an operations runtime and tracks the owner", () => {
|
||||
clearOperationsRuntimeState();
|
||||
const runtime = createRuntime("one");
|
||||
expect(registerOperationsRuntimeForOwner(runtime, "plugin-one")).toEqual({ ok: true });
|
||||
expect(getRegisteredOperationsRuntime()).toBe(runtime);
|
||||
expect(getRegisteredOperationsRuntimeOwner()).toBe("plugin-one");
|
||||
});
|
||||
|
||||
it("rejects a second owner and allows same-owner refresh", () => {
|
||||
clearOperationsRuntimeState();
|
||||
const first = createRuntime("one");
|
||||
const second = createRuntime("two");
|
||||
const replacement = createRuntime("three");
|
||||
expect(registerOperationsRuntimeForOwner(first, "plugin-one")).toEqual({ ok: true });
|
||||
expect(registerOperationsRuntimeForOwner(second, "plugin-two")).toEqual({
|
||||
ok: false,
|
||||
existingOwner: "plugin-one",
|
||||
});
|
||||
expect(
|
||||
registerOperationsRuntimeForOwner(replacement, "plugin-one", {
|
||||
allowSameOwnerRefresh: true,
|
||||
}),
|
||||
).toEqual({ ok: true });
|
||||
expect(getRegisteredOperationsRuntime()).toBe(replacement);
|
||||
});
|
||||
|
||||
it("restores and clears runtime state", () => {
|
||||
clearOperationsRuntimeState();
|
||||
const runtime = createRuntime("restore");
|
||||
restoreOperationsRuntimeState({
|
||||
runtime,
|
||||
ownerPluginId: "plugin-restore",
|
||||
});
|
||||
expect(getRegisteredOperationsRuntime()).toBe(runtime);
|
||||
expect(getRegisteredOperationsRuntimeOwner()).toBe("plugin-restore");
|
||||
clearOperationsRuntimeState();
|
||||
expect(getRegisteredOperationsRuntime()).toBeUndefined();
|
||||
expect(getRegisteredOperationsRuntimeOwner()).toBeUndefined();
|
||||
});
|
||||
|
||||
it("summarizes generic operation records", () => {
|
||||
const summary = summarizeOperationRecords([
|
||||
{
|
||||
operationId: "op-1",
|
||||
namespace: "tasks",
|
||||
kind: "cli",
|
||||
status: "queued",
|
||||
description: "Queued task",
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
},
|
||||
{
|
||||
operationId: "op-2",
|
||||
namespace: "imports",
|
||||
kind: "csv",
|
||||
status: "failed",
|
||||
description: "Failed import",
|
||||
createdAt: 2,
|
||||
updatedAt: 2,
|
||||
},
|
||||
]);
|
||||
expect(summary).toEqual({
|
||||
total: 2,
|
||||
active: 1,
|
||||
terminal: 1,
|
||||
failures: 1,
|
||||
byNamespace: {
|
||||
imports: 1,
|
||||
tasks: 1,
|
||||
},
|
||||
byKind: {
|
||||
cli: 1,
|
||||
csv: 1,
|
||||
},
|
||||
byStatus: {
|
||||
failed: 1,
|
||||
queued: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
277
src/plugins/operations-state.ts
Normal file
277
src/plugins/operations-state.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export type PluginOperationRecord = {
|
||||
operationId: string;
|
||||
namespace: string;
|
||||
kind: string;
|
||||
status: string;
|
||||
sourceId?: string;
|
||||
requesterSessionKey?: string;
|
||||
childSessionKey?: string;
|
||||
parentOperationId?: string;
|
||||
agentId?: string;
|
||||
runId?: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
createdAt: number;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
updatedAt: number;
|
||||
error?: string;
|
||||
progressSummary?: string;
|
||||
terminalSummary?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PluginOperationListQuery = {
|
||||
namespace?: string;
|
||||
kind?: string;
|
||||
status?: string;
|
||||
sessionKey?: string;
|
||||
runId?: string;
|
||||
sourceId?: string;
|
||||
parentOperationId?: string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type PluginOperationSummary = {
|
||||
total: number;
|
||||
active: number;
|
||||
terminal: number;
|
||||
failures: number;
|
||||
byNamespace: Record<string, number>;
|
||||
byKind: Record<string, number>;
|
||||
byStatus: Record<string, number>;
|
||||
};
|
||||
|
||||
export type PluginOperationCreateEvent = {
|
||||
type: "create";
|
||||
namespace: string;
|
||||
kind: string;
|
||||
status?: string;
|
||||
sourceId?: string;
|
||||
requesterSessionKey?: string;
|
||||
childSessionKey?: string;
|
||||
parentOperationId?: string;
|
||||
agentId?: string;
|
||||
runId?: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
createdAt?: number;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
updatedAt?: number;
|
||||
error?: string;
|
||||
progressSummary?: string | null;
|
||||
terminalSummary?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PluginOperationTransitionEvent = {
|
||||
type: "transition";
|
||||
operationId?: string;
|
||||
runId?: string;
|
||||
status: string;
|
||||
at?: number;
|
||||
startedAt?: number;
|
||||
endedAt?: number;
|
||||
error?: string | null;
|
||||
progressSummary?: string | null;
|
||||
terminalSummary?: string | null;
|
||||
metadataPatch?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PluginOperationPatchEvent = {
|
||||
type: "patch";
|
||||
operationId?: string;
|
||||
runId?: string;
|
||||
at?: number;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
error?: string | null;
|
||||
progressSummary?: string | null;
|
||||
terminalSummary?: string | null;
|
||||
metadataPatch?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type PluginOperationDispatchEvent =
|
||||
| PluginOperationCreateEvent
|
||||
| PluginOperationTransitionEvent
|
||||
| PluginOperationPatchEvent;
|
||||
|
||||
export type PluginOperationDispatchResult = {
|
||||
matched: boolean;
|
||||
created?: boolean;
|
||||
record: PluginOperationRecord | null;
|
||||
};
|
||||
|
||||
export type PluginOperationsCancelResult = {
|
||||
found: boolean;
|
||||
cancelled: boolean;
|
||||
reason?: string;
|
||||
record?: PluginOperationRecord | null;
|
||||
};
|
||||
|
||||
export type PluginOperationAuditSeverity = "warn" | "error";
|
||||
|
||||
export type PluginOperationAuditFinding = {
|
||||
severity: PluginOperationAuditSeverity;
|
||||
code: string;
|
||||
operation: PluginOperationRecord;
|
||||
detail: string;
|
||||
ageMs?: number;
|
||||
};
|
||||
|
||||
export type PluginOperationAuditSummary = {
|
||||
total: number;
|
||||
warnings: number;
|
||||
errors: number;
|
||||
byCode: Record<string, number>;
|
||||
};
|
||||
|
||||
export type PluginOperationAuditQuery = {
|
||||
namespace?: string;
|
||||
severity?: PluginOperationAuditSeverity;
|
||||
code?: string;
|
||||
};
|
||||
|
||||
export type PluginOperationMaintenanceQuery = {
|
||||
namespace?: string;
|
||||
apply?: boolean;
|
||||
};
|
||||
|
||||
export type PluginOperationMaintenanceSummary = {
|
||||
reconciled: number;
|
||||
cleanupStamped: number;
|
||||
pruned: number;
|
||||
};
|
||||
|
||||
export type PluginOperationsRuntime = {
|
||||
dispatch(event: PluginOperationDispatchEvent): Promise<PluginOperationDispatchResult>;
|
||||
getById(operationId: string): Promise<PluginOperationRecord | null>;
|
||||
findByRunId(runId: string): Promise<PluginOperationRecord | null>;
|
||||
list(query?: PluginOperationListQuery): Promise<PluginOperationRecord[]>;
|
||||
summarize(query?: PluginOperationListQuery): Promise<PluginOperationSummary>;
|
||||
audit(query?: PluginOperationAuditQuery): Promise<PluginOperationAuditFinding[]>;
|
||||
maintenance(query?: PluginOperationMaintenanceQuery): Promise<PluginOperationMaintenanceSummary>;
|
||||
cancel(params: {
|
||||
cfg: OpenClawConfig;
|
||||
operationId: string;
|
||||
}): Promise<PluginOperationsCancelResult>;
|
||||
};
|
||||
|
||||
type OperationsRuntimeState = {
|
||||
runtime?: PluginOperationsRuntime;
|
||||
ownerPluginId?: string;
|
||||
};
|
||||
|
||||
type RegisterOperationsRuntimeResult = { ok: true } | { ok: false; existingOwner?: string };
|
||||
|
||||
const operationsRuntimeState: OperationsRuntimeState = {};
|
||||
|
||||
function normalizeOwnedPluginId(ownerPluginId: string): string {
|
||||
return ownerPluginId.trim();
|
||||
}
|
||||
|
||||
export function registerOperationsRuntimeForOwner(
|
||||
runtime: PluginOperationsRuntime,
|
||||
ownerPluginId: string,
|
||||
opts?: { allowSameOwnerRefresh?: boolean },
|
||||
): RegisterOperationsRuntimeResult {
|
||||
const nextOwner = normalizeOwnedPluginId(ownerPluginId);
|
||||
const existingOwner = operationsRuntimeState.ownerPluginId?.trim();
|
||||
if (
|
||||
operationsRuntimeState.runtime &&
|
||||
existingOwner &&
|
||||
existingOwner !== nextOwner &&
|
||||
!(opts?.allowSameOwnerRefresh === true && existingOwner === nextOwner)
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
existingOwner,
|
||||
};
|
||||
}
|
||||
operationsRuntimeState.runtime = runtime;
|
||||
operationsRuntimeState.ownerPluginId = nextOwner;
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export function getRegisteredOperationsRuntime(): PluginOperationsRuntime | undefined {
|
||||
return operationsRuntimeState.runtime;
|
||||
}
|
||||
|
||||
export function getRegisteredOperationsRuntimeOwner(): string | undefined {
|
||||
return operationsRuntimeState.ownerPluginId;
|
||||
}
|
||||
|
||||
export function hasRegisteredOperationsRuntime(): boolean {
|
||||
return operationsRuntimeState.runtime !== undefined;
|
||||
}
|
||||
|
||||
export function restoreOperationsRuntimeState(state: OperationsRuntimeState): void {
|
||||
operationsRuntimeState.runtime = state.runtime;
|
||||
operationsRuntimeState.ownerPluginId = state.ownerPluginId?.trim() || undefined;
|
||||
}
|
||||
|
||||
export function clearOperationsRuntimeState(): void {
|
||||
operationsRuntimeState.runtime = undefined;
|
||||
operationsRuntimeState.ownerPluginId = undefined;
|
||||
}
|
||||
|
||||
export function isActiveOperationStatus(status: string): boolean {
|
||||
return status === "queued" || status === "running";
|
||||
}
|
||||
|
||||
export function isFailureOperationStatus(status: string): boolean {
|
||||
return status === "failed" || status === "timed_out" || status === "lost";
|
||||
}
|
||||
|
||||
export function summarizeOperationRecords(
|
||||
records: Iterable<PluginOperationRecord>,
|
||||
): PluginOperationSummary {
|
||||
const summary: PluginOperationSummary = {
|
||||
total: 0,
|
||||
active: 0,
|
||||
terminal: 0,
|
||||
failures: 0,
|
||||
byNamespace: {},
|
||||
byKind: {},
|
||||
byStatus: {},
|
||||
};
|
||||
for (const record of records) {
|
||||
summary.total += 1;
|
||||
summary.byNamespace[record.namespace] = (summary.byNamespace[record.namespace] ?? 0) + 1;
|
||||
summary.byKind[record.kind] = (summary.byKind[record.kind] ?? 0) + 1;
|
||||
summary.byStatus[record.status] = (summary.byStatus[record.status] ?? 0) + 1;
|
||||
if (isActiveOperationStatus(record.status)) {
|
||||
summary.active += 1;
|
||||
} else {
|
||||
summary.terminal += 1;
|
||||
}
|
||||
if (isFailureOperationStatus(record.status)) {
|
||||
summary.failures += 1;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
|
||||
export function summarizeOperationAuditFindings(
|
||||
findings: Iterable<PluginOperationAuditFinding>,
|
||||
): PluginOperationAuditSummary {
|
||||
const summary: PluginOperationAuditSummary = {
|
||||
total: 0,
|
||||
warnings: 0,
|
||||
errors: 0,
|
||||
byCode: {},
|
||||
};
|
||||
for (const finding of findings) {
|
||||
summary.total += 1;
|
||||
summary.byCode[finding.code] = (summary.byCode[finding.code] ?? 0) + 1;
|
||||
if (finding.severity === "error") {
|
||||
summary.errors += 1;
|
||||
continue;
|
||||
}
|
||||
summary.warnings += 1;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
registerMemoryPromptSection,
|
||||
registerMemoryRuntime,
|
||||
} from "./memory-state.js";
|
||||
import { registerOperationsRuntimeForOwner } from "./operations-state.js";
|
||||
import { normalizeRegisteredProvider } from "./provider-validation.js";
|
||||
import { createEmptyPluginRegistry } from "./registry-empty.js";
|
||||
import { withPluginRuntimePluginIdScope } from "./runtime/gateway-request-scope.js";
|
||||
@@ -1153,6 +1154,20 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
ownerPluginId: record.id,
|
||||
});
|
||||
},
|
||||
registerOperationsRuntime: (runtime) => {
|
||||
const result = registerOperationsRuntimeForOwner(runtime, record.id, {
|
||||
allowSameOwnerRefresh: true,
|
||||
});
|
||||
if (!result.ok) {
|
||||
const ownerDetail = result.existingOwner ? ` (${result.existingOwner})` : "";
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `operations runtime already registered${ownerDetail}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
on: (hookName, handler, opts) =>
|
||||
registerTypedHook(record, hookName, handler, opts, params.hookPolicy),
|
||||
}
|
||||
|
||||
@@ -215,6 +215,20 @@ describe("plugin runtime command execution", () => {
|
||||
]);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "exposes runtime.operations helpers",
|
||||
assert: (runtime: ReturnType<typeof createPluginRuntime>) => {
|
||||
expect(runtime.operations).toBeDefined();
|
||||
expectFunctionKeys(runtime.operations as Record<string, unknown>, [
|
||||
"dispatch",
|
||||
"getById",
|
||||
"findByRunId",
|
||||
"list",
|
||||
"summarize",
|
||||
"cancel",
|
||||
]);
|
||||
},
|
||||
},
|
||||
] as const)("$name", ({ assert }) => {
|
||||
expectRuntimeShape(assert);
|
||||
});
|
||||
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
createLazyRuntimeMethodBinder,
|
||||
createLazyRuntimeModule,
|
||||
} from "../../shared/lazy-runtime.js";
|
||||
import { defaultTaskOperationsRuntime } from "../../tasks/operations-runtime.js";
|
||||
import { VERSION } from "../../version.js";
|
||||
import { listWebSearchProviders, runWebSearch } from "../../web-search/runtime.js";
|
||||
import { getRegisteredOperationsRuntime } from "../operations-state.js";
|
||||
import { createRuntimeAgent } from "./runtime-agent.js";
|
||||
import { defineCachedValue } from "./runtime-cache.js";
|
||||
import { createRuntimeChannel } from "./runtime-channel.js";
|
||||
@@ -96,6 +98,20 @@ function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
|
||||
};
|
||||
}
|
||||
|
||||
function createRuntimeOperations(): PluginRuntime["operations"] {
|
||||
const resolveRuntime = () => getRegisteredOperationsRuntime() ?? defaultTaskOperationsRuntime;
|
||||
return {
|
||||
dispatch: (event) => resolveRuntime().dispatch(event),
|
||||
getById: (operationId) => resolveRuntime().getById(operationId),
|
||||
findByRunId: (runId) => resolveRuntime().findByRunId(runId),
|
||||
list: (query) => resolveRuntime().list(query),
|
||||
summarize: (query) => resolveRuntime().summarize(query),
|
||||
audit: (query) => resolveRuntime().audit(query),
|
||||
maintenance: (query) => resolveRuntime().maintenance(query),
|
||||
cancel: (params) => resolveRuntime().cancel(params),
|
||||
};
|
||||
}
|
||||
|
||||
function createUnavailableSubagentRuntime(): PluginRuntime["subagent"] {
|
||||
const unavailable = () => {
|
||||
throw new Error("Plugin runtime subagent methods are only available during a gateway request.");
|
||||
@@ -203,6 +219,7 @@ export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}):
|
||||
events: createRuntimeEvents(),
|
||||
logging: createRuntimeLogging(),
|
||||
state: { resolveStateDir },
|
||||
operations: createRuntimeOperations(),
|
||||
} satisfies Omit<
|
||||
PluginRuntime,
|
||||
"tts" | "mediaUnderstanding" | "stt" | "modelAuth" | "imageGeneration"
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
||||
import type { LogLevel } from "../../logging/levels.js";
|
||||
import type {
|
||||
PluginOperationAuditFinding,
|
||||
PluginOperationAuditQuery,
|
||||
PluginOperationDispatchEvent,
|
||||
PluginOperationDispatchResult,
|
||||
PluginOperationListQuery,
|
||||
PluginOperationMaintenanceQuery,
|
||||
PluginOperationMaintenanceSummary,
|
||||
PluginOperationRecord,
|
||||
PluginOperationSummary,
|
||||
PluginOperationsCancelResult,
|
||||
} from "../operations-state.js";
|
||||
|
||||
export type { HeartbeatRunResult };
|
||||
|
||||
@@ -115,4 +127,19 @@ export type PluginRuntimeCore = {
|
||||
cfg?: import("../../config/config.js").OpenClawConfig;
|
||||
}) => Promise<import("../../agents/model-auth.js").ResolvedProviderAuth>;
|
||||
};
|
||||
operations: {
|
||||
dispatch: (event: PluginOperationDispatchEvent) => Promise<PluginOperationDispatchResult>;
|
||||
getById: (operationId: string) => Promise<PluginOperationRecord | null>;
|
||||
findByRunId: (runId: string) => Promise<PluginOperationRecord | null>;
|
||||
list: (query?: PluginOperationListQuery) => Promise<PluginOperationRecord[]>;
|
||||
summarize: (query?: PluginOperationListQuery) => Promise<PluginOperationSummary>;
|
||||
audit: (query?: PluginOperationAuditQuery) => Promise<PluginOperationAuditFinding[]>;
|
||||
maintenance: (
|
||||
query?: PluginOperationMaintenanceQuery,
|
||||
) => Promise<PluginOperationMaintenanceSummary>;
|
||||
cancel: (params: {
|
||||
cfg: import("../../config/config.js").OpenClawConfig;
|
||||
operationId: string;
|
||||
}) => Promise<PluginOperationsCancelResult>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ import type {
|
||||
} from "../tts/provider-types.js";
|
||||
import type { DeliveryContext } from "../utils/delivery-context.js";
|
||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||
import type { PluginOperationsRuntime } from "./operations-state.js";
|
||||
import type { SecretInputMode } from "./provider-auth-types.js";
|
||||
import type { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
|
||||
import type { PluginRuntime } from "./runtime/types.js";
|
||||
@@ -1767,6 +1768,8 @@ export type OpenClawPluginApi = {
|
||||
registerMemoryEmbeddingProvider: (
|
||||
adapter: import("./memory-embedding-providers.js").MemoryEmbeddingProviderAdapter,
|
||||
) => void;
|
||||
/** Register the active operations runtime adapter (exclusive slot — only one active at a time). */
|
||||
registerOperationsRuntime: (runtime: PluginOperationsRuntime) => void;
|
||||
resolvePath: (input: string) => string;
|
||||
/** Register a lifecycle hook handler */
|
||||
on: <K extends PluginHookName>(
|
||||
|
||||
Reference in New Issue
Block a user