Tasks:永続化タスク
責務
task-executor.ts と detached-task-runtime.ts が OpenClaw の「タスク (task)」サブシステムを構成します:プロセス跨ぎ、セッション跨ぎ、再起動跨ぎが起こり得る任意の非同期作業をライフサイクル付きの 1 レコードにモデル化し,それがどの runtime(subagent / acp / cli / cron)で走っていようと,同じ状態機械 (state machine)、同じ SQLite 永続化 (persistence)、同じキャンセルと回収セマンティクスを共有します。CronService はスケジューリング時機を担当し,task-executor.ts は「1 レコードが queued から succeeded/failed/timed_out/cancelled/lost までどう進むか」を担当します。
タスクはスレッドでも worker でもありません——状態オブジェクト(TaskRecord,TaskRecord:116-146)に,対応する runtime が自身で実装したライフサイクルフックを加えただけです。runtime(subagent/acp/cli/cron,TaskRuntime:5)がどう本当に実行するかを決め,task システムは:記録保存、状態遷移、元セッションへの投递、期限切れレコードの定期クリーンアップだけを管轄します。
設計動機
なぜ cron だけでは足りず,task 層を別途抽出するのか?
- runtime 跨ぎ:cron は定期,ACP は IDE がトリガーする prompt,subagent は agent 主ループが派生する子タスク,CLI はユーザが端末で叩くコマンド。4 種の runtime のトリガー時機は完全に異なりますが,すべて同じ問題に直面します:プロセス崩壊後にある作業が未完了だとどう知るか?gateway 再起動後にどう回収するか?結果をどう元セッションに戻すか?これらを task registry に抽象すれば,各 runtime は自身の
DetachedTaskLifecycleRuntime(DEFAULT_DETACHED_TASK_LIFECYCLE_RUNTIME:35-49)だけを実装し,他のロジックは共有できます。 - flow 概念:多くのタスクはより大きな flow に属します(例:1 つの agent run が 3 つの子タスクをトリガーし,主タスクがキャンセルされれば 3 つの子タスクもキャンセル)。
task-flow-registry(src/tasks/task-flow-registry.ts)がタスクに親 flow を付け,バッチキャンセル、バッチ状態照会を可能にします。detached ACP/subagent run に対しては,executor が自動で「one-task flow」を包み(isOneTaskFlowEligible + ensureSingleTaskFlow:46-92),単発 run でも flow の状態/再試行面を享受できます。 - delivery 状態:タスク終了後,結果は必ず元セッションに戻るとは限りません——ユーザが IDE を閉じたか,cron が isolated session で元セッションがないかもしれません。
TaskDeliveryStatus(TaskDeliveryStatus:16-23)がこの状態を単独追跡します:pending/delivered/session_queued/failed/parent_missing/not_applicableで,「タスク実行成功」と「結果がクライアントに届いた」を疎結合にします。
主要ファイル
task-registry.types.ts:1-146—TaskRuntime/TaskStatus/TaskDeliveryStatus/TaskNotifyPolicy/TaskRecordの全型宣言。task-executor.ts:46-127—ensureSingleTaskFlow+createQueuedTaskRun+createRunningTaskRun,タスク作成入口(one-task flow 包装含む)。detached-task-runtime.ts:32-66—DetachedTaskLifecycleRuntimeインターフェース、デフォルト runtime、プラグイン登録機構。tryRecoverTaskBeforeMarkLost:134-171— 回収 hook:best-effort,5 秒閾値警告,任意のエラーは mark-lost に fallback。task-registry.process-state.ts:1-32— プロセスレベル in-memory 索引(tasks/taskIdsByRunId/taskIdsByOwnerKey/taskIdsByParentFlowId/taskIdsByRelatedSessionKey/tasksWithPendingDelivery),Symbol.for("openclaw.taskRegistry.state")で globalThis に付加。task-registry.store.sqlite.ts:48-77— Kysely でマッピングされたtask_runs+task_delivery_stateテーブル,openclaw-state.dbにミラー。task-registry.maintenance.ts:73-100— バックグラウンド sweeper,周期 60s,TASK_STALE_RUNNING_MS=30*60_000,TASK_RECONCILE_GRACE_MS=5*60_000。task-retention.ts:4-29— 終態タスクは 7 日保持,lost タスクは 1 日だけ。src/tasks/task-flow-registry.store.sqlite.ts— flow 永続化,parentFlowIdカスケード対応。cron-task-cancel.ts:12-77— プロセスレベル active cron task run のキャンセルハンドル,settlement grace 60s。task-registry.reconcile.ts— 公共 facade:reconcileInspectableTasks/reconcileTaskLookupToken。
データフロー
タスクの状態機械は 7 状態だけ(TaskStatus:7-14)です:
export type TaskStatus =
| "queued" // 落庫済み,runtime が引上げ待ち
| "running" // runtime が実行開始を報告
| "succeeded" // 終態:成功
| "failed" // 終態:失敗(error 付き)
| "timed_out" // 終態:タイムアウト
| "cancelled" // 終態:ユーザ/システムキャンセル
| "lost"; // 終態:sweeper が grace 後にも活発を観測できず,lost とマークcreateQueuedTaskRun / createRunningTaskRun が作成入口で,両者とも ensureSingleTaskFlow(ensureSingleTaskFlow:56)を呼びます:
function isOneTaskFlowEligible(task: TaskRecord): boolean {
if (task.parentFlowId?.trim() || task.scopeKind !== "session") return false;
if (task.deliveryStatus === "not_applicable") return false;
return task.runtime === "acp" || task.runtime === "subagent";
}
function ensureSingleTaskFlow(params: { task; requesterOrigin? }): TaskRecord {
if (!isOneTaskFlowEligible(params.task)) return params.task;
try {
const flow = createTaskFlowForTask({ task: params.task, requesterOrigin: params.requesterOrigin });
if (!flow) return params.task;
const linked = linkTaskToFlowById({ taskId: params.task.taskId, flowId: flow.flowId });
if (!linked) { deleteTaskFlowRecordById(flow.flowId); return params.task; }
if (linked.parentFlowId !== flow.flowId) { deleteTaskFlowRecordById(flow.flowId); return linked; }
return linked;
} catch (error) {
log.warn("Failed to create one-task flow for detached run", { ... });
return params.task;
}
}重要な判定:scopeKind === "session" かつ runtime ∈ {acp, subagent} かつ deliveryStatus !== "not_applicable"——この 3 つが満たされた時だけ自動で flow を包みます。システムレベルタスク(scopeKind: "system")と cron タスクは独自の cancel/delivery パスがあり,flow は再利用しません。失敗時は warn だけでスローせず,タスク作成自体が flow 包装失敗でブロックされないようにします。
detached-task-runtime.ts(getDetachedTaskLifecycleRuntime:47)は統一 API を外部に露出しますが,具体実装はプラグインで置き換え可能です:
const DEFAULT_DETACHED_TASK_LIFECYCLE_RUNTIME: DetachedTaskLifecycleRuntime = {
createQueuedTaskRun: createQueuedTaskRunFromExecutor,
createRunningTaskRun: createRunningTaskRunFromExecutor,
startTaskRunByRunId: startTaskRunByRunIdFromExecutor,
recordTaskRunProgressByRunId: recordTaskRunProgressByRunIdFromExecutor,
finalizeTaskRunByRunId: finalizeTaskRunByRunIdFromExecutor,
completeTaskRunByRunId: completeTaskRunByRunIdFromExecutor,
failTaskRunByRunId: failTaskRunByRunIdFromExecutor,
setDetachedTaskDeliveryStatusByRunId: setDetachedTaskDeliveryStatusByRunIdFromExecutor,
cancelDetachedTaskRunById: cancelDetachedTaskRunByIdInCore,
};getDetachedTaskLifecycleRuntime() はプラグイン登録の実装を優先返し,なければデフォルトを使います——これで plugin が task ライフサイクル全体を接管(例:task を独立 worker プロセスで走らせる)でき,task-executor を変える必要がありません。
境界と失敗
- プロセスレベル索引の symbol-keyed globalThis:
getTaskRegistryProcessState()(getTaskRegistryProcessState:18)はSymbol.for("openclaw.taskRegistry.state")でglobalThisに付加し,モジュールホットリロード(dev watch)やテスト隔離時にも索引を共有——さもなくば異なるモジュールインスタンスがそれぞれ in-memory 索引を持ち,SQLite に落としたタスクとメモリ索引が不一致になります。 - sweeper バッチ yield:
SWEEP_YIELD_BATCH_SIZE = 25(SWEEP_YIELD_BATCH_SIZE:83)で,25 タスク処理ごとにイベントループを明け,大批量クリーンアップでメインスレッドをブロックしないようにします。 - stale running 判定:
TASK_STALE_RUNNING_MS = 30 * 60_000——running 状態が 30 分以上イベント更新(recordTaskRunProgressByRunId含む)なしのときだけ sweeper が回収を開始します。これは遅いタスクに余裕を持たせ,正常な長時間実行を誤殺しません。 - recovery hook 耐障害:
tryRecoverTaskBeforeMarkLost(tryRecoverTaskBeforeMarkLost:134)は hook 呼び出し全体を try/catch で包みます——プラグイン実装が throw しても,不正オブジェクトを返しても,5s 閾値を超えても,warn だけで mark-lost に進みます。理由:recovery は best-effort で,悪いプラグインがクリーンアップをブロックしてはいけません。 - retention 二本立て:終態タスクは 7 日保持,lost タスクは 1 日だけ(
resolveTaskRetentionMs:5-9)。lost は「sweeper が推測した終態」で,信頼度は runtime が明示的に報告した succeeded/failed より低く,保持期間を短くします——排查ウィンドウを残しつつ「幽霊タスク」が長期に DB を占拠しないように。 - one-task flow 失敗降格:
ensureSingleTaskFlowはどのステップでエラーを投げても元taskを返し(再包装せず),ログに warn を残します。タスク作成が成功することの方が flow 包装の完全性より重要だからです。 - scopeKind 区別:
scopeKind: "system"のタスクにはrequesterSessionKeyがなく(system-scope requesterSessionKey:92-94),ownerKey が唯一の検索アンカーです——これでシステムタスクとある session の ownerKey が衝突するのを回避します。 - cron-task-cancel settlement grace:gateway 再起動時に
abortActiveCronTaskRuns(abortActiveCronTaskRunSettlementGrace:50)が 60 秒の「沈降寛限期」を起動し,abort された promise が finally でリソースを掃除し終態を書き終わる時間を稼ぎます——直接表を clear すると cleanup が迷子になります。
まとめ
Tasks サブシステムは「1 条の非同期作業」を 7 状態の TaskRecord に抽象化し,4 種の runtime(subagent/acp/cli/cron)が 1 套の状態機械 + SQLite 永続化 + sweeper 回収を共有します。Cron:定期タスク とは双方向関係です:cron は registerActiveCronTaskRun で自身を task プロセスレベル表に掛け,task システムは cron-task-cancel.ts で cron に「再起動時の活発 run 打断」能力を提供します。ACP が起動する detached prompt も同じ套を走り(詳しくは ACP:IDE 橋接),runtime が "acp" とマークされるだけです。session 関連状態と task delivery 回投は Memory ファイル 内の session store パスを使います。