Skip to content

Commit 853c842

Browse files
committed
恢复无执行记录的遗留运行任务
1 parent ea32324 commit 853c842

5 files changed

Lines changed: 53 additions & 2 deletions

File tree

docs/architecture.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
> 状态:当前有效
44
> 最后核对:2026-07-15
5-
> 适用版本:源码 `main` 分支,当前发布基线 0.1.21
5+
> 适用版本:源码 `main` 分支,当前开发基线 0.1.22
66
77
## 结论与适用边界
88

@@ -100,6 +100,7 @@ pending / running / failed ──cancel──> cancelled
100100
- 同一非空 `batchId` 在单个 Gateway 内串行;不同批次和空 `batchId` 可以并行。
101101
- Worker 硬超时、运行中取消和宽限期后的关闭会终止子进程树;Watchdog 在使用数据库记录的旧 PID 前,还会校验进程命令是否匹配配置的 OpenCode 可执行文件。
102102
- Worker 定时更新 `task_runs.heartbeatAt`。Watchdog 发现心跳过期时终止记录的 `childPid`、关闭本次 run,并按同一重试预算恢复任务。
103+
- Gateway 获得单实例锁后、Worker 接单前,会把不存在 active run 的遗留 `running` 任务恢复为 `pending`,覆盖进程在创建 run 前后崩溃留下的孤儿状态。
103104
- 输出只保留最后 64 KiB,避免单任务无限占用 Gateway 内存;发现 JSON 输出中的 `sessionID` 会写入执行记录。
104105

105106
该实现提供 at-least-once 倾向的本地恢复,不保证 exactly-once。任务执行外部副作用时,应自行设计幂等键或可重复执行策略。

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-supertask",
3-
"version": "0.1.21",
3+
"version": "0.1.22",
44
"description": "AI Agent 任务调度系统 — OpenCode 插件 + CLI + Gateway 常驻进程",
55
"type": "module",
66
"main": "dist/plugin/supertask.js",

src/core/services/task.service.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,28 @@ export class TaskService {
242242
return result.length;
243243
}
244244

245+
static async resetOrphanRunningToPending(): Promise<number> {
246+
const result = await db
247+
.update(tasks)
248+
.set({
249+
status: 'pending',
250+
startedAt: null,
251+
finishedAt: null,
252+
})
253+
.where(
254+
and(
255+
eq(tasks.status, 'running'),
256+
sql`NOT EXISTS (
257+
SELECT 1 FROM ${taskRuns}
258+
WHERE ${taskRuns.taskId} = ${tasks.id}
259+
AND ${taskRuns.status} = 'running'
260+
)`,
261+
),
262+
)
263+
.returning();
264+
return result.length;
265+
}
266+
245267
static async cancel(id: number, scope: { cwd?: string } = {}): Promise<Task | null> {
246268
const conditions = [
247269
eq(tasks.id, id),

src/gateway/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ async function main() {
101101
const watchdog = new Watchdog(cfg);
102102
const scheduler = new Scheduler(cfg);
103103

104+
const recoveredOrphans = await TaskService.resetOrphanRunningToPending();
105+
if (recoveredOrphans > 0) {
106+
console.log(JSON.stringify({
107+
ts: new Date().toISOString(),
108+
level: 'warn',
109+
msg: 'reset orphan running tasks to pending',
110+
count: recoveredOrphans,
111+
}));
112+
}
113+
104114
initializeGatewayHealth({
105115
workerPollIntervalMs: cfg.worker.pollIntervalMs,
106116
schedulerEnabled: cfg.scheduler.enabled,

tests/task-service.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, test, expect, beforeEach } from 'bun:test';
22
import { setupTestDb } from './helpers/mock-db';
33
import { TaskService } from '../src/core/services/task.service';
4+
import { TaskRunService } from '../src/core/services/task-run.service';
45

56
describe('TaskService', () => {
67
beforeEach(() => {
@@ -900,5 +901,22 @@ describe('TaskService', () => {
900901
const count = await TaskService.resetRunningToPending([]);
901902
expect(count).toBe(0);
902903
});
904+
905+
test('启动恢复只重置没有 active run 的孤儿 running 任务', async () => {
906+
const orphan = await TaskService.add({ name: '孤儿任务', agent: 'a', prompt: '恢复任务' });
907+
const active = await TaskService.add({ name: '执行中任务', agent: 'a', prompt: '保持执行' });
908+
const closed = await TaskService.add({ name: '已关闭 run 的任务', agent: 'a', prompt: '重新排队' });
909+
await TaskService.start(orphan.id);
910+
await TaskService.start(active.id);
911+
await TaskService.start(closed.id);
912+
await TaskRunService.create({ taskId: active.id });
913+
const closedRun = await TaskRunService.create({ taskId: closed.id });
914+
await TaskRunService.fail(closedRun.id, 'Gateway 在同步任务状态前退出');
915+
916+
expect(await TaskService.resetOrphanRunningToPending()).toBe(2);
917+
expect((await TaskService.getById(orphan.id))?.status).toBe('pending');
918+
expect((await TaskService.getById(closed.id))?.status).toBe('pending');
919+
expect((await TaskService.getById(active.id))?.status).toBe('running');
920+
});
903921
});
904922
});

0 commit comments

Comments
 (0)