Skip to content

Commit 4ee1fa4

Browse files
bugerclaude
andauthored
feat: task queue fixes - stale recovery, smart defaults, search (#477)
* feat: task queue fixes - stale recovery, smart defaults, search, purge - On startup, mark all orphaned 'working' tasks as 'failed' (crash recovery) - Default `visor tasks` shows only active tasks, use --all for history - Add --search <text> to filter tasks by input message - Add --instance <id> to filter by visor instance - Add `visor tasks purge --age 7d` to clean old terminal tasks - Add failStaleTasks() and purgeOldTasks() to TaskStore interface Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: escape SQL LIKE wildcards and add test coverage for task queue Prevents SQL wildcard injection in search filter by escaping %, _, \ characters. Changes purgeOldTasks boundary from < to <= for consistency. Adds 10 new tests covering failStaleTasks, purgeOldTasks, search, claimedBy filter, and claimTask. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use cli-table3 for tasks output, fix cancel prefix matching, fix markdown in code blocks - Replace manual table formatting with cli-table3 for consistent styled output in tasks list, show, and stats subcommands - Add colored state indicators (yellow=working, green=completed, red=failed) - Fix cancel command not finding tasks by prefix (was requiring full UUID) - Extract shared findTaskByPrefix helper used by both cancel and show - Fix markdownToSlack converting bold/links inside fenced code blocks by moving all transformations into the line-by-line code block tracker Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 12d9273 commit 4ee1fa4

7 files changed

Lines changed: 528 additions & 86 deletions

File tree

src/agent-protocol/task-store.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ export interface ListTasksFilter {
110110
contextId?: string;
111111
state?: TaskState[];
112112
workflowId?: string;
113+
search?: string; // text search in request_message
114+
claimedBy?: string; // filter by instance/worker
113115
limit?: number; // default 50, max 200
114116
offset?: number;
115117
}
@@ -160,7 +162,11 @@ export interface TaskStore {
160162
// Queue monitoring (optional — only SqliteTaskStore implements this)
161163
listTasksRaw?(filter: ListTasksFilter): { rows: TaskQueueRow[]; total: number };
162164

163-
// Cleanup
165+
// Cleanup & recovery
166+
/** Mark all 'working' tasks as 'failed' (crash recovery on startup). */
167+
failStaleTasks(reason?: string): number;
168+
/** Delete completed/failed/canceled tasks older than the given age. */
169+
purgeOldTasks(olderThanMs: number): number;
164170
deleteExpiredTasks(): string[];
165171
deleteTask(taskId: string): void;
166172
}
@@ -320,6 +326,16 @@ export class SqliteTaskStore implements TaskStore {
320326
conditions.push('workflow_id = ?');
321327
params.push(filter.workflowId);
322328
}
329+
if (filter.search) {
330+
// Escape SQL LIKE wildcards to prevent wildcard injection
331+
const escaped = filter.search.replace(/[%_\\]/g, '\\$&');
332+
conditions.push("request_message LIKE ? ESCAPE '\\'");
333+
params.push(`%${escaped}%`);
334+
}
335+
if (filter.claimedBy) {
336+
conditions.push('claimed_by = ?');
337+
params.push(filter.claimedBy);
338+
}
323339

324340
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
325341
return { where, params };
@@ -532,6 +548,38 @@ export class SqliteTaskStore implements TaskStore {
532548
// Cleanup
533549
// -------------------------------------------------------------------------
534550

551+
failStaleTasks(reason?: string): number {
552+
const db = this.getDb();
553+
const now = nowISO();
554+
const msg = reason || 'Process terminated while task was running';
555+
const statusMessage = JSON.stringify({
556+
message_id: crypto.randomUUID(),
557+
role: 'agent',
558+
parts: [{ text: msg }],
559+
});
560+
const result = db
561+
.prepare(
562+
`UPDATE agent_tasks
563+
SET state = 'failed', updated_at = ?, status_message = ?
564+
WHERE state = 'working'`
565+
)
566+
.run(now, statusMessage);
567+
return result.changes;
568+
}
569+
570+
purgeOldTasks(olderThanMs: number): number {
571+
const db = this.getDb();
572+
const cutoff = new Date(Date.now() - olderThanMs).toISOString();
573+
const result = db
574+
.prepare(
575+
`DELETE FROM agent_tasks
576+
WHERE state IN ('completed', 'failed', 'canceled', 'rejected')
577+
AND updated_at <= ?`
578+
)
579+
.run(cutoff);
580+
return result.changes;
581+
}
582+
535583
deleteExpiredTasks(): string[] {
536584
const db = this.getDb();
537585
const now = nowISO();

0 commit comments

Comments
 (0)