fix(agent-jobs): recovery cron skips already-terminal jobs
This commit is contained in:
@@ -1218,6 +1218,7 @@ export const recoverStaleJobs = internalMutation({
|
|||||||
.withIndex('by_status', (q) => q.eq('status', status))
|
.withIndex('by_status', (q) => q.eq('status', status))
|
||||||
.collect();
|
.collect();
|
||||||
for (const job of jobs) {
|
for (const job of jobs) {
|
||||||
|
if (isTerminalJob(job)) continue;
|
||||||
const last = job.lastHeartbeatAt ?? job.claimedAt ?? job.createdAt;
|
const last = job.lastHeartbeatAt ?? job.claimedAt ?? job.createdAt;
|
||||||
if (last > threshold) continue;
|
if (last > threshold) continue;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const seedJob = async (
|
|||||||
status?: ActiveStatus;
|
status?: ActiveStatus;
|
||||||
claimedAt?: number;
|
claimedAt?: number;
|
||||||
lastHeartbeatAt?: number;
|
lastHeartbeatAt?: number;
|
||||||
|
workspaceStatus?: string;
|
||||||
|
completedAt?: number;
|
||||||
|
threadStatus?: 'running' | 'resolved' | 'ignored' | 'waiting_for_user';
|
||||||
|
threadResolvedAt?: number;
|
||||||
},
|
},
|
||||||
) =>
|
) =>
|
||||||
await t.mutation(async (ctx) => {
|
await t.mutation(async (ctx) => {
|
||||||
@@ -55,9 +59,12 @@ const seedJob = async (
|
|||||||
spoonId,
|
spoonId,
|
||||||
title: 'Recover the worker job',
|
title: 'Recover the worker job',
|
||||||
source: 'user_request',
|
source: 'user_request',
|
||||||
status: 'running',
|
status: args.threadStatus ?? 'running',
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
relatedAgentRequestId: requestId,
|
relatedAgentRequestId: requestId,
|
||||||
|
...(args.threadResolvedAt === undefined
|
||||||
|
? {}
|
||||||
|
: { resolvedAt: args.threadResolvedAt }),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
@@ -69,7 +76,7 @@ const seedJob = async (
|
|||||||
status: args.status ?? 'running',
|
status: args.status ?? 'running',
|
||||||
prompt: 'Recover the worker job',
|
prompt: 'Recover the worker job',
|
||||||
runtime: 'opencode',
|
runtime: 'opencode',
|
||||||
workspaceStatus: 'active',
|
workspaceStatus: args.workspaceStatus ?? 'active',
|
||||||
baseBranch: 'main',
|
baseBranch: 'main',
|
||||||
workBranch: 'spoon/recover-worker-job',
|
workBranch: 'spoon/recover-worker-job',
|
||||||
forkOwner: 'team',
|
forkOwner: 'team',
|
||||||
@@ -85,6 +92,9 @@ const seedJob = async (
|
|||||||
...(args.lastHeartbeatAt === undefined
|
...(args.lastHeartbeatAt === undefined
|
||||||
? {}
|
? {}
|
||||||
: { lastHeartbeatAt: args.lastHeartbeatAt }),
|
: { lastHeartbeatAt: args.lastHeartbeatAt }),
|
||||||
|
...(args.completedAt === undefined
|
||||||
|
? {}
|
||||||
|
: { completedAt: args.completedAt }),
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
@@ -126,6 +136,31 @@ describe('recoverStaleJobs', () => {
|
|||||||
expect(state.thread?.resolvedAt).toBeTypeOf('number');
|
expect(state.thread?.resolvedAt).toBeTypeOf('number');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('leaves a terminal-workspace maintenance job untouched', async () => {
|
||||||
|
const t = convexTest(schema, modules);
|
||||||
|
const resolvedAt = Date.now() - 5 * 60 * 1000;
|
||||||
|
const ids = await seedJob(t, {
|
||||||
|
status: 'running',
|
||||||
|
workspaceStatus: 'stopped',
|
||||||
|
completedAt: resolvedAt,
|
||||||
|
threadStatus: 'resolved',
|
||||||
|
threadResolvedAt: resolvedAt,
|
||||||
|
// stale heartbeat: the worker stopped its heartbeat after applying the
|
||||||
|
// maintenance decision, so a naive cron would time this job out
|
||||||
|
claimedAt: Date.now() - 10 * 60 * 1000,
|
||||||
|
lastHeartbeatAt: Date.now() - 10 * 60 * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await t.mutation(internal.agentJobs.recoverStaleJobs, {});
|
||||||
|
const state = await readState(t, ids);
|
||||||
|
|
||||||
|
expect(result).toEqual({ recovered: 0 });
|
||||||
|
expect(state.job?.status).toBe('running');
|
||||||
|
expect(state.job?.workspaceStatus).toBe('stopped');
|
||||||
|
expect(state.thread?.status).toBe('resolved');
|
||||||
|
expect(state.thread?.resolvedAt).toBe(resolvedAt);
|
||||||
|
});
|
||||||
|
|
||||||
test('leaves a running job with a fresh heartbeat untouched', async () => {
|
test('leaves a running job with a fresh heartbeat untouched', async () => {
|
||||||
const t = convexTest(schema, modules);
|
const t = convexTest(schema, modules);
|
||||||
const ids = await seedJob(t, { lastHeartbeatAt: Date.now() });
|
const ids = await seedJob(t, { lastHeartbeatAt: Date.now() });
|
||||||
|
|||||||
Reference in New Issue
Block a user