Revert "refactor: move tasks behind plugin-sdk seam"

This reverts commit da6e9bb76f.
This commit is contained in:
Peter Steinberger 2026-04-01 01:27:24 +09:00
parent 6f74a572d9
commit 759d37635d
No known key found for this signature in database
86 changed files with 836 additions and 1276 deletions

View File

@ -1 +0,0 @@
export * from "./runtime-api.js";

View File

@ -1,29 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.js";
import { createPluginRuntimeMock } from "../../test/helpers/plugins/plugin-runtime-mock.js";
import tasksPlugin from "./index.js";
describe("tasks plugin", () => {
it("registers the default operations runtime, maintenance service, and CLI", () => {
const registerOperationsRuntime = vi.fn();
const registerService = vi.fn();
const registerCli = vi.fn();
tasksPlugin.register(
createTestPluginApi({
id: "tasks",
name: "Tasks",
source: "test",
config: {},
runtime: createPluginRuntimeMock(),
registerOperationsRuntime,
registerService,
registerCli,
}),
);
expect(registerOperationsRuntime).toHaveBeenCalledTimes(1);
expect(registerService).toHaveBeenCalledTimes(1);
expect(registerCli).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,33 +0,0 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import {
createDefaultOperationsMaintenanceService,
defaultOperationsRuntime,
} from "./runtime-api.js";
import { registerTasksCli } from "./src/cli.js";
export default definePluginEntry({
id: "tasks",
name: "Tasks",
description: "Durable task inspection and maintenance CLI",
register(api) {
api.registerOperationsRuntime(defaultOperationsRuntime);
api.registerService(createDefaultOperationsMaintenanceService());
api.registerCli(
({ program }) => {
registerTasksCli(program, {
config: api.config,
operations: api.runtime.operations,
});
},
{
descriptors: [
{
name: "tasks",
description: "Inspect durable background task state",
hasSubcommands: true,
},
],
},
);
},
});

View File

@ -1,9 +0,0 @@
{
"id": "tasks",
"enabledByDefault": true,
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}

View File

@ -1,12 +0,0 @@
{
"name": "@openclaw/tasks",
"version": "2026.3.31",
"private": true,
"description": "OpenClaw durable tasks plugin",
"type": "module",
"openclaw": {
"extensions": [
"./index.ts"
]
}
}

View File

@ -1,4 +0,0 @@
export {
createDefaultOperationsMaintenanceService,
defaultOperationsRuntime,
} from "openclaw/plugin-sdk/tasks";

View File

@ -1,371 +0,0 @@
import type {
PluginOperationAuditFinding,
PluginOperationRecord,
PluginOperationsRuntime,
} from "openclaw/plugin-sdk/plugin-entry";
import { createLoggerBackedRuntime, type OutputRuntimeEnv } from "openclaw/plugin-sdk/runtime";
import { beforeEach, describe, expect, it, vi } from "vitest";
import {
runTasksAudit,
runTasksCancel,
runTasksList,
runTasksMaintenance,
runTasksNotify,
runTasksShow,
} from "./cli.runtime.js";
function createRuntimeCapture() {
const logs: string[] = [];
const errors: string[] = [];
const runtime = createLoggerBackedRuntime({
logger: {
info(message) {
logs.push(message);
},
error(message) {
errors.push(message);
},
},
exitError(code) {
return new Error(`exit ${code}`);
},
}) as OutputRuntimeEnv;
return { runtime, logs, errors };
}
function createOperationsMock(): PluginOperationsRuntime {
return {
dispatch: vi.fn().mockResolvedValue({
matched: false,
record: null,
}),
getById: vi.fn().mockResolvedValue(null),
findByRunId: vi.fn().mockResolvedValue(null),
list: vi.fn().mockResolvedValue([]),
summarize: vi.fn().mockResolvedValue({
total: 0,
active: 0,
terminal: 0,
failures: 0,
byNamespace: {},
byKind: {},
byStatus: {},
}),
audit: vi.fn().mockResolvedValue([]),
maintenance: vi.fn().mockResolvedValue({
reconciled: 0,
cleanupStamped: 0,
pruned: 0,
}),
cancel: vi.fn().mockResolvedValue({
found: false,
cancelled: false,
}),
};
}
const taskFixture: PluginOperationRecord = {
operationId: "task-12345678",
namespace: "tasks",
kind: "acp",
status: "running",
sourceId: "run-12345678",
requesterSessionKey: "agent:main:main",
childSessionKey: "agent:codex:acp:child",
runId: "run-12345678",
title: "Task title",
description: "Create a file",
createdAt: Date.parse("2026-03-29T10:00:00.000Z"),
updatedAt: Date.parse("2026-03-29T10:00:10.000Z"),
progressSummary: "No output for 60s. It may be waiting for input.",
metadata: {
deliveryStatus: "pending",
notifyPolicy: "state_changes",
},
};
describe("tasks CLI runtime", () => {
let operations: PluginOperationsRuntime;
let logs: string[];
let errors: string[];
let runtime: OutputRuntimeEnv;
let config: import("openclaw/plugin-sdk/plugin-entry").OpenClawConfig;
beforeEach(() => {
operations = createOperationsMock();
({ runtime, logs, errors } = createRuntimeCapture());
config = {};
});
it("lists task rows with progress summary fallback", async () => {
vi.mocked(operations.list).mockResolvedValue([taskFixture]);
await runTasksList(
{
runtime: "acp",
status: "running",
},
{
config,
operations,
},
runtime,
);
expect(logs[0]).toContain("Background tasks: 1");
expect(logs[1]).toContain("Task pressure: 0 queued · 1 running · 0 issues");
expect(logs.join("\n")).toContain("No output for 60s. It may be waiting for input.");
});
it("shows detailed task fields including notify and recent events", async () => {
vi.mocked(operations.findByRunId).mockResolvedValue(taskFixture);
await runTasksShow(
{ lookup: "run-12345678" },
{
config: {},
operations,
},
runtime,
);
expect(logs.join("\n")).toContain("notify: state_changes");
expect(logs.join("\n")).toContain(
"progressSummary: No output for 60s. It may be waiting for input.",
);
});
it("updates notify policy for an existing task", async () => {
vi.mocked(operations.findByRunId).mockResolvedValue(taskFixture);
vi.mocked(operations.dispatch).mockResolvedValue({
matched: true,
record: {
...taskFixture,
metadata: {
...taskFixture.metadata,
notifyPolicy: "silent",
},
},
});
await runTasksNotify(
{ lookup: "run-12345678", notify: "silent" },
{
config: {},
operations,
},
runtime,
);
expect(operations.dispatch).toHaveBeenCalledWith({
type: "patch",
operationId: "task-12345678",
at: expect.any(Number),
metadataPatch: {
notifyPolicy: "silent",
},
});
expect(logs[0]).toContain("Updated task-12345678 notify policy to silent.");
});
it("cancels a running task and reports the updated runtime", async () => {
vi.mocked(operations.findByRunId).mockResolvedValue(taskFixture);
vi.mocked(operations.cancel).mockResolvedValue({
found: true,
cancelled: true,
record: {
...taskFixture,
status: "cancelled",
},
});
vi.mocked(operations.getById).mockResolvedValue({
...taskFixture,
status: "cancelled",
});
await runTasksCancel(
{ lookup: "run-12345678" },
{
config,
operations,
},
runtime,
);
expect(operations.cancel).toHaveBeenCalledWith({
cfg: config,
operationId: "task-12345678",
});
expect(logs[0]).toContain("Cancelled task-12345678 (acp) run run-12345678.");
expect(errors).toEqual([]);
});
it("shows task audit findings with filters", async () => {
const findings: PluginOperationAuditFinding[] = [
{
severity: "error",
code: "stale_running",
operation: taskFixture,
ageMs: 45 * 60_000,
detail: "running task appears stuck",
},
{
severity: "warn",
code: "delivery_failed",
operation: {
...taskFixture,
operationId: "task-87654321",
status: "failed",
},
ageMs: 10 * 60_000,
detail: "terminal update delivery failed",
},
];
vi.mocked(operations.audit)
.mockResolvedValueOnce(findings)
.mockResolvedValueOnce([findings[0]!]);
await runTasksAudit(
{ severity: "error", code: "stale_running", limit: 1 },
{
config: {},
operations,
},
runtime,
);
expect(logs[0]).toContain("Task audit: 2 findings · 1 errors · 1 warnings");
expect(logs[1]).toContain("Showing 1 matching findings.");
expect(logs.join("\n")).toContain("stale_running");
expect(logs.join("\n")).toContain("running task appears stuck");
expect(logs.join("\n")).not.toContain("delivery_failed");
});
it("previews task maintenance without applying changes", async () => {
vi.mocked(operations.audit).mockResolvedValue([
{
severity: "error",
code: "stale_running",
operation: taskFixture,
detail: "running task appears stuck",
},
{
severity: "warn",
code: "lost",
operation: {
...taskFixture,
operationId: "task-2",
status: "lost",
},
detail: "backing session missing",
},
]);
vi.mocked(operations.maintenance).mockResolvedValue({
reconciled: 2,
cleanupStamped: 1,
pruned: 3,
});
vi.mocked(operations.summarize).mockResolvedValue({
total: 5,
active: 2,
terminal: 3,
failures: 1,
byNamespace: { tasks: 5 },
byKind: { acp: 1, cron: 2, subagent: 1, cli: 1 },
byStatus: {
queued: 1,
running: 1,
succeeded: 1,
lost: 1,
failed: 1,
},
});
await runTasksMaintenance(
{},
{
config: {},
operations,
},
runtime,
);
expect(logs[0]).toContain(
"Task maintenance (preview): 2 reconcile · 1 cleanup stamp · 3 prune",
);
expect(logs[1]).toContain(
"Task health: 1 queued · 1 running · 1 audit errors · 1 audit warnings",
);
expect(logs[2]).toContain("Dry run only.");
});
it("shows before and after audit health when applying maintenance", async () => {
vi.mocked(operations.audit)
.mockResolvedValueOnce([
{
severity: "error",
code: "stale_running",
operation: taskFixture,
detail: "running task appears stuck",
},
{
severity: "warn",
code: "missing_cleanup",
operation: {
...taskFixture,
operationId: "task-2",
status: "succeeded",
},
detail: "missing cleanupAfter",
},
])
.mockResolvedValueOnce([
{
severity: "warn",
code: "lost",
operation: {
...taskFixture,
operationId: "task-2",
status: "lost",
},
detail: "backing session missing",
},
]);
vi.mocked(operations.maintenance).mockResolvedValue({
reconciled: 2,
cleanupStamped: 1,
pruned: 3,
});
vi.mocked(operations.summarize).mockResolvedValue({
total: 4,
active: 2,
terminal: 2,
failures: 1,
byNamespace: { tasks: 4 },
byKind: { acp: 1, cron: 2, subagent: 1 },
byStatus: {
queued: 1,
running: 1,
succeeded: 1,
lost: 1,
},
});
await runTasksMaintenance(
{ apply: true },
{
config: {},
operations,
},
runtime,
);
expect(logs[0]).toContain(
"Task maintenance (applied): 2 reconcile · 1 cleanup stamp · 3 prune",
);
expect(logs[1]).toContain(
"Task health after apply: 1 queued · 1 running · 0 audit errors · 1 audit warnings",
);
expect(logs[2]).toContain("Task health before apply: 1 audit errors · 1 audit warnings");
});
});

View File

@ -1,464 +0,0 @@
import type {
OpenClawConfig,
PluginOperationAuditFinding,
PluginOperationRecord,
PluginOperationsRuntime,
} from "openclaw/plugin-sdk/plugin-entry";
import { info, type RuntimeEnv } from "openclaw/plugin-sdk/runtime";
type TasksCliDeps = {
config: OpenClawConfig;
operations: PluginOperationsRuntime;
};
type TaskNotifyPolicy = "done_only" | "state_changes" | "silent";
const KIND_PAD = 8;
const STATUS_PAD = 10;
const DELIVERY_PAD = 14;
const ID_PAD = 10;
const RUN_PAD = 10;
function truncate(value: string, maxChars: number) {
if (value.length <= maxChars) {
return value;
}
if (maxChars <= 1) {
return value.slice(0, maxChars);
}
return `${value.slice(0, maxChars - 1)}`;
}
function shortToken(value: string | undefined, maxChars = ID_PAD): string {
const trimmed = value?.trim();
if (!trimmed) {
return "n/a";
}
return truncate(trimmed, maxChars);
}
function readStringMetadata(record: PluginOperationRecord, key: string): string | undefined {
const value = record.metadata?.[key];
return typeof value === "string" && value.trim() ? value : undefined;
}
function readNumberMetadata(record: PluginOperationRecord, key: string): number | undefined {
const value = record.metadata?.[key];
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
function formatTaskRows(tasks: PluginOperationRecord[]) {
const header = [
"Task".padEnd(ID_PAD),
"Kind".padEnd(KIND_PAD),
"Status".padEnd(STATUS_PAD),
"Delivery".padEnd(DELIVERY_PAD),
"Run".padEnd(RUN_PAD),
"Child Session".padEnd(36),
"Summary",
].join(" ");
const lines = [header];
for (const task of tasks) {
const summary = truncate(
task.terminalSummary?.trim() ||
task.progressSummary?.trim() ||
task.title?.trim() ||
task.description.trim(),
80,
);
const line = [
shortToken(task.operationId).padEnd(ID_PAD),
task.kind.padEnd(KIND_PAD),
task.status.padEnd(STATUS_PAD),
(readStringMetadata(task, "deliveryStatus") ?? "n/a").padEnd(DELIVERY_PAD),
shortToken(task.runId, RUN_PAD).padEnd(RUN_PAD),
truncate(task.childSessionKey?.trim() || "n/a", 36).padEnd(36),
summary,
].join(" ");
lines.push(line.trimEnd());
}
return lines;
}
function formatAgeMs(ageMs: number | undefined): string {
if (typeof ageMs !== "number" || ageMs < 1000) {
return "fresh";
}
const totalSeconds = Math.floor(ageMs / 1000);
const days = Math.floor(totalSeconds / 86_400);
const hours = Math.floor((totalSeconds % 86_400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (days > 0) {
return `${days}d${hours}h`;
}
if (hours > 0) {
return `${hours}h${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m`;
}
return `${totalSeconds}s`;
}
function formatAuditRows(findings: PluginOperationAuditFinding[]) {
const header = [
"Severity".padEnd(8),
"Code".padEnd(22),
"Task".padEnd(ID_PAD),
"Status".padEnd(STATUS_PAD),
"Age".padEnd(8),
"Detail",
].join(" ");
const lines = [header];
for (const finding of findings) {
lines.push(
[
finding.severity.padEnd(8),
finding.code.padEnd(22),
shortToken(finding.operation.operationId).padEnd(ID_PAD),
finding.operation.status.padEnd(STATUS_PAD),
formatAgeMs(finding.ageMs).padEnd(8),
truncate(finding.detail, 88),
]
.join(" ")
.trimEnd(),
);
}
return lines;
}
function summarizeAuditFindings(findings: Iterable<PluginOperationAuditFinding>) {
const summary = {
total: 0,
warnings: 0,
errors: 0,
byCode: {} as Record<string, number>,
};
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;
}
function formatTaskListSummary(tasks: PluginOperationRecord[]) {
const queued = tasks.filter((task) => task.status === "queued").length;
const running = tasks.filter((task) => task.status === "running").length;
const failures = tasks.filter(
(task) => task.status === "failed" || task.status === "timed_out" || task.status === "lost",
).length;
return `${queued} queued · ${running} running · ${failures} issues`;
}
async function resolveTaskLookupToken(
operations: PluginOperationsRuntime,
lookup: string,
): Promise<PluginOperationRecord | null> {
const token = lookup.trim();
if (!token) {
return null;
}
const byId = await operations.getById(token);
if (byId?.namespace === "tasks") {
return byId;
}
const byRunId = await operations.findByRunId(token);
if (byRunId?.namespace === "tasks") {
return byRunId;
}
const bySession = await operations.list({
namespace: "tasks",
sessionKey: token,
limit: 1,
});
return bySession[0] ?? null;
}
export async function runTasksList(
opts: { json?: boolean; runtime?: string; status?: string },
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const tasks = await deps.operations.list({
namespace: "tasks",
...(opts.runtime ? { kind: opts.runtime.trim() } : {}),
...(opts.status ? { status: opts.status.trim() } : {}),
});
if (opts.json) {
runtime.log(
JSON.stringify(
{
count: tasks.length,
runtime: opts.runtime ?? null,
status: opts.status ?? null,
tasks,
},
null,
2,
),
);
return;
}
runtime.log(info(`Background tasks: ${tasks.length}`));
runtime.log(info(`Task pressure: ${formatTaskListSummary(tasks)}`));
if (opts.runtime) {
runtime.log(info(`Runtime filter: ${opts.runtime}`));
}
if (opts.status) {
runtime.log(info(`Status filter: ${opts.status}`));
}
if (tasks.length === 0) {
runtime.log("No background tasks found.");
return;
}
for (const line of formatTaskRows(tasks)) {
runtime.log(line);
}
}
export async function runTasksShow(
opts: { json?: boolean; lookup: string },
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const task = await resolveTaskLookupToken(deps.operations, opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
if (opts.json) {
runtime.log(JSON.stringify(task, null, 2));
return;
}
const lines = [
"Background task:",
`taskId: ${task.operationId}`,
`kind: ${task.kind}`,
`sourceId: ${task.sourceId ?? "n/a"}`,
`status: ${task.status}`,
`result: ${readStringMetadata(task, "terminalOutcome") ?? "n/a"}`,
`delivery: ${readStringMetadata(task, "deliveryStatus") ?? "n/a"}`,
`notify: ${readStringMetadata(task, "notifyPolicy") ?? "n/a"}`,
`requesterSessionKey: ${task.requesterSessionKey ?? "n/a"}`,
`childSessionKey: ${task.childSessionKey ?? "n/a"}`,
`parentTaskId: ${task.parentOperationId ?? "n/a"}`,
`agentId: ${task.agentId ?? "n/a"}`,
`runId: ${task.runId ?? "n/a"}`,
`label: ${task.title ?? "n/a"}`,
`task: ${task.description}`,
`createdAt: ${new Date(task.createdAt).toISOString()}`,
`startedAt: ${task.startedAt ? new Date(task.startedAt).toISOString() : "n/a"}`,
`endedAt: ${task.endedAt ? new Date(task.endedAt).toISOString() : "n/a"}`,
`lastEventAt: ${new Date(task.updatedAt).toISOString()}`,
`cleanupAfter: ${(() => {
const cleanupAfter = readNumberMetadata(task, "cleanupAfter");
return cleanupAfter ? new Date(cleanupAfter).toISOString() : "n/a";
})()}`,
...(task.error ? [`error: ${task.error}`] : []),
...(task.progressSummary ? [`progressSummary: ${task.progressSummary}`] : []),
...(task.terminalSummary ? [`terminalSummary: ${task.terminalSummary}`] : []),
];
for (const line of lines) {
runtime.log(line);
}
}
export async function runTasksNotify(
opts: { lookup: string; notify: TaskNotifyPolicy },
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const task = await resolveTaskLookupToken(deps.operations, opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
const updated = await deps.operations.dispatch({
type: "patch",
operationId: task.operationId,
at: Date.now(),
metadataPatch: {
notifyPolicy: opts.notify,
},
});
if (!updated.matched || !updated.record) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
runtime.log(`Updated ${updated.record.operationId} notify policy to ${opts.notify}.`);
}
export async function runTasksCancel(
opts: { lookup: string },
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const task = await resolveTaskLookupToken(deps.operations, opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
const result = await deps.operations.cancel({
cfg: deps.config,
operationId: task.operationId,
});
if (!result.found) {
runtime.error(result.reason ?? `Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
if (!result.cancelled) {
runtime.error(result.reason ?? `Could not cancel task: ${opts.lookup}`);
runtime.exit(1);
return;
}
const updated = await deps.operations.getById(task.operationId);
runtime.log(
`Cancelled ${updated?.operationId ?? task.operationId} (${updated?.kind ?? task.kind})${updated?.runId ? ` run ${updated.runId}` : ""}.`,
);
}
export async function runTasksAudit(
opts: {
json?: boolean;
severity?: "warn" | "error";
code?: string;
limit?: number;
},
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const allFindings = await deps.operations.audit({
namespace: "tasks",
});
const findings = await deps.operations.audit({
namespace: "tasks",
...(opts.severity ? { severity: opts.severity } : {}),
...(opts.code ? { code: opts.code.trim() } : {}),
});
const displayed =
typeof opts.limit === "number" && opts.limit > 0 ? findings.slice(0, opts.limit) : findings;
const summary = summarizeAuditFindings(allFindings);
if (opts.json) {
runtime.log(
JSON.stringify(
{
count: allFindings.length,
filteredCount: findings.length,
displayed: displayed.length,
filters: {
severity: opts.severity ?? null,
code: opts.code ?? null,
limit: opts.limit ?? null,
},
summary,
findings: displayed,
},
null,
2,
),
);
return;
}
runtime.log(
info(
`Task audit: ${summary.total} findings · ${summary.errors} errors · ${summary.warnings} warnings`,
),
);
if (opts.severity || opts.code) {
runtime.log(info(`Showing ${findings.length} matching findings.`));
}
if (opts.severity) {
runtime.log(info(`Severity filter: ${opts.severity}`));
}
if (opts.code) {
runtime.log(info(`Code filter: ${opts.code}`));
}
if (typeof opts.limit === "number" && opts.limit > 0) {
runtime.log(info(`Limit: ${opts.limit}`));
}
if (displayed.length === 0) {
runtime.log("No task audit findings.");
return;
}
for (const line of formatAuditRows(displayed)) {
runtime.log(line);
}
}
export async function runTasksMaintenance(
opts: { json?: boolean; apply?: boolean },
deps: TasksCliDeps,
runtime: RuntimeEnv,
) {
const auditBeforeFindings = await deps.operations.audit({
namespace: "tasks",
});
const maintenance = await deps.operations.maintenance({
namespace: "tasks",
apply: Boolean(opts.apply),
});
const summary = await deps.operations.summarize({
namespace: "tasks",
});
const auditAfterFindings = opts.apply
? await deps.operations.audit({
namespace: "tasks",
})
: auditBeforeFindings;
const auditBefore = summarizeAuditFindings(auditBeforeFindings);
const auditAfter = summarizeAuditFindings(auditAfterFindings);
if (opts.json) {
runtime.log(
JSON.stringify(
{
mode: opts.apply ? "apply" : "preview",
maintenance,
tasks: summary,
auditBefore,
auditAfter,
},
null,
2,
),
);
return;
}
runtime.log(
info(
`Task maintenance (${opts.apply ? "applied" : "preview"}): ${maintenance.reconciled} reconcile · ${maintenance.cleanupStamped} cleanup stamp · ${maintenance.pruned} prune`,
),
);
runtime.log(
info(
`${opts.apply ? "Task health after apply" : "Task health"}: ${summary.byStatus.queued ?? 0} queued · ${summary.byStatus.running ?? 0} running · ${auditAfter.errors} audit errors · ${auditAfter.warnings} audit warnings`,
),
);
if (opts.apply) {
runtime.log(
info(
`Task health before apply: ${auditBefore.errors} audit errors · ${auditBefore.warnings} audit warnings`,
),
);
}
if (!opts.apply) {
runtime.log("Dry run only. Re-run with `openclaw tasks maintenance --apply` to write changes.");
}
}

View File

@ -1,162 +0,0 @@
import type { Command } from "commander";
import type { OpenClawConfig, PluginOperationsRuntime } from "openclaw/plugin-sdk/plugin-entry";
import { defaultRuntime } from "openclaw/plugin-sdk/runtime";
import {
runTasksAudit,
runTasksCancel,
runTasksList,
runTasksMaintenance,
runTasksNotify,
runTasksShow,
} from "./cli.runtime.js";
function parsePositiveIntOrUndefined(value: unknown): number | undefined {
if (typeof value !== "string" || !value.trim()) {
return undefined;
}
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
}
export function registerTasksCli(
program: Command,
deps: {
config: OpenClawConfig;
operations: PluginOperationsRuntime;
},
) {
const tasks = program
.command("tasks")
.description("Inspect durable background task state")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts) => {
await runTasksList(
{
json: Boolean(opts.json),
runtime: opts.runtime as string | undefined,
status: opts.status as string | undefined,
},
deps,
defaultRuntime,
);
});
tasks.enablePositionalOptions();
tasks
.command("list")
.description("List tracked background tasks")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as
| {
json?: boolean;
runtime?: string;
status?: string;
}
| undefined;
await runTasksList(
{
json: Boolean(opts.json || parentOpts?.json),
runtime: (opts.runtime as string | undefined) ?? parentOpts?.runtime,
status: (opts.status as string | undefined) ?? parentOpts?.status,
},
deps,
defaultRuntime,
);
});
tasks
.command("audit")
.description("Show stale or broken background task runs")
.option("--json", "Output as JSON", false)
.option("--severity <level>", "Filter by severity (warn, error)")
.option("--code <name>", "Filter by finding code")
.option("--limit <n>", "Limit displayed findings")
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runTasksAudit(
{
json: Boolean(opts.json || parentOpts?.json),
severity: opts.severity as "warn" | "error" | undefined,
code: opts.code as string | undefined,
limit: parsePositiveIntOrUndefined(opts.limit),
},
deps,
defaultRuntime,
);
});
tasks
.command("maintenance")
.description("Preview or apply task ledger maintenance")
.option("--json", "Output as JSON", false)
.option("--apply", "Apply reconciliation, cleanup stamping, and pruning", false)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runTasksMaintenance(
{
json: Boolean(opts.json || parentOpts?.json),
apply: Boolean(opts.apply),
},
deps,
defaultRuntime,
);
});
tasks
.command("show")
.description("Show one background task by task id, run id, or session key")
.argument("<lookup>", "Task id, run id, or session key")
.option("--json", "Output as JSON", false)
.action(async (lookup, opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runTasksShow(
{
lookup,
json: Boolean(opts.json || parentOpts?.json),
},
deps,
defaultRuntime,
);
});
tasks
.command("notify")
.description("Set task notify policy")
.argument("<lookup>", "Task id, run id, or session key")
.argument("<notify>", "Notify policy (done_only, state_changes, silent)")
.action(async (lookup, notify) => {
await runTasksNotify(
{
lookup,
notify: notify as "done_only" | "state_changes" | "silent",
},
deps,
defaultRuntime,
);
});
tasks
.command("cancel")
.description("Cancel a running background task")
.argument("<lookup>", "Task id, run id, or session key")
.action(async (lookup) => {
await runTasksCancel(
{
lookup,
},
deps,
defaultRuntime,
);
});
}

View File

@ -185,17 +185,9 @@
"types": "./dist/plugin-sdk/plugin-runtime.d.ts",
"default": "./dist/plugin-sdk/plugin-runtime.js"
},
"./plugin-sdk/tasks": {
"types": "./dist/plugin-sdk/tasks.d.ts",
"default": "./dist/plugin-sdk/tasks.js"
},
"./plugin-sdk/tasks-summary": {
"types": "./dist/plugin-sdk/tasks-summary.d.ts",
"default": "./dist/plugin-sdk/tasks-summary.js"
},
"./plugin-sdk/tasks-empty-summary": {
"types": "./dist/plugin-sdk/tasks-empty-summary.d.ts",
"default": "./dist/plugin-sdk/tasks-empty-summary.js"
"./plugin-sdk/operations-default": {
"types": "./dist/plugin-sdk/operations-default.d.ts",
"default": "./dist/plugin-sdk/operations-default.js"
},
"./plugin-sdk/security-runtime": {
"types": "./dist/plugin-sdk/security-runtime.d.ts",

View File

@ -1,9 +0,0 @@
{
"name": "@openclaw/tasks-host-sdk",
"version": "0.0.0-private",
"private": true,
"type": "module",
"exports": {
"./runtime-core": "./src/runtime-core.ts"
}
}

View File

@ -1,13 +0,0 @@
export * from "./flow-registry.js";
export * from "./flow-registry.store.js";
export * from "./flow-registry.types.js";
export * from "./flow-runtime.js";
export * from "./operations-runtime.js";
export * from "./task-executor.js";
export * from "./task-registry.audit.shared.js";
export * from "./task-registry.audit.js";
export * from "./task-registry.maintenance.js";
export * from "./task-registry.store.js";
export * from "./task-registry.summary.js";
export * from "./task-registry.js";
export * from "./task-registry.types.js";

View File

@ -1 +0,0 @@
export { sendMessage } from "../../../src/infra/outbound/message.js";

View File

@ -36,9 +36,7 @@
"speech-runtime",
"speech-core",
"plugin-runtime",
"tasks",
"tasks-empty-summary",
"tasks-summary",
"operations-default",
"security-runtime",
"gateway-runtime",
"github-copilot-login",

View File

@ -1,14 +1,14 @@
import {
createRunningTaskRun,
completeTaskRunByRunId,
failTaskRunByRunId,
startTaskRunByRunId,
} from "openclaw/plugin-sdk/tasks";
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
import type { OpenClawConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { normalizeAgentId } from "../../routing/session-key.js";
import { isAcpSessionKey } from "../../sessions/session-key-utils.js";
import {
createRunningTaskRun,
completeTaskRunByRunId,
failTaskRunByRunId,
startTaskRunByRunId,
} from "../../tasks/task-executor.js";
import type { DeliveryContext } from "../../utils/delivery-context.js";
import {
AcpRuntimeError,

View File

@ -37,9 +37,9 @@ vi.mock("../runtime/registry.js", async (importOriginal) => {
let AcpSessionManager: typeof import("./manager.js").AcpSessionManager;
let AcpRuntimeError: typeof import("../runtime/errors.js").AcpRuntimeError;
let resetAcpSessionManagerForTests: typeof import("./manager.js").__testing.resetAcpSessionManagerForTests;
let findTaskByRunId: typeof import("openclaw/plugin-sdk/tasks").findTaskByRunId;
let resetTaskRegistryForTests: typeof import("openclaw/plugin-sdk/tasks").resetTaskRegistryForTests;
let resetFlowRegistryForTests: typeof import("openclaw/plugin-sdk/tasks").resetFlowRegistryForTests;
let findTaskByRunId: typeof import("../../tasks/task-registry.js").findTaskByRunId;
let resetTaskRegistryForTests: typeof import("../../tasks/task-registry.js").resetTaskRegistryForTests;
let resetFlowRegistryForTests: typeof import("../../tasks/flow-registry.js").resetFlowRegistryForTests;
let installInMemoryTaskAndFlowRegistryRuntime: typeof import("../../test-utils/task-flow-registry-runtime.js").installInMemoryTaskAndFlowRegistryRuntime;
const baseCfg = {
@ -184,8 +184,8 @@ describe("AcpSessionManager", () => {
__testing: { resetAcpSessionManagerForTests },
} = await import("./manager.js"));
({ AcpRuntimeError } = await import("../runtime/errors.js"));
({ findTaskByRunId, resetTaskRegistryForTests } = await import("openclaw/plugin-sdk/tasks"));
({ resetFlowRegistryForTests } = await import("openclaw/plugin-sdk/tasks"));
({ findTaskByRunId, resetTaskRegistryForTests } = await import("../../tasks/task-registry.js"));
({ resetFlowRegistryForTests } = await import("../../tasks/flow-registry.js"));
({ installInMemoryTaskAndFlowRegistryRuntime } =
await import("../../test-utils/task-flow-registry-runtime.js"));
});

View File

@ -1,12 +1,12 @@
import { appendFile, mkdir } from "node:fs/promises";
import path from "node:path";
import { recordTaskRunProgressByRunId } from "openclaw/plugin-sdk/tasks";
import { readAcpSessionEntry } from "../acp/runtime/session-meta.js";
import { resolveSessionFilePath, resolveSessionFilePathOptions } from "../config/sessions/paths.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { scopedHeartbeatWakeOptions } from "../routing/session-key.js";
import { recordTaskRunProgressByRunId } from "../tasks/task-executor.js";
const DEFAULT_STREAM_FLUSH_MS = 2_500;
const DEFAULT_NO_OUTPUT_NOTICE_MS = 60_000;

View File

@ -1,4 +1,3 @@
import { resetTaskRegistryForTests } from "openclaw/plugin-sdk/tasks";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as acpSessionManager from "../acp/control-plane/manager.js";
import type { AcpInitializeSessionInput } from "../acp/control-plane/manager.types.js";
@ -19,6 +18,7 @@ import {
type SessionBindingPlacement,
type SessionBindingRecord,
} from "../infra/outbound/session-binding-service.js";
import { resetTaskRegistryForTests } from "../tasks/task-registry.js";
import * as acpSpawnParentStream from "./acp-spawn-parent-stream.js";
function createDefaultSpawnConfig(): OpenClawConfig {

View File

@ -1,5 +1,4 @@
import crypto from "node:crypto";
import { createRunningTaskRun } from "openclaw/plugin-sdk/tasks";
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
import {
cleanupFailedAcpSpawn,
@ -45,6 +44,7 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../routing/session-key.js";
import { createRunningTaskRun } from "../tasks/task-executor.js";
import {
deliveryContextFromSession,
formatConversationTarget,

View File

@ -192,7 +192,7 @@ async function loadFreshOpenClawToolsForSessionStatusTest() {
vi.doMock("../auto-reply/status.js", () => ({
buildStatusMessage: buildStatusMessageMock,
}));
vi.doMock("openclaw/plugin-sdk/tasks", () => ({
vi.doMock("../tasks/task-registry.js", () => ({
listTasksForSessionKey: (sessionKey: string) => listTasksForSessionKeyMock(sessionKey),
}));
({ createSessionStatusTool } = await import("./tools/session-status-tool.js"));

View File

@ -1,11 +1,11 @@
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { defaultRuntime } from "../runtime.js";
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
import {
completeTaskRunByRunId,
failTaskRunByRunId,
setDetachedTaskDeliveryStatusByRunId,
} from "openclaw/plugin-sdk/tasks";
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
import { defaultRuntime } from "../runtime.js";
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
} from "../tasks/task-executor.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
import {
captureSubagentCompletionReply,

View File

@ -1,7 +1,7 @@
import { createRunningTaskRun } from "openclaw/plugin-sdk/tasks";
import { loadConfig } from "../config/config.js";
import { callGateway } from "../gateway/call.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { createRunningTaskRun } from "../tasks/task-executor.js";
import { type DeliveryContext, normalizeDeliveryContext } from "../utils/delivery-context.js";
import { ensureRuntimePluginsLoaded } from "./runtime-plugins.js";
import type { SubagentRunOutcome } from "./subagent-announce.js";

View File

@ -1,5 +1,4 @@
import { Type } from "@sinclair/typebox";
import { listTasksForSessionKey } from "openclaw/plugin-sdk/tasks";
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
import { buildStatusMessage } from "../../auto-reply/status.js";
@ -24,6 +23,7 @@ import {
resolveAgentIdFromSessionKey,
} from "../../routing/session-key.js";
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
import { listTasksForSessionKey } from "../../tasks/task-registry.js";
import { resolveAgentConfig, resolveAgentDir } from "../agent-scope.js";
import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../date-time.js";
import { resolveModelAuthLabel } from "../model-auth-label.js";

View File

@ -114,7 +114,8 @@ const { handleAcpCommand } = await import("./commands-acp.js");
const { buildCommandTestParams } = await import("./commands-spawn.test-harness.js");
const { __testing: acpManagerTesting } = await import("../../acp/control-plane/manager.js");
const { __testing: acpResetTargetTesting } = await import("./acp-reset-target.js");
const { createTaskRecord, resetTaskRegistryForTests } = await import("openclaw/plugin-sdk/tasks");
const { createTaskRecord, resetTaskRegistryForTests } =
await import("../../tasks/task-registry.js");
function parseTelegramChatIdForTest(raw?: string | null): string | undefined {
const trimmed = raw?.trim().replace(/^telegram:/i, "");

View File

@ -1,4 +1,3 @@
import { findLatestTaskForSessionKey } from "openclaw/plugin-sdk/tasks";
import { getAcpSessionManager } from "../../../acp/control-plane/manager.js";
import {
parseRuntimeTimeoutSecondsInput,
@ -9,6 +8,7 @@ import {
validateRuntimePermissionProfileInput,
} from "../../../acp/control-plane/runtime-options.js";
import { resolveAcpSessionIdentifierLinesFromIdentity } from "../../../acp/runtime/session-identifiers.js";
import { findLatestTaskForSessionKey } from "../../../tasks/task-registry.js";
import type { CommandHandlerResult, HandleCommandsParams } from "../commands-types.js";
import {
ACP_CWD_USAGE,

View File

@ -1,7 +1,7 @@
import { findTaskByRunId } from "openclaw/plugin-sdk/tasks";
import { countPendingDescendantRuns } from "../../../agents/subagent-registry.js";
import { loadSessionStore, resolveStorePath } from "../../../config/sessions.js";
import { formatDurationCompact } from "../../../shared/subagents-format.js";
import { findTaskByRunId } from "../../../tasks/task-registry.js";
import type { CommandHandlerResult } from "../commands-types.js";
import { formatRunLabel } from "../subagents-utils.js";
import {

View File

@ -249,7 +249,8 @@ const { parseConfigCommand } = await import("./config-commands.js");
const { parseDebugCommand } = await import("./debug-commands.js");
const { parseInlineDirectives } = await import("./directive-handling.js");
const { buildCommandContext, handleCommands } = await import("./commands.js");
const { createTaskRecord, resetTaskRegistryForTests } = await import("openclaw/plugin-sdk/tasks");
const { createTaskRecord, resetTaskRegistryForTests } =
await import("../../tasks/task-registry.js");
let testWorkspaceDir = os.tmpdir();

View File

@ -10,6 +10,12 @@ const mocks = vi.hoisted(() => ({
flowsCancelCommand: vi.fn(),
sessionsCommand: vi.fn(),
sessionsCleanupCommand: vi.fn(),
tasksListCommand: vi.fn(),
tasksAuditCommand: vi.fn(),
tasksMaintenanceCommand: vi.fn(),
tasksShowCommand: vi.fn(),
tasksNotifyCommand: vi.fn(),
tasksCancelCommand: vi.fn(),
setVerbose: vi.fn(),
runtime: {
log: vi.fn(),
@ -25,6 +31,12 @@ const flowsShowCommand = mocks.flowsShowCommand;
const flowsCancelCommand = mocks.flowsCancelCommand;
const sessionsCommand = mocks.sessionsCommand;
const sessionsCleanupCommand = mocks.sessionsCleanupCommand;
const tasksListCommand = mocks.tasksListCommand;
const tasksAuditCommand = mocks.tasksAuditCommand;
const tasksMaintenanceCommand = mocks.tasksMaintenanceCommand;
const tasksShowCommand = mocks.tasksShowCommand;
const tasksNotifyCommand = mocks.tasksNotifyCommand;
const tasksCancelCommand = mocks.tasksCancelCommand;
const setVerbose = mocks.setVerbose;
const runtime = mocks.runtime;
@ -50,6 +62,15 @@ vi.mock("../../commands/sessions-cleanup.js", () => ({
sessionsCleanupCommand: mocks.sessionsCleanupCommand,
}));
vi.mock("../../commands/tasks.js", () => ({
tasksListCommand: mocks.tasksListCommand,
tasksAuditCommand: mocks.tasksAuditCommand,
tasksMaintenanceCommand: mocks.tasksMaintenanceCommand,
tasksShowCommand: mocks.tasksShowCommand,
tasksNotifyCommand: mocks.tasksNotifyCommand,
tasksCancelCommand: mocks.tasksCancelCommand,
}));
vi.mock("../../globals.js", () => ({
setVerbose: mocks.setVerbose,
}));
@ -75,6 +96,12 @@ describe("registerStatusHealthSessionsCommands", () => {
flowsCancelCommand.mockResolvedValue(undefined);
sessionsCommand.mockResolvedValue(undefined);
sessionsCleanupCommand.mockResolvedValue(undefined);
tasksListCommand.mockResolvedValue(undefined);
tasksAuditCommand.mockResolvedValue(undefined);
tasksMaintenanceCommand.mockResolvedValue(undefined);
tasksShowCommand.mockResolvedValue(undefined);
tasksNotifyCommand.mockResolvedValue(undefined);
tasksCancelCommand.mockResolvedValue(undefined);
});
it("runs status command with timeout and debug-derived verbose", async () => {
@ -222,6 +249,90 @@ describe("registerStatusHealthSessionsCommands", () => {
);
});
it("runs tasks list from the parent command", async () => {
await runCli(["tasks", "--json", "--runtime", "acp", "--status", "running"]);
expect(tasksListCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
runtime: "acp",
status: "running",
}),
runtime,
);
});
it("runs tasks show subcommand with lookup forwarding", async () => {
await runCli(["tasks", "show", "run-123", "--json"]);
expect(tasksShowCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
json: true,
}),
runtime,
);
});
it("runs tasks maintenance subcommand with apply forwarding", async () => {
await runCli(["tasks", "--json", "maintenance", "--apply"]);
expect(tasksMaintenanceCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
apply: true,
}),
runtime,
);
});
it("runs tasks audit subcommand with filters", async () => {
await runCli([
"tasks",
"--json",
"audit",
"--severity",
"error",
"--code",
"stale_running",
"--limit",
"5",
]);
expect(tasksAuditCommand).toHaveBeenCalledWith(
expect.objectContaining({
json: true,
severity: "error",
code: "stale_running",
limit: 5,
}),
runtime,
);
});
it("runs tasks notify subcommand with lookup and policy forwarding", async () => {
await runCli(["tasks", "notify", "run-123", "state_changes"]);
expect(tasksNotifyCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
notify: "state_changes",
}),
runtime,
);
});
it("runs tasks cancel subcommand with lookup forwarding", async () => {
await runCli(["tasks", "cancel", "run-123"]);
expect(tasksCancelCommand).toHaveBeenCalledWith(
expect.objectContaining({
lookup: "run-123",
}),
runtime,
);
});
it("runs flows list from the parent command", async () => {
await runCli(["flows", "--json", "--status", "blocked"]);

View File

@ -4,12 +4,21 @@ import { healthCommand } from "../../commands/health.js";
import { sessionsCleanupCommand } from "../../commands/sessions-cleanup.js";
import { sessionsCommand } from "../../commands/sessions.js";
import { statusCommand } from "../../commands/status.js";
import {
tasksAuditCommand,
tasksCancelCommand,
tasksListCommand,
tasksMaintenanceCommand,
tasksNotifyCommand,
tasksShowCommand,
} from "../../commands/tasks.js";
import { setVerbose } from "../../globals.js";
import { defaultRuntime } from "../../runtime.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import { runCommandWithRuntime } from "../cli-utils.js";
import { formatHelpExamples } from "../help-format.js";
import { parsePositiveIntOrUndefined } from "./helpers.js";
function resolveVerbose(opts: { verbose?: boolean; debug?: boolean }): boolean {
return Boolean(opts.verbose || opts.debug);
@ -218,6 +227,159 @@ export function registerStatusHealthSessionsCommands(program: Command) {
);
});
});
const tasksCmd = program
.command("tasks")
.description("Inspect durable background task state")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json),
runtime: opts.runtime as string | undefined,
status: opts.status as string | undefined,
},
defaultRuntime,
);
});
});
tasksCmd.enablePositionalOptions();
tasksCmd
.command("list")
.description("List tracked background tasks")
.option("--json", "Output as JSON", false)
.option("--runtime <name>", "Filter by kind (subagent, acp, cron, cli)")
.option(
"--status <name>",
"Filter by status (queued, running, succeeded, failed, timed_out, cancelled, lost)",
)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as
| {
json?: boolean;
runtime?: string;
status?: string;
}
| undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksListCommand(
{
json: Boolean(opts.json || parentOpts?.json),
runtime: (opts.runtime as string | undefined) ?? parentOpts?.runtime,
status: (opts.status as string | undefined) ?? parentOpts?.status,
},
defaultRuntime,
);
});
});
tasksCmd
.command("audit")
.description("Show stale or broken background task runs")
.option("--json", "Output as JSON", false)
.option("--severity <level>", "Filter by severity (warn, error)")
.option(
"--code <name>",
"Filter by finding code (stale_queued, stale_running, lost, delivery_failed, missing_cleanup, inconsistent_timestamps)",
)
.option("--limit <n>", "Limit displayed findings")
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksAuditCommand(
{
json: Boolean(opts.json || parentOpts?.json),
severity: opts.severity as "warn" | "error" | undefined,
code: opts.code as
| "stale_queued"
| "stale_running"
| "lost"
| "delivery_failed"
| "missing_cleanup"
| "inconsistent_timestamps"
| undefined,
limit: parsePositiveIntOrUndefined(opts.limit),
},
defaultRuntime,
);
});
});
tasksCmd
.command("maintenance")
.description("Preview or apply task ledger maintenance")
.option("--json", "Output as JSON", false)
.option("--apply", "Apply reconciliation, cleanup stamping, and pruning", false)
.action(async (opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksMaintenanceCommand(
{
json: Boolean(opts.json || parentOpts?.json),
apply: Boolean(opts.apply),
},
defaultRuntime,
);
});
});
tasksCmd
.command("show")
.description("Show one background task by task id, run id, or session key")
.argument("<lookup>", "Task id, run id, or session key")
.option("--json", "Output as JSON", false)
.action(async (lookup, opts, command) => {
const parentOpts = command.parent?.opts() as { json?: boolean } | undefined;
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksShowCommand(
{
lookup,
json: Boolean(opts.json || parentOpts?.json),
},
defaultRuntime,
);
});
});
tasksCmd
.command("notify")
.description("Set task notify policy")
.argument("<lookup>", "Task id, run id, or session key")
.argument("<notify>", "Notify policy (done_only, state_changes, silent)")
.action(async (lookup, notify) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksNotifyCommand(
{
lookup,
notify: notify as "done_only" | "state_changes" | "silent",
},
defaultRuntime,
);
});
});
tasksCmd
.command("cancel")
.description("Cancel a running background task")
.argument("<lookup>", "Task id, run id, or session key")
.action(async (lookup) => {
await runCommandWithRuntime(defaultRuntime, async () => {
await tasksCancelCommand(
{
lookup,
},
defaultRuntime,
);
});
});
const flowsCmd = program
.command("flows")
.description("Inspect ClawFlow state")

View File

@ -52,11 +52,11 @@ vi.mock("../plugins/memory-state.js", () => ({
hasMemoryRuntime: hasMemoryRuntimeMock,
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/task-registry.js", () => ({
ensureTaskRegistryReady: ensureTaskRegistryReadyMock,
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/task-registry.maintenance.js", () => ({
startTaskRegistryMaintenance: startTaskRegistryMaintenanceMock,
}));

View File

@ -32,11 +32,11 @@ vi.mock("../plugins/status.js", () => ({
mocks.buildPluginCompatibilityWarnings(...args),
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/flow-registry.js", () => ({
listFlowRecords: (...args: unknown[]) => mocks.listFlowRecords(...args),
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/task-registry.js", () => ({
listTasksForFlowId: (...args: unknown[]) => mocks.listTasksForFlowId(...args),
}));

View File

@ -1,10 +1,10 @@
import { listFlowRecords } from "openclaw/plugin-sdk/tasks";
import { listTasksForFlowId } from "openclaw/plugin-sdk/tasks";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { buildWorkspaceSkillStatus } from "../agents/skills-status.js";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/config.js";
import { buildPluginCompatibilityWarnings, buildPluginStatusReport } from "../plugins/status.js";
import { listFlowRecords } from "../tasks/flow-registry.js";
import { listTasksForFlowId } from "../tasks/task-registry.js";
import { note } from "../terminal/note.js";
import { detectLegacyWorkspaceDirs, formatLegacyWorkspaceWarning } from "./doctor-workspace.js";

View File

@ -12,11 +12,17 @@ const mocks = vi.hoisted(() => ({
loadConfigMock: vi.fn(() => ({ loaded: true })),
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/flow-registry.js", () => ({
listFlowRecords: (...args: unknown[]) => mocks.listFlowRecordsMock(...args),
resolveFlowForLookupToken: (...args: unknown[]) => mocks.resolveFlowForLookupTokenMock(...args),
getFlowById: (...args: unknown[]) => mocks.getFlowByIdMock(...args),
}));
vi.mock("../tasks/task-registry.js", () => ({
listTasksForFlowId: (...args: unknown[]) => mocks.listTasksForFlowIdMock(...args),
}));
vi.mock("../tasks/task-executor.js", () => ({
getFlowTaskSummary: (...args: unknown[]) => mocks.getFlowTaskSummaryMock(...args),
cancelFlowById: (...args: unknown[]) => mocks.cancelFlowByIdMock(...args),
}));

View File

@ -1,10 +1,10 @@
import { getFlowById, listFlowRecords, resolveFlowForLookupToken } from "openclaw/plugin-sdk/tasks";
import type { FlowRecord, FlowStatus } from "openclaw/plugin-sdk/tasks";
import { cancelFlowById, getFlowTaskSummary } from "openclaw/plugin-sdk/tasks";
import { listTasksForFlowId } from "openclaw/plugin-sdk/tasks";
import { loadConfig } from "../config/config.js";
import { info } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import { getFlowById, listFlowRecords, resolveFlowForLookupToken } from "../tasks/flow-registry.js";
import type { FlowRecord, FlowStatus } from "../tasks/flow-registry.types.js";
import { cancelFlowById, getFlowTaskSummary } from "../tasks/task-executor.js";
import { listTasksForFlowId } from "../tasks/task-registry.js";
import { isRich, theme } from "../terminal/theme.js";
const ID_PAD = 10;

View File

@ -1,12 +1,10 @@
import {
createEmptyTaskAuditSummary,
createEmptyTaskRegistrySummary,
} from "openclaw/plugin-sdk/tasks-empty-summary";
import type { OpenClawConfig } from "../config/types.js";
import type { UpdateCheckResult } from "../infra/update-check.js";
import { loggingState } from "../logging/state.js";
import { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { createEmptyTaskAuditSummary } from "../tasks/task-registry.audit.shared.js";
import { createEmptyTaskRegistrySummary } from "../tasks/task-registry.summary.js";
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
import type { StatusScanResult } from "./status.scan.js";
import {

View File

@ -1,8 +1,4 @@
import { existsSync } from "node:fs";
import {
createEmptyTaskAuditSummary,
createEmptyTaskRegistrySummary,
} from "openclaw/plugin-sdk/tasks-empty-summary";
import { resolveMemorySearchConfig } from "../agents/memory-search.js";
import { hasPotentialConfiguredChannels } from "../channels/config-presence.js";
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
@ -21,6 +17,8 @@ import {
import { runExec } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
import { createLazyRuntimeSurface } from "../shared/lazy-runtime.js";
import { createEmptyTaskAuditSummary } from "../tasks/task-registry.audit.shared.js";
import { createEmptyTaskRegistrySummary } from "../tasks/task-registry.summary.js";
import type { buildChannelsTable as buildChannelsTableFn } from "./status-all/channels.js";
import type { getAgentLocalStatuses as getAgentLocalStatusesFn } from "./status.agent-local.js";
import { buildColdStartUpdateResult, scanStatusJsonCore } from "./status.scan.json-core.js";

View File

@ -63,7 +63,7 @@ vi.mock("../infra/system-events.js", () => ({
peekSystemEvents: vi.fn(() => []),
}));
vi.mock("openclaw/plugin-sdk/tasks-summary", () => ({
vi.mock("../tasks/task-registry.maintenance.js", () => ({
getInspectableTaskRegistrySummary: vi.fn(() => ({
total: 0,
active: 0,

View File

@ -17,7 +17,7 @@ let channelSummaryModulePromise: Promise<typeof import("../infra/channel-summary
let linkChannelModulePromise: Promise<typeof import("./status.link-channel.js")> | undefined;
let configIoModulePromise: Promise<typeof import("../config/io.js")> | undefined;
let taskRegistryMaintenanceModulePromise:
| Promise<typeof import("openclaw/plugin-sdk/tasks-summary")>
| Promise<typeof import("../tasks/task-registry.maintenance.js")>
| undefined;
function loadChannelSummaryModule() {
@ -41,7 +41,7 @@ function loadConfigIoModule() {
}
function loadTaskRegistryMaintenanceModule() {
taskRegistryMaintenanceModulePromise ??= import("openclaw/plugin-sdk/tasks-summary");
taskRegistryMaintenanceModulePromise ??= import("../tasks/task-registry.maintenance.js");
return taskRegistryMaintenanceModulePromise;
}

View File

@ -467,7 +467,7 @@ vi.mock("../daemon/node-service.js", () => ({
vi.mock("../node-host/config.js", () => ({
loadNodeHostConfig: mocks.loadNodeHostConfig,
}));
vi.mock("openclaw/plugin-sdk/tasks", () => ({
vi.mock("../tasks/task-registry.maintenance.js", () => ({
getInspectableTaskRegistrySummary: mocks.getInspectableTaskRegistrySummary,
getInspectableTaskAuditSummary: mocks.getInspectableTaskAuditSummary,
}));

View File

@ -1,6 +1,6 @@
import type { TaskAuditSummary } from "openclaw/plugin-sdk/tasks-summary";
import type { TaskRegistrySummary } from "openclaw/plugin-sdk/tasks-summary";
import type { ChannelId } from "../channels/plugins/types.js";
import type { TaskAuditSummary } from "../tasks/task-registry.audit.js";
import type { TaskRegistrySummary } from "../tasks/task-registry.types.js";
export type SessionStatus = {
agentId?: string;

424
src/commands/tasks.ts Normal file
View File

@ -0,0 +1,424 @@
import { loadConfig } from "../config/config.js";
import { info } from "../globals.js";
import type { RuntimeEnv } from "../runtime.js";
import {
listTaskAuditFindings,
summarizeTaskAuditFindings,
type TaskAuditCode,
type TaskAuditFinding,
type TaskAuditSeverity,
} from "../tasks/task-registry.audit.js";
import { cancelTaskById, getTaskById, updateTaskNotifyPolicyById } from "../tasks/task-registry.js";
import {
getInspectableTaskAuditSummary,
getInspectableTaskRegistrySummary,
previewTaskRegistryMaintenance,
runTaskRegistryMaintenance,
} from "../tasks/task-registry.maintenance.js";
import {
reconcileInspectableTasks,
reconcileTaskLookupToken,
} from "../tasks/task-registry.reconcile.js";
import { summarizeTaskRecords } from "../tasks/task-registry.summary.js";
import type { TaskNotifyPolicy, TaskRecord } from "../tasks/task-registry.types.js";
import { isRich, theme } from "../terminal/theme.js";
const RUNTIME_PAD = 8;
const STATUS_PAD = 10;
const DELIVERY_PAD = 14;
const ID_PAD = 10;
const RUN_PAD = 10;
function truncate(value: string, maxChars: number) {
if (value.length <= maxChars) {
return value;
}
if (maxChars <= 1) {
return value.slice(0, maxChars);
}
return `${value.slice(0, maxChars - 1)}`;
}
function shortToken(value: string | undefined, maxChars = ID_PAD): string {
const trimmed = value?.trim();
if (!trimmed) {
return "n/a";
}
return truncate(trimmed, maxChars);
}
function formatTaskStatusCell(status: string, rich: boolean) {
const padded = status.padEnd(STATUS_PAD);
if (!rich) {
return padded;
}
if (status === "succeeded") {
return theme.success(padded);
}
if (status === "failed" || status === "lost" || status === "timed_out") {
return theme.error(padded);
}
if (status === "running") {
return theme.accentBright(padded);
}
return theme.muted(padded);
}
function formatTaskRows(tasks: TaskRecord[], rich: boolean) {
const header = [
"Task".padEnd(ID_PAD),
"Kind".padEnd(RUNTIME_PAD),
"Status".padEnd(STATUS_PAD),
"Delivery".padEnd(DELIVERY_PAD),
"Run".padEnd(RUN_PAD),
"Child Session",
"Summary",
].join(" ");
const lines = [rich ? theme.heading(header) : header];
for (const task of tasks) {
const summary = truncate(
task.terminalSummary?.trim() ||
task.progressSummary?.trim() ||
task.label?.trim() ||
task.task.trim(),
80,
);
const line = [
shortToken(task.taskId).padEnd(ID_PAD),
task.runtime.padEnd(RUNTIME_PAD),
formatTaskStatusCell(task.status, rich),
task.deliveryStatus.padEnd(DELIVERY_PAD),
shortToken(task.runId, RUN_PAD).padEnd(RUN_PAD),
truncate(task.childSessionKey?.trim() || "n/a", 36).padEnd(36),
summary,
].join(" ");
lines.push(line.trimEnd());
}
return lines;
}
function formatTaskListSummary(tasks: TaskRecord[]) {
const summary = summarizeTaskRecords(tasks);
return `${summary.byStatus.queued} queued · ${summary.byStatus.running} running · ${summary.failures} issues`;
}
function formatAgeMs(ageMs: number | undefined): string {
if (typeof ageMs !== "number" || ageMs < 1000) {
return "fresh";
}
const totalSeconds = Math.floor(ageMs / 1000);
const days = Math.floor(totalSeconds / 86_400);
const hours = Math.floor((totalSeconds % 86_400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (days > 0) {
return `${days}d${hours}h`;
}
if (hours > 0) {
return `${hours}h${minutes}m`;
}
if (minutes > 0) {
return `${minutes}m`;
}
return `${totalSeconds}s`;
}
function formatAuditRows(findings: TaskAuditFinding[], rich: boolean) {
const header = [
"Severity".padEnd(8),
"Code".padEnd(22),
"Task".padEnd(ID_PAD),
"Status".padEnd(STATUS_PAD),
"Age".padEnd(8),
"Detail",
].join(" ");
const lines = [rich ? theme.heading(header) : header];
for (const finding of findings) {
const severity = finding.severity.padEnd(8);
const status = formatTaskStatusCell(finding.task.status, rich);
const severityCell = !rich
? severity
: finding.severity === "error"
? theme.error(severity)
: theme.warn(severity);
lines.push(
[
severityCell,
finding.code.padEnd(22),
shortToken(finding.task.taskId).padEnd(ID_PAD),
status,
formatAgeMs(finding.ageMs).padEnd(8),
truncate(finding.detail, 88),
]
.join(" ")
.trimEnd(),
);
}
return lines;
}
export async function tasksListCommand(
opts: { json?: boolean; runtime?: string; status?: string },
runtime: RuntimeEnv,
) {
const runtimeFilter = opts.runtime?.trim();
const statusFilter = opts.status?.trim();
const tasks = reconcileInspectableTasks().filter((task) => {
if (runtimeFilter && task.runtime !== runtimeFilter) {
return false;
}
if (statusFilter && task.status !== statusFilter) {
return false;
}
return true;
});
if (opts.json) {
runtime.log(
JSON.stringify(
{
count: tasks.length,
runtime: runtimeFilter ?? null,
status: statusFilter ?? null,
tasks,
},
null,
2,
),
);
return;
}
runtime.log(info(`Background tasks: ${tasks.length}`));
runtime.log(info(`Task pressure: ${formatTaskListSummary(tasks)}`));
if (runtimeFilter) {
runtime.log(info(`Runtime filter: ${runtimeFilter}`));
}
if (statusFilter) {
runtime.log(info(`Status filter: ${statusFilter}`));
}
if (tasks.length === 0) {
runtime.log("No background tasks found.");
return;
}
const rich = isRich();
for (const line of formatTaskRows(tasks, rich)) {
runtime.log(line);
}
}
export async function tasksShowCommand(
opts: { json?: boolean; lookup: string },
runtime: RuntimeEnv,
) {
const task = reconcileTaskLookupToken(opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
if (opts.json) {
runtime.log(JSON.stringify(task, null, 2));
return;
}
const lines = [
"Background task:",
`taskId: ${task.taskId}`,
`kind: ${task.runtime}`,
`sourceId: ${task.sourceId ?? "n/a"}`,
`status: ${task.status}`,
`result: ${task.terminalOutcome ?? "n/a"}`,
`delivery: ${task.deliveryStatus}`,
`notify: ${task.notifyPolicy}`,
`requesterSessionKey: ${task.requesterSessionKey}`,
`childSessionKey: ${task.childSessionKey ?? "n/a"}`,
`parentTaskId: ${task.parentTaskId ?? "n/a"}`,
`agentId: ${task.agentId ?? "n/a"}`,
`runId: ${task.runId ?? "n/a"}`,
`label: ${task.label ?? "n/a"}`,
`task: ${task.task}`,
`createdAt: ${new Date(task.createdAt).toISOString()}`,
`startedAt: ${task.startedAt ? new Date(task.startedAt).toISOString() : "n/a"}`,
`endedAt: ${task.endedAt ? new Date(task.endedAt).toISOString() : "n/a"}`,
`lastEventAt: ${task.lastEventAt ? new Date(task.lastEventAt).toISOString() : "n/a"}`,
`cleanupAfter: ${task.cleanupAfter ? new Date(task.cleanupAfter).toISOString() : "n/a"}`,
...(task.error ? [`error: ${task.error}`] : []),
...(task.progressSummary ? [`progressSummary: ${task.progressSummary}`] : []),
...(task.terminalSummary ? [`terminalSummary: ${task.terminalSummary}`] : []),
];
for (const line of lines) {
runtime.log(line);
}
}
export async function tasksNotifyCommand(
opts: { lookup: string; notify: TaskNotifyPolicy },
runtime: RuntimeEnv,
) {
const task = reconcileTaskLookupToken(opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
const updated = updateTaskNotifyPolicyById({
taskId: task.taskId,
notifyPolicy: opts.notify,
});
if (!updated) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
runtime.log(`Updated ${updated.taskId} notify policy to ${updated.notifyPolicy}.`);
}
export async function tasksCancelCommand(opts: { lookup: string }, runtime: RuntimeEnv) {
const task = reconcileTaskLookupToken(opts.lookup);
if (!task) {
runtime.error(`Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
const result = await cancelTaskById({
cfg: loadConfig(),
taskId: task.taskId,
});
if (!result.found) {
runtime.error(result.reason ?? `Task not found: ${opts.lookup}`);
runtime.exit(1);
return;
}
if (!result.cancelled) {
runtime.error(result.reason ?? `Could not cancel task: ${opts.lookup}`);
runtime.exit(1);
return;
}
const updated = getTaskById(task.taskId);
runtime.log(
`Cancelled ${updated?.taskId ?? task.taskId} (${updated?.runtime ?? task.runtime})${updated?.runId ? ` run ${updated.runId}` : ""}.`,
);
}
export async function tasksAuditCommand(
opts: {
json?: boolean;
severity?: TaskAuditSeverity;
code?: TaskAuditCode;
limit?: number;
},
runtime: RuntimeEnv,
) {
const severityFilter = opts.severity?.trim() as TaskAuditSeverity | undefined;
const codeFilter = opts.code?.trim() as TaskAuditCode | undefined;
const allFindings = listTaskAuditFindings();
const findings = allFindings.filter((finding) => {
if (severityFilter && finding.severity !== severityFilter) {
return false;
}
if (codeFilter && finding.code !== codeFilter) {
return false;
}
return true;
});
const limit = typeof opts.limit === "number" && opts.limit > 0 ? opts.limit : undefined;
const displayed = limit ? findings.slice(0, limit) : findings;
const summary = summarizeTaskAuditFindings(allFindings);
if (opts.json) {
runtime.log(
JSON.stringify(
{
count: allFindings.length,
filteredCount: findings.length,
displayed: displayed.length,
filters: {
severity: severityFilter ?? null,
code: codeFilter ?? null,
limit: limit ?? null,
},
summary,
findings: displayed,
},
null,
2,
),
);
return;
}
runtime.log(
info(
`Task audit: ${summary.total} findings · ${summary.errors} errors · ${summary.warnings} warnings`,
),
);
if (severityFilter || codeFilter) {
runtime.log(info(`Showing ${findings.length} matching findings.`));
}
if (severityFilter) {
runtime.log(info(`Severity filter: ${severityFilter}`));
}
if (codeFilter) {
runtime.log(info(`Code filter: ${codeFilter}`));
}
if (limit) {
runtime.log(info(`Limit: ${limit}`));
}
if (displayed.length === 0) {
runtime.log("No task audit findings.");
return;
}
const rich = isRich();
for (const line of formatAuditRows(displayed, rich)) {
runtime.log(line);
}
}
export async function tasksMaintenanceCommand(
opts: { json?: boolean; apply?: boolean },
runtime: RuntimeEnv,
) {
const auditBefore = getInspectableTaskAuditSummary();
const maintenance = opts.apply ? runTaskRegistryMaintenance() : previewTaskRegistryMaintenance();
const summary = getInspectableTaskRegistrySummary();
const auditAfter = opts.apply ? getInspectableTaskAuditSummary() : auditBefore;
if (opts.json) {
runtime.log(
JSON.stringify(
{
mode: opts.apply ? "apply" : "preview",
maintenance,
tasks: summary,
auditBefore,
auditAfter,
},
null,
2,
),
);
return;
}
runtime.log(
info(
`Task maintenance (${opts.apply ? "applied" : "preview"}): ${maintenance.reconciled} reconcile · ${maintenance.cleanupStamped} cleanup stamp · ${maintenance.pruned} prune`,
),
);
runtime.log(
info(
`${opts.apply ? "Task health after apply" : "Task health"}: ${summary.byStatus.queued} queued · ${summary.byStatus.running} running · ${auditAfter.errors} audit errors · ${auditAfter.warnings} audit warnings`,
),
);
if (opts.apply) {
runtime.log(
info(
`Task health before apply: ${auditBefore.errors} audit errors · ${auditBefore.warnings} audit warnings`,
),
);
}
if (!opts.apply) {
runtime.log("Dry run only. Re-run with `openclaw tasks maintenance --apply` to write changes.");
}
}

View File

@ -1,8 +1,8 @@
import fs from "node:fs/promises";
import path from "node:path";
import * as taskExecutor from "openclaw/plugin-sdk/tasks";
import { findTaskByRunId, resetTaskRegistryForTests } from "openclaw/plugin-sdk/tasks";
import { describe, expect, it, vi } from "vitest";
import * as taskExecutor from "../../tasks/task-executor.js";
import { findTaskByRunId, resetTaskRegistryForTests } from "../../tasks/task-registry.js";
import { setupCronServiceSuite, writeCronStoreSnapshot } from "../service.test-harness.js";
import type { CronJob } from "../types.js";
import { run, start, stop } from "./ops.js";

View File

@ -1,10 +1,10 @@
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { CommandLane } from "../../process/lanes.js";
import {
completeTaskRunByRunId,
createRunningTaskRun,
failTaskRunByRunId,
} from "openclaw/plugin-sdk/tasks";
import { enqueueCommandInLane } from "../../process/command-queue.js";
import { CommandLane } from "../../process/lanes.js";
} from "../../tasks/task-executor.js";
import type { CronJob, CronJobCreate, CronJobPatch } from "../types.js";
import { normalizeCronCreateDeliveryInput } from "./initial-delivery.js";
import {

View File

@ -1,11 +1,11 @@
import fs from "node:fs/promises";
import * as taskExecutor from "openclaw/plugin-sdk/tasks";
import { resetTaskRegistryForTests } from "openclaw/plugin-sdk/tasks";
import { afterEach, describe, expect, it, vi } from "vitest";
import { setupCronServiceSuite, writeCronStoreSnapshot } from "../../cron/service.test-harness.js";
import { createCronServiceState } from "../../cron/service/state.js";
import { onTimer } from "../../cron/service/timer.js";
import type { CronJob } from "../../cron/types.js";
import * as taskExecutor from "../../tasks/task-executor.js";
import { resetTaskRegistryForTests } from "../../tasks/task-registry.js";
const { logger, makeStorePath } = setupCronServiceSuite({
prefix: "cron-service-timer-seam",

View File

@ -1,12 +1,12 @@
import {
completeTaskRunByRunId,
createRunningTaskRun,
failTaskRunByRunId,
} from "openclaw/plugin-sdk/tasks";
import { resolveFailoverReasonFromError } from "../../agents/failover-error.js";
import type { CronConfig, CronRetryOn } from "../../config/types.cron.js";
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
import { DEFAULT_AGENT_ID } from "../../routing/session-key.js";
import {
completeTaskRunByRunId,
createRunningTaskRun,
failTaskRunByRunId,
} from "../../tasks/task-executor.js";
import { resolveCronDeliveryPlan } from "../delivery.js";
import { sweepCronRunSessions } from "../session-reaper.js";
import type {

View File

@ -1,6 +1,6 @@
import { findTaskByRunId, resetTaskRegistryForTests } from "openclaw/plugin-sdk/tasks";
import { afterEach, describe, expect, it, vi } from "vitest";
import { BARE_SESSION_RESET_PROMPT } from "../../auto-reply/reply/session-reset-prompt.js";
import { findTaskByRunId, resetTaskRegistryForTests } from "../../tasks/task-registry.js";
import { withTempDir } from "../../test-helpers/temp-dir.js";
import { agentHandlers } from "./agent.js";
import { expectSubagentFollowupReactivation } from "./subagent-followup.test-helpers.js";

View File

@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import { createRunningTaskRun } from "openclaw/plugin-sdk/tasks";
import { listAgentIds } from "../../agents/agent-scope.js";
import type { AgentInternalEvent } from "../../agents/internal-events.js";
import {
@ -29,6 +28,7 @@ import { classifySessionKeyShape, normalizeAgentId } from "../../routing/session
import { defaultRuntime } from "../../runtime.js";
import { normalizeInputProvenance, type InputProvenance } from "../../sessions/input-provenance.js";
import { resolveSendPolicy } from "../../sessions/send-policy.js";
import { createRunningTaskRun } from "../../tasks/task-executor.js";
import {
normalizeDeliveryContext,
normalizeSessionDeliveryFields,

View File

@ -1,4 +1,3 @@
import { getInspectableTaskRegistrySummary } from "openclaw/plugin-sdk/tasks-summary";
import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/runs.js";
import { getTotalPendingReplies } from "../auto-reply/reply/dispatcher-registry.js";
import type { CliDeps } from "../cli/deps.js";
@ -17,6 +16,7 @@ import {
} from "../infra/restart.js";
import { setCommandLaneConcurrency, getTotalQueueSize } from "../process/command-queue.js";
import { CommandLane } from "../process/lanes.js";
import { getInspectableTaskRegistrySummary } from "../tasks/task-registry.maintenance.js";
import type { ChannelHealthMonitor } from "./channel-health-monitor.js";
import type { ChannelKind } from "./config-reload-plan.js";
import type { GatewayReloadPlan } from "./config-reload.js";

View File

@ -1,5 +1,4 @@
import path from "node:path";
import { getInspectableTaskRegistrySummary } from "openclaw/plugin-sdk/tasks-summary";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { getActiveEmbeddedRunCount } from "../agents/pi-embedded-runner/runs.js";
import { registerSkillsChangeListener } from "../agents/skills/refresh.js";
@ -79,6 +78,10 @@ import {
} from "../secrets/runtime.js";
import { onSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
import { onSessionTranscriptUpdate } from "../sessions/transcript-events.js";
import {
getInspectableTaskRegistrySummary,
startTaskRegistryMaintenance,
} from "../tasks/task-registry.maintenance.js";
import { runSetupWizard } from "../wizard/setup.js";
import { createAuthRateLimiter, type AuthRateLimiter } from "./auth-rate-limit.js";
import { startChannelHealthMonitor } from "./channel-health-monitor.js";
@ -905,6 +908,7 @@ export async function startGatewayServer(
});
if (!minimalTestGateway) {
startTaskRegistryMaintenance();
({ tickInterval, healthInterval, dedupeCleanup, mediaCleanup } =
startGatewayMaintenanceTimers({
broadcast,

View File

@ -1,8 +1,6 @@
import { defaultTaskOperationsRuntime } from "../../packages/tasks-host-sdk/src/runtime-core.js";
import { startTaskRegistryMaintenance } from "../../packages/tasks-host-sdk/src/runtime-core.js";
import type { OpenClawPluginService } from "../plugins/types.js";
export * from "../../packages/tasks-host-sdk/src/runtime-core.js";
import { defaultTaskOperationsRuntime } from "../tasks/operations-runtime.js";
import { startTaskRegistryMaintenance } from "../tasks/task-registry.maintenance.js";
export const defaultOperationsRuntime = defaultTaskOperationsRuntime;

View File

@ -1,10 +0,0 @@
import {
createEmptyTaskAuditSummary,
type TaskAuditSummary,
} from "../../packages/tasks-host-sdk/src/task-registry.audit.shared.js";
import { createEmptyTaskRegistrySummary } from "../../packages/tasks-host-sdk/src/task-registry.summary.js";
import type { TaskRegistrySummary } from "../../packages/tasks-host-sdk/src/task-registry.types.js";
export { createEmptyTaskAuditSummary, createEmptyTaskRegistrySummary };
export type { TaskAuditSummary, TaskRegistrySummary };

View File

@ -1,19 +0,0 @@
import {
createEmptyTaskAuditSummary,
type TaskAuditSummary,
} from "../../packages/tasks-host-sdk/src/task-registry.audit.shared.js";
import {
getInspectableTaskAuditSummary,
getInspectableTaskRegistrySummary,
} from "../../packages/tasks-host-sdk/src/task-registry.maintenance.js";
import { createEmptyTaskRegistrySummary } from "../../packages/tasks-host-sdk/src/task-registry.summary.js";
import type { TaskRegistrySummary } from "../../packages/tasks-host-sdk/src/task-registry.types.js";
export {
createEmptyTaskAuditSummary,
createEmptyTaskRegistrySummary,
getInspectableTaskAuditSummary,
getInspectableTaskRegistrySummary,
};
export type { TaskAuditSummary, TaskRegistrySummary };

View File

@ -1,4 +1,3 @@
import { defaultTaskOperationsRuntime } from "openclaw/plugin-sdk/tasks";
import { resolveStateDir } from "../../config/paths.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "../../plugin-sdk/facade-runtime.js";
import { resolveGlobalSingleton } from "../../shared/global-singleton.js";
@ -7,6 +6,7 @@ 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";

View File

@ -1,7 +1,7 @@
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import type { DatabaseSync, StatementSync } from "node:sqlite";
import { requireNodeSqlite } from "../../../src/infra/node-sqlite.js";
import type { DeliveryContext } from "../../../src/utils/delivery-context.js";
import { requireNodeSqlite } from "../infra/node-sqlite.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
import { resolveFlowRegistryDir, resolveFlowRegistrySqlitePath } from "./flow-registry.paths.js";
import type { FlowRegistryStoreSnapshot } from "./flow-registry.store.js";
import type { FlowOutputBag, FlowRecord, FlowShape } from "./flow-registry.types.js";

View File

@ -1,6 +1,6 @@
import { statSync } from "node:fs";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { createFlowRecord, getFlowById, resetFlowRegistryForTests } from "./flow-registry.js";
import { resolveFlowRegistryDir, resolveFlowRegistrySqlitePath } from "./flow-registry.paths.js";
import { configureFlowRegistryRuntime } from "./flow-registry.store.js";

View File

@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import {
createFlowRecord,
deleteFlowRecordById,

View File

@ -1,4 +1,4 @@
import type { DeliveryContext } from "../../../src/utils/delivery-context.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
import type { TaskNotifyPolicy } from "./task-registry.types.js";
export type FlowShape = "single_task" | "linear";

View File

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { getFlowById, resetFlowRegistryForTests, updateFlowRecordById } from "./flow-registry.js";
import {
appendFlowOutput,

View File

@ -1,7 +1,7 @@
import { requestHeartbeatNow } from "../../../src/infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../../src/infra/system-events.js";
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
import { isDeliverableMessageChannel } from "../../../src/utils/message-channel.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { isDeliverableMessageChannel } from "../utils/message-channel.js";
import { createFlowRecord, getFlowById, updateFlowRecordById } from "./flow-registry.js";
import type { FlowOutputBag, FlowOutputValue, FlowRecord } from "./flow-registry.types.js";
import { createQueuedTaskRun, createRunningTaskRun } from "./task-executor.js";

View File

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { defaultTaskOperationsRuntime } from "./operations-runtime.js";
import { findTaskByRunId, resetTaskRegistryForTests } from "./task-registry.js";

View File

@ -10,8 +10,8 @@ import type {
PluginOperationSummary,
PluginOperationsCancelResult,
PluginOperationsRuntime,
} from "../../../src/plugins/operations-state.js";
import { summarizeOperationRecords } from "../../../src/plugins/operations-state.js";
} from "../plugins/operations-state.js";
import { summarizeOperationRecords } from "../plugins/operations-state.js";
import {
listTaskAuditFindings,
type TaskAuditFinding,

View File

@ -14,10 +14,10 @@ const RAW_TASK_MUTATORS = [
] as const;
const ALLOWED_CALLERS = new Set([
"src/operations-runtime.ts",
"src/task-executor.ts",
"src/task-registry.ts",
"src/task-registry.maintenance.ts",
"tasks/operations-runtime.ts",
"tasks/task-executor.ts",
"tasks/task-registry.ts",
"tasks/task-registry.maintenance.ts",
]);
async function listSourceFiles(root: string): Promise<string[]> {

View File

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import {
getFlowById,
listFlowRecords,

View File

@ -1,5 +1,5 @@
import type { OpenClawConfig } from "../../../src/config/config.js";
import { createSubsystemLogger } from "../../../src/logging/subsystem.js";
import type { OpenClawConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import {
createFlowForTask,
createFlowRecord,

View File

@ -0,0 +1 @@
export { sendMessage } from "../infra/outbound/message.js";

View File

@ -6,11 +6,15 @@ const TASK_ROOT = path.resolve(import.meta.dirname);
const SRC_ROOT = path.resolve(TASK_ROOT, "..");
const ALLOWED_IMPORTERS = new Set([
"src/flow-runtime.ts",
"src/operations-runtime.ts",
"src/runtime-core.ts",
"src/task-executor.ts",
"src/task-registry.maintenance.ts",
"agents/tools/session-status-tool.ts",
"auto-reply/reply/commands-acp/runtime-options.ts",
"auto-reply/reply/commands-subagents/action-info.ts",
"commands/doctor-workspace-status.ts",
"commands/flows.ts",
"tasks/flow-runtime.ts",
"tasks/operations-runtime.ts",
"tasks/task-executor.ts",
"tasks/task-registry.maintenance.ts",
]);
async function listSourceFiles(root: string): Promise<string[]> {

View File

@ -1,6 +1,6 @@
import { readAcpSessionEntry } from "../../../src/acp/runtime/session-meta.js";
import { loadSessionStore, resolveStorePath } from "../../../src/config/sessions.js";
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
import { readAcpSessionEntry } from "../acp/runtime/session-meta.js";
import { loadSessionStore, resolveStorePath } from "../config/sessions.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { listTaskAuditFindings, summarizeTaskAuditFindings } from "./task-registry.audit.js";
import type { TaskAuditSummary } from "./task-registry.audit.js";
import {

View File

@ -1,6 +1,6 @@
import os from "node:os";
import path from "node:path";
import { resolveStateDir } from "../../../src/config/paths.js";
import { resolveStateDir } from "../config/paths.js";
export function resolveTaskStateDir(env: NodeJS.ProcessEnv = process.env): string {
const explicit = env.OPENCLAW_STATE_DIR?.trim();

View File

@ -1,7 +1,7 @@
import { chmodSync, existsSync, mkdirSync } from "node:fs";
import type { DatabaseSync, StatementSync } from "node:sqlite";
import { requireNodeSqlite } from "../../../src/infra/node-sqlite.js";
import type { DeliveryContext } from "../../../src/utils/delivery-context.js";
import { requireNodeSqlite } from "../infra/node-sqlite.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
import { resolveTaskRegistryDir, resolveTaskRegistrySqlitePath } from "./task-registry.paths.js";
import type { TaskRegistryStoreSnapshot } from "./task-registry.store.js";
import type { TaskDeliveryState, TaskRecord } from "./task-registry.types.js";

View File

@ -1,13 +1,13 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { startAcpSpawnParentStreamRelay } from "../../../src/agents/acp-spawn-parent-stream.js";
import { emitAgentEvent } from "../../../src/infra/agent-events.js";
import { startAcpSpawnParentStreamRelay } from "../agents/acp-spawn-parent-stream.js";
import { emitAgentEvent } from "../infra/agent-events.js";
import {
hasPendingHeartbeatWake,
resetHeartbeatWakeStateForTests,
} from "../../../src/infra/heartbeat-wake.js";
import { peekSystemEvents, resetSystemEventsForTest } from "../../../src/infra/system-events.js";
import { withTempDir } from "../../../src/test-helpers/temp-dir.js";
import { installInMemoryTaskAndFlowRegistryRuntime } from "../../../src/test-utils/task-flow-registry-runtime.js";
} from "../infra/heartbeat-wake.js";
import { peekSystemEvents, resetSystemEventsForTest } from "../infra/system-events.js";
import { withTempDir } from "../test-helpers/temp-dir.js";
import { installInMemoryTaskAndFlowRegistryRuntime } from "../test-utils/task-flow-registry-runtime.js";
import { createFlowRecord, getFlowById, resetFlowRegistryForTests } from "./flow-registry.js";
import {
createTaskRecord,

View File

@ -1,14 +1,14 @@
import crypto from "node:crypto";
import { getAcpSessionManager } from "../../../src/acp/control-plane/manager.js";
import { killSubagentRunAdmin } from "../../../src/agents/subagent-control.js";
import type { OpenClawConfig } from "../../../src/config/config.js";
import { onAgentEvent } from "../../../src/infra/agent-events.js";
import { requestHeartbeatNow } from "../../../src/infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../../src/infra/system-events.js";
import { createSubsystemLogger } from "../../../src/logging/subsystem.js";
import { parseAgentSessionKey } from "../../../src/routing/session-key.js";
import { normalizeDeliveryContext } from "../../../src/utils/delivery-context.js";
import { isDeliverableMessageChannel } from "../../../src/utils/message-channel.js";
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
import { killSubagentRunAdmin } from "../agents/subagent-control.js";
import type { OpenClawConfig } from "../config/config.js";
import { onAgentEvent } from "../infra/agent-events.js";
import { requestHeartbeatNow } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.js";
import { isDeliverableMessageChannel } from "../utils/message-channel.js";
import { getFlowById, syncFlowFromTask } from "./flow-registry.js";
import {
formatTaskBlockedFollowupMessage,

View File

@ -1,4 +1,4 @@
import type { DeliveryContext } from "../../../src/utils/delivery-context.js";
import type { DeliveryContext } from "../utils/delivery-context.js";
export type TaskRuntime = "subagent" | "acp" | "cli" | "cron";

View File

@ -2,14 +2,14 @@ import {
configureFlowRegistryRuntime,
type FlowRegistryStore,
type FlowRegistryStoreSnapshot,
} from "openclaw/plugin-sdk/tasks";
import type { FlowRecord } from "openclaw/plugin-sdk/tasks";
} from "../tasks/flow-registry.store.js";
import type { FlowRecord } from "../tasks/flow-registry.types.js";
import {
configureTaskRegistryRuntime,
type TaskRegistryStore,
type TaskRegistryStoreSnapshot,
} from "openclaw/plugin-sdk/tasks";
import type { TaskDeliveryState, TaskRecord } from "openclaw/plugin-sdk/tasks";
} from "../tasks/task-registry.store.js";
import type { TaskDeliveryState, TaskRecord } from "../tasks/task-registry.types.js";
function cloneTask(task: TaskRecord): TaskRecord {
return { ...task };

View File

@ -13,8 +13,7 @@
"include": [
"src/plugin-sdk/**/*.ts",
"src/types/**/*.d.ts",
"packages/memory-host-sdk/src/**/*.ts",
"packages/tasks-host-sdk/src/**/*.ts"
"packages/memory-host-sdk/src/**/*.ts"
],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}