diff --git a/extensions/tasks/api.ts b/extensions/tasks/api.ts deleted file mode 100644 index 6606fb316b4..00000000000 --- a/extensions/tasks/api.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./runtime-api.js"; diff --git a/extensions/tasks/index.test.ts b/extensions/tasks/index.test.ts deleted file mode 100644 index d9da16de07e..00000000000 --- a/extensions/tasks/index.test.ts +++ /dev/null @@ -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); - }); -}); diff --git a/extensions/tasks/index.ts b/extensions/tasks/index.ts deleted file mode 100644 index a2ea2834716..00000000000 --- a/extensions/tasks/index.ts +++ /dev/null @@ -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, - }, - ], - }, - ); - }, -}); diff --git a/extensions/tasks/openclaw.plugin.json b/extensions/tasks/openclaw.plugin.json deleted file mode 100644 index d277f3e83c0..00000000000 --- a/extensions/tasks/openclaw.plugin.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "tasks", - "enabledByDefault": true, - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": {} - } -} diff --git a/extensions/tasks/package.json b/extensions/tasks/package.json deleted file mode 100644 index ad0bf7152d8..00000000000 --- a/extensions/tasks/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@openclaw/tasks", - "version": "2026.3.31", - "private": true, - "description": "OpenClaw durable tasks plugin", - "type": "module", - "openclaw": { - "extensions": [ - "./index.ts" - ] - } -} diff --git a/extensions/tasks/runtime-api.ts b/extensions/tasks/runtime-api.ts deleted file mode 100644 index befa0e3b83d..00000000000 --- a/extensions/tasks/runtime-api.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - createDefaultOperationsMaintenanceService, - defaultOperationsRuntime, -} from "openclaw/plugin-sdk/tasks"; diff --git a/extensions/tasks/src/cli.runtime.test.ts b/extensions/tasks/src/cli.runtime.test.ts deleted file mode 100644 index 2901096c43c..00000000000 --- a/extensions/tasks/src/cli.runtime.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/extensions/tasks/src/cli.runtime.ts b/extensions/tasks/src/cli.runtime.ts deleted file mode 100644 index 5aa9a1a62c1..00000000000 --- a/extensions/tasks/src/cli.runtime.ts +++ /dev/null @@ -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) { - const summary = { - total: 0, - warnings: 0, - errors: 0, - byCode: {} as Record, - }; - 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 { - 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."); - } -} diff --git a/extensions/tasks/src/cli.ts b/extensions/tasks/src/cli.ts deleted file mode 100644 index c239d679df4..00000000000 --- a/extensions/tasks/src/cli.ts +++ /dev/null @@ -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 ", "Filter by kind (subagent, acp, cron, cli)") - .option( - "--status ", - "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 ", "Filter by kind (subagent, acp, cron, cli)") - .option( - "--status ", - "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 ", "Filter by severity (warn, error)") - .option("--code ", "Filter by finding code") - .option("--limit ", "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("", "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("", "Task id, run id, or session key") - .argument("", "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("", "Task id, run id, or session key") - .action(async (lookup) => { - await runTasksCancel( - { - lookup, - }, - deps, - defaultRuntime, - ); - }); -} diff --git a/package.json b/package.json index 10ace073f68..cd3cc31c00a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/tasks-host-sdk/package.json b/packages/tasks-host-sdk/package.json deleted file mode 100644 index be18d95586f..00000000000 --- a/packages/tasks-host-sdk/package.json +++ /dev/null @@ -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" - } -} diff --git a/packages/tasks-host-sdk/src/runtime-core.ts b/packages/tasks-host-sdk/src/runtime-core.ts deleted file mode 100644 index 3159e25c779..00000000000 --- a/packages/tasks-host-sdk/src/runtime-core.ts +++ /dev/null @@ -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"; diff --git a/packages/tasks-host-sdk/src/task-registry-delivery-runtime.ts b/packages/tasks-host-sdk/src/task-registry-delivery-runtime.ts deleted file mode 100644 index 90aa6cff4a4..00000000000 --- a/packages/tasks-host-sdk/src/task-registry-delivery-runtime.ts +++ /dev/null @@ -1 +0,0 @@ -export { sendMessage } from "../../../src/infra/outbound/message.js"; diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 3ad39e5cc60..b103e7c7d9e 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -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", diff --git a/src/acp/control-plane/manager.core.ts b/src/acp/control-plane/manager.core.ts index 61ff2d05dcd..526b324d168 100644 --- a/src/acp/control-plane/manager.core.ts +++ b/src/acp/control-plane/manager.core.ts @@ -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, diff --git a/src/acp/control-plane/manager.test.ts b/src/acp/control-plane/manager.test.ts index db76f9edc4a..477c4451cf8 100644 --- a/src/acp/control-plane/manager.test.ts +++ b/src/acp/control-plane/manager.test.ts @@ -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")); }); diff --git a/src/agents/acp-spawn-parent-stream.ts b/src/agents/acp-spawn-parent-stream.ts index a42ad960d1d..1c7d6786ec7 100644 --- a/src/agents/acp-spawn-parent-stream.ts +++ b/src/agents/acp-spawn-parent-stream.ts @@ -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; diff --git a/src/agents/acp-spawn.test.ts b/src/agents/acp-spawn.test.ts index 125ca230fe5..b9a4657e9db 100644 --- a/src/agents/acp-spawn.test.ts +++ b/src/agents/acp-spawn.test.ts @@ -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 { diff --git a/src/agents/acp-spawn.ts b/src/agents/acp-spawn.ts index 69bd8a3664b..841293965d4 100644 --- a/src/agents/acp-spawn.ts +++ b/src/agents/acp-spawn.ts @@ -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, diff --git a/src/agents/openclaw-tools.session-status.test.ts b/src/agents/openclaw-tools.session-status.test.ts index 5c72dd0c356..e41ae65a5c3 100644 --- a/src/agents/openclaw-tools.session-status.test.ts +++ b/src/agents/openclaw-tools.session-status.test.ts @@ -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")); diff --git a/src/agents/subagent-registry-lifecycle.ts b/src/agents/subagent-registry-lifecycle.ts index 6a2d433941d..06134fe0b8b 100644 --- a/src/agents/subagent-registry-lifecycle.ts +++ b/src/agents/subagent-registry-lifecycle.ts @@ -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, diff --git a/src/agents/subagent-registry-run-manager.ts b/src/agents/subagent-registry-run-manager.ts index 024ab84cf7e..33e70961de0 100644 --- a/src/agents/subagent-registry-run-manager.ts +++ b/src/agents/subagent-registry-run-manager.ts @@ -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"; diff --git a/src/agents/tools/session-status-tool.ts b/src/agents/tools/session-status-tool.ts index e5cdaeff7ee..d5338de4e3e 100644 --- a/src/agents/tools/session-status-tool.ts +++ b/src/agents/tools/session-status-tool.ts @@ -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"; diff --git a/src/auto-reply/reply/commands-acp.test.ts b/src/auto-reply/reply/commands-acp.test.ts index 1bcb66f11d3..e41fe6d9ae9 100644 --- a/src/auto-reply/reply/commands-acp.test.ts +++ b/src/auto-reply/reply/commands-acp.test.ts @@ -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, ""); diff --git a/src/auto-reply/reply/commands-acp/runtime-options.ts b/src/auto-reply/reply/commands-acp/runtime-options.ts index 199f6b4702d..f88c88caed1 100644 --- a/src/auto-reply/reply/commands-acp/runtime-options.ts +++ b/src/auto-reply/reply/commands-acp/runtime-options.ts @@ -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, diff --git a/src/auto-reply/reply/commands-subagents/action-info.ts b/src/auto-reply/reply/commands-subagents/action-info.ts index 87ad82b2df3..69f39f9cb26 100644 --- a/src/auto-reply/reply/commands-subagents/action-info.ts +++ b/src/auto-reply/reply/commands-subagents/action-info.ts @@ -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 { diff --git a/src/auto-reply/reply/commands.test.ts b/src/auto-reply/reply/commands.test.ts index f6720d9119e..555dc26a6f7 100644 --- a/src/auto-reply/reply/commands.test.ts +++ b/src/auto-reply/reply/commands.test.ts @@ -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(); diff --git a/src/cli/program/register.status-health-sessions.test.ts b/src/cli/program/register.status-health-sessions.test.ts index ba5053fc2e6..f7c222a6893 100644 --- a/src/cli/program/register.status-health-sessions.test.ts +++ b/src/cli/program/register.status-health-sessions.test.ts @@ -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"]); diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index 85ae149528f..992a9663f2b 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -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 ", "Filter by kind (subagent, acp, cron, cli)") + .option( + "--status ", + "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 ", "Filter by kind (subagent, acp, cron, cli)") + .option( + "--status ", + "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 ", "Filter by severity (warn, error)") + .option( + "--code ", + "Filter by finding code (stale_queued, stale_running, lost, delivery_failed, missing_cleanup, inconsistent_timestamps)", + ) + .option("--limit ", "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("", "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("", "Task id, run id, or session key") + .argument("", "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("", "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") diff --git a/src/cli/run-main.exit.test.ts b/src/cli/run-main.exit.test.ts index 2517c58e59c..dd29b76bb1d 100644 --- a/src/cli/run-main.exit.test.ts +++ b/src/cli/run-main.exit.test.ts @@ -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, })); diff --git a/src/commands/doctor-workspace-status.test.ts b/src/commands/doctor-workspace-status.test.ts index d1b85594359..595734fd2c1 100644 --- a/src/commands/doctor-workspace-status.test.ts +++ b/src/commands/doctor-workspace-status.test.ts @@ -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), })); diff --git a/src/commands/doctor-workspace-status.ts b/src/commands/doctor-workspace-status.ts index c2464b0fb6b..21a5c56de4d 100644 --- a/src/commands/doctor-workspace-status.ts +++ b/src/commands/doctor-workspace-status.ts @@ -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"; diff --git a/src/commands/flows.test.ts b/src/commands/flows.test.ts index f7abba7e5bf..21bceee11a9 100644 --- a/src/commands/flows.test.ts +++ b/src/commands/flows.test.ts @@ -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), })); diff --git a/src/commands/flows.ts b/src/commands/flows.ts index 484632e4f1f..b52a2b6f33e 100644 --- a/src/commands/flows.ts +++ b/src/commands/flows.ts @@ -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; diff --git a/src/commands/status.scan.json-core.ts b/src/commands/status.scan.json-core.ts index 743f04d5fb9..784f506a970 100644 --- a/src/commands/status.scan.json-core.ts +++ b/src/commands/status.scan.json-core.ts @@ -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 { diff --git a/src/commands/status.scan.ts b/src/commands/status.scan.ts index c4ff119cd70..6c19e31710b 100644 --- a/src/commands/status.scan.ts +++ b/src/commands/status.scan.ts @@ -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"; diff --git a/src/commands/status.summary.test.ts b/src/commands/status.summary.test.ts index 479732aeda3..65f6d6fa3c9 100644 --- a/src/commands/status.summary.test.ts +++ b/src/commands/status.summary.test.ts @@ -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, diff --git a/src/commands/status.summary.ts b/src/commands/status.summary.ts index 15df134f103..980f78d5cb6 100644 --- a/src/commands/status.summary.ts +++ b/src/commands/status.summary.ts @@ -17,7 +17,7 @@ let channelSummaryModulePromise: Promise | undefined; let configIoModulePromise: Promise | undefined; let taskRegistryMaintenanceModulePromise: - | Promise + | Promise | 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; } diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index a71b0f0429a..bf4bcd087c3 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -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, })); diff --git a/src/commands/status.types.ts b/src/commands/status.types.ts index f603b1caecc..61650340f43 100644 --- a/src/commands/status.types.ts +++ b/src/commands/status.types.ts @@ -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; diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts new file mode 100644 index 00000000000..8d263b5e037 --- /dev/null +++ b/src/commands/tasks.ts @@ -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."); + } +} diff --git a/src/cron/service/ops.test.ts b/src/cron/service/ops.test.ts index 77949791d97..108beffcfc8 100644 --- a/src/cron/service/ops.test.ts +++ b/src/cron/service/ops.test.ts @@ -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"; diff --git a/src/cron/service/ops.ts b/src/cron/service/ops.ts index 59152051e8f..1daeeccdf80 100644 --- a/src/cron/service/ops.ts +++ b/src/cron/service/ops.ts @@ -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 { diff --git a/src/cron/service/timer.test.ts b/src/cron/service/timer.test.ts index ca5de3e9750..e6009c42fdc 100644 --- a/src/cron/service/timer.test.ts +++ b/src/cron/service/timer.test.ts @@ -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", diff --git a/src/cron/service/timer.ts b/src/cron/service/timer.ts index 1bb4cc48219..2e167cf6d72 100644 --- a/src/cron/service/timer.ts +++ b/src/cron/service/timer.ts @@ -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 { diff --git a/src/gateway/server-methods/agent.test.ts b/src/gateway/server-methods/agent.test.ts index 3485bbc229b..5ee015a63f9 100644 --- a/src/gateway/server-methods/agent.test.ts +++ b/src/gateway/server-methods/agent.test.ts @@ -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"; diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 9dad99ba5e0..7ce897b7018 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -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, diff --git a/src/gateway/server-reload-handlers.ts b/src/gateway/server-reload-handlers.ts index e58610e375f..ff56554baeb 100644 --- a/src/gateway/server-reload-handlers.ts +++ b/src/gateway/server-reload-handlers.ts @@ -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"; diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index e7d09c2cdd5..a20e37e794d 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -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, diff --git a/src/plugin-sdk/tasks.ts b/src/plugin-sdk/operations-default.ts similarity index 56% rename from src/plugin-sdk/tasks.ts rename to src/plugin-sdk/operations-default.ts index 51cc908ab63..ae928552702 100644 --- a/src/plugin-sdk/tasks.ts +++ b/src/plugin-sdk/operations-default.ts @@ -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; diff --git a/src/plugin-sdk/tasks-empty-summary.ts b/src/plugin-sdk/tasks-empty-summary.ts deleted file mode 100644 index c684ba70be6..00000000000 --- a/src/plugin-sdk/tasks-empty-summary.ts +++ /dev/null @@ -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 }; diff --git a/src/plugin-sdk/tasks-summary.ts b/src/plugin-sdk/tasks-summary.ts deleted file mode 100644 index 5ed14ee6aba..00000000000 --- a/src/plugin-sdk/tasks-summary.ts +++ /dev/null @@ -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 }; diff --git a/src/plugins/runtime/index.ts b/src/plugins/runtime/index.ts index ffee1ccea53..f72954cb915 100644 --- a/src/plugins/runtime/index.ts +++ b/src/plugins/runtime/index.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/flow-registry.paths.ts b/src/tasks/flow-registry.paths.ts similarity index 100% rename from packages/tasks-host-sdk/src/flow-registry.paths.ts rename to src/tasks/flow-registry.paths.ts diff --git a/packages/tasks-host-sdk/src/flow-registry.store.sqlite.ts b/src/tasks/flow-registry.store.sqlite.ts similarity index 98% rename from packages/tasks-host-sdk/src/flow-registry.store.sqlite.ts rename to src/tasks/flow-registry.store.sqlite.ts index 3aef47ad29e..ecbd3e000dd 100644 --- a/packages/tasks-host-sdk/src/flow-registry.store.sqlite.ts +++ b/src/tasks/flow-registry.store.sqlite.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/flow-registry.store.test.ts b/src/tasks/flow-registry.store.test.ts similarity index 98% rename from packages/tasks-host-sdk/src/flow-registry.store.test.ts rename to src/tasks/flow-registry.store.test.ts index e2a884339d1..c0e742bd2f8 100644 --- a/packages/tasks-host-sdk/src/flow-registry.store.test.ts +++ b/src/tasks/flow-registry.store.test.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/flow-registry.store.ts b/src/tasks/flow-registry.store.ts similarity index 100% rename from packages/tasks-host-sdk/src/flow-registry.store.ts rename to src/tasks/flow-registry.store.ts diff --git a/packages/tasks-host-sdk/src/flow-registry.test.ts b/src/tasks/flow-registry.test.ts similarity index 99% rename from packages/tasks-host-sdk/src/flow-registry.test.ts rename to src/tasks/flow-registry.test.ts index f3648af7097..a6a72322904 100644 --- a/packages/tasks-host-sdk/src/flow-registry.test.ts +++ b/src/tasks/flow-registry.test.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/flow-registry.ts b/src/tasks/flow-registry.ts similarity index 100% rename from packages/tasks-host-sdk/src/flow-registry.ts rename to src/tasks/flow-registry.ts diff --git a/packages/tasks-host-sdk/src/flow-registry.types.ts b/src/tasks/flow-registry.types.ts similarity index 91% rename from packages/tasks-host-sdk/src/flow-registry.types.ts rename to src/tasks/flow-registry.types.ts index c35d6a55b1e..188f5c9ceeb 100644 --- a/packages/tasks-host-sdk/src/flow-registry.types.ts +++ b/src/tasks/flow-registry.types.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/flow-runtime.test.ts b/src/tasks/flow-runtime.test.ts similarity index 99% rename from packages/tasks-host-sdk/src/flow-runtime.test.ts rename to src/tasks/flow-runtime.test.ts index 7e8437d1424..1d37a777a46 100644 --- a/packages/tasks-host-sdk/src/flow-runtime.test.ts +++ b/src/tasks/flow-runtime.test.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/flow-runtime.ts b/src/tasks/flow-runtime.ts similarity index 97% rename from packages/tasks-host-sdk/src/flow-runtime.ts rename to src/tasks/flow-runtime.ts index 31c5e94f6d6..0384dc5f139 100644 --- a/packages/tasks-host-sdk/src/flow-runtime.ts +++ b/src/tasks/flow-runtime.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/operations-runtime.test.ts b/src/tasks/operations-runtime.test.ts similarity index 98% rename from packages/tasks-host-sdk/src/operations-runtime.test.ts rename to src/tasks/operations-runtime.test.ts index 563a02ecf05..88fb8e007ce 100644 --- a/packages/tasks-host-sdk/src/operations-runtime.test.ts +++ b/src/tasks/operations-runtime.test.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/operations-runtime.ts b/src/tasks/operations-runtime.ts similarity index 98% rename from packages/tasks-host-sdk/src/operations-runtime.ts rename to src/tasks/operations-runtime.ts index b1e5b535a61..ba207b540eb 100644 --- a/packages/tasks-host-sdk/src/operations-runtime.ts +++ b/src/tasks/operations-runtime.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/task-executor-boundary.test.ts b/src/tasks/task-executor-boundary.test.ts similarity index 92% rename from packages/tasks-host-sdk/src/task-executor-boundary.test.ts rename to src/tasks/task-executor-boundary.test.ts index 57d3e51c359..b2c5af37892 100644 --- a/packages/tasks-host-sdk/src/task-executor-boundary.test.ts +++ b/src/tasks/task-executor-boundary.test.ts @@ -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 { diff --git a/packages/tasks-host-sdk/src/task-executor-policy.test.ts b/src/tasks/task-executor-policy.test.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-executor-policy.test.ts rename to src/tasks/task-executor-policy.test.ts diff --git a/packages/tasks-host-sdk/src/task-executor-policy.ts b/src/tasks/task-executor-policy.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-executor-policy.ts rename to src/tasks/task-executor-policy.ts diff --git a/packages/tasks-host-sdk/src/task-executor.test.ts b/src/tasks/task-executor.test.ts similarity index 99% rename from packages/tasks-host-sdk/src/task-executor.test.ts rename to src/tasks/task-executor.test.ts index 6b449acf9cd..f8403a9b56e 100644 --- a/packages/tasks-host-sdk/src/task-executor.test.ts +++ b/src/tasks/task-executor.test.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/task-executor.ts b/src/tasks/task-executor.ts similarity index 98% rename from packages/tasks-host-sdk/src/task-executor.ts rename to src/tasks/task-executor.ts index 804dcccac2f..b2e955ce768 100644 --- a/packages/tasks-host-sdk/src/task-executor.ts +++ b/src/tasks/task-executor.ts @@ -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, diff --git a/src/tasks/task-registry-delivery-runtime.ts b/src/tasks/task-registry-delivery-runtime.ts new file mode 100644 index 00000000000..0ffc31c2431 --- /dev/null +++ b/src/tasks/task-registry-delivery-runtime.ts @@ -0,0 +1 @@ +export { sendMessage } from "../infra/outbound/message.js"; diff --git a/packages/tasks-host-sdk/src/task-registry-import-boundary.test.ts b/src/tasks/task-registry-import-boundary.test.ts similarity index 79% rename from packages/tasks-host-sdk/src/task-registry-import-boundary.test.ts rename to src/tasks/task-registry-import-boundary.test.ts index f57928d3426..8be3a7bce1f 100644 --- a/packages/tasks-host-sdk/src/task-registry-import-boundary.test.ts +++ b/src/tasks/task-registry-import-boundary.test.ts @@ -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 { diff --git a/packages/tasks-host-sdk/src/task-registry.audit.shared.ts b/src/tasks/task-registry.audit.shared.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.audit.shared.ts rename to src/tasks/task-registry.audit.shared.ts diff --git a/packages/tasks-host-sdk/src/task-registry.audit.test.ts b/src/tasks/task-registry.audit.test.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.audit.test.ts rename to src/tasks/task-registry.audit.test.ts diff --git a/packages/tasks-host-sdk/src/task-registry.audit.ts b/src/tasks/task-registry.audit.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.audit.ts rename to src/tasks/task-registry.audit.ts diff --git a/packages/tasks-host-sdk/src/task-registry.maintenance.ts b/src/tasks/task-registry.maintenance.ts similarity index 96% rename from packages/tasks-host-sdk/src/task-registry.maintenance.ts rename to src/tasks/task-registry.maintenance.ts index 2b87ed4bd1b..f639e122172 100644 --- a/packages/tasks-host-sdk/src/task-registry.maintenance.ts +++ b/src/tasks/task-registry.maintenance.ts @@ -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 { diff --git a/packages/tasks-host-sdk/src/task-registry.paths.ts b/src/tasks/task-registry.paths.ts similarity index 91% rename from packages/tasks-host-sdk/src/task-registry.paths.ts rename to src/tasks/task-registry.paths.ts index 009c712fccf..35488f76b54 100644 --- a/packages/tasks-host-sdk/src/task-registry.paths.ts +++ b/src/tasks/task-registry.paths.ts @@ -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(); diff --git a/packages/tasks-host-sdk/src/task-registry.reconcile.ts b/src/tasks/task-registry.reconcile.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.reconcile.ts rename to src/tasks/task-registry.reconcile.ts diff --git a/packages/tasks-host-sdk/src/task-registry.store.sqlite.ts b/src/tasks/task-registry.store.sqlite.ts similarity index 98% rename from packages/tasks-host-sdk/src/task-registry.store.sqlite.ts rename to src/tasks/task-registry.store.sqlite.ts index 536d93b279b..80ad7de93b1 100644 --- a/packages/tasks-host-sdk/src/task-registry.store.sqlite.ts +++ b/src/tasks/task-registry.store.sqlite.ts @@ -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"; diff --git a/packages/tasks-host-sdk/src/task-registry.store.test.ts b/src/tasks/task-registry.store.test.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.store.test.ts rename to src/tasks/task-registry.store.test.ts diff --git a/packages/tasks-host-sdk/src/task-registry.store.ts b/src/tasks/task-registry.store.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.store.ts rename to src/tasks/task-registry.store.ts diff --git a/packages/tasks-host-sdk/src/task-registry.summary.ts b/src/tasks/task-registry.summary.ts similarity index 100% rename from packages/tasks-host-sdk/src/task-registry.summary.ts rename to src/tasks/task-registry.summary.ts diff --git a/packages/tasks-host-sdk/src/task-registry.test.ts b/src/tasks/task-registry.test.ts similarity index 99% rename from packages/tasks-host-sdk/src/task-registry.test.ts rename to src/tasks/task-registry.test.ts index cbbca737028..9e693e030ff 100644 --- a/packages/tasks-host-sdk/src/task-registry.test.ts +++ b/src/tasks/task-registry.test.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/task-registry.ts b/src/tasks/task-registry.ts similarity index 98% rename from packages/tasks-host-sdk/src/task-registry.ts rename to src/tasks/task-registry.ts index 8b865fa5e4b..89c83f2eea7 100644 --- a/packages/tasks-host-sdk/src/task-registry.ts +++ b/src/tasks/task-registry.ts @@ -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, diff --git a/packages/tasks-host-sdk/src/task-registry.types.ts b/src/tasks/task-registry.types.ts similarity index 95% rename from packages/tasks-host-sdk/src/task-registry.types.ts rename to src/tasks/task-registry.types.ts index 242278b7119..74a2296dba0 100644 --- a/packages/tasks-host-sdk/src/task-registry.types.ts +++ b/src/tasks/task-registry.types.ts @@ -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"; diff --git a/src/test-utils/task-flow-registry-runtime.ts b/src/test-utils/task-flow-registry-runtime.ts index f50a343e99b..371c264c102 100644 --- a/src/test-utils/task-flow-registry-runtime.ts +++ b/src/test-utils/task-flow-registry-runtime.ts @@ -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 }; diff --git a/tsconfig.plugin-sdk.dts.json b/tsconfig.plugin-sdk.dts.json index 3b24fe15d5b..b3245912c27 100644 --- a/tsconfig.plugin-sdk.dts.json +++ b/tsconfig.plugin-sdk.dts.json @@ -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"] }