Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions packages/core/src/queues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { MessageJobData, ResponseJobData } from './types';

const QUEUE_DB_PATH = path.join(TINYCLAW_HOME, 'tinyclaw.db');
const MAX_RETRIES = 5;
const MAX_MANUAL_RETRIES = 3;

let db: Database.Database | null = null;
export const queueEvents = new EventEmitter();
Expand All @@ -29,7 +30,9 @@ export function initQueueDb(): void {
message TEXT NOT NULL, agent TEXT, files TEXT,
conversation_id TEXT, from_agent TEXT,
status TEXT NOT NULL DEFAULT 'pending',
retry_count INTEGER NOT NULL DEFAULT 0, last_error TEXT,
retry_count INTEGER NOT NULL DEFAULT 0,
manual_retry_count INTEGER NOT NULL DEFAULT 0,
last_error TEXT,
created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS responses (
Expand Down Expand Up @@ -67,6 +70,12 @@ export function initQueueDb(): void {
if (!cols.some(c => c.name === 'metadata')) {
db.exec('ALTER TABLE responses ADD COLUMN metadata TEXT');
}

// Migrate: add manual_retry_count to messages if missing (for existing databases)
const msgCols = db.prepare("PRAGMA table_info(messages)").all() as { name: string }[];
if (!msgCols.some(c => c.name === 'manual_retry_count')) {
db.exec('ALTER TABLE messages ADD COLUMN manual_retry_count INTEGER NOT NULL DEFAULT 0');
}
}

function getDb(): Database.Database {
Expand Down Expand Up @@ -172,8 +181,15 @@ export function getDeadMessages(): any[] {
return getDb().prepare(`SELECT * FROM messages WHERE status='dead' ORDER BY updated_at DESC`).all();
}

export function retryDeadMessage(rowId: number): boolean {
return getDb().prepare(`UPDATE messages SET status='pending',retry_count=0,updated_at=? WHERE id=? AND status='dead'`).run(Date.now(), rowId).changes > 0;
export function retryDeadMessage(rowId: number): boolean | 'cap_exceeded' {
const msg = getDb().prepare('SELECT manual_retry_count FROM messages WHERE id=? AND status=\'dead\'').get(rowId) as { manual_retry_count: number } | undefined;
if (!msg) return false;
if (msg.manual_retry_count >= MAX_MANUAL_RETRIES) return 'cap_exceeded';
// Set retry_count to MAX_RETRIES - 1 so the message gets exactly one more
// automatic attempt before going dead again, rather than a full reset to 0.
return getDb().prepare(
`UPDATE messages SET status='pending', retry_count=?, manual_retry_count=manual_retry_count+1, updated_at=? WHERE id=? AND status='dead'`
).run(MAX_RETRIES - 1, Date.now(), rowId).changes > 0;
}

export function deleteDeadMessage(rowId: number): boolean {
Expand Down
7 changes: 5 additions & 2 deletions packages/server/src/routes/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,18 @@ export function createQueueRoutes(conversations: Map<string, Conversation>) {
},
failedReason: m.last_error,
attemptsMade: m.retry_count,
manualRetriesUsed: m.manual_retry_count,
manualRetriesRemaining: Math.max(0, 3 - m.manual_retry_count),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number instead of shared constant

manualRetriesRemaining is computed with the literal 3 rather than a shared constant. Because MAX_MANUAL_RETRIES is defined (but not exported) in queues.ts, this file has no choice but to hardcode the value. If the cap is ever adjusted in queues.ts, the API response will silently report a stale limit, making manualRetriesRemaining wrong for any client that relies on it.

The same problem appears on line 124 where the error message uses ${3} — syntactically a template-literal interpolation that always evaluates to the literal 3.

Fix: Export MAX_MANUAL_RETRIES from queues.ts and import it here:

In packages/core/src/queues.ts:

export const MAX_MANUAL_RETRIES = 3;

In packages/server/src/routes/queue.ts:

import { ..., MAX_MANUAL_RETRIES } from '@tinyclaw/core';

Then replace both occurrences:

Suggested change
manualRetriesRemaining: Math.max(0, 3 - m.manual_retry_count),
manualRetriesRemaining: Math.max(0, MAX_MANUAL_RETRIES - m.manual_retry_count),

timestamp: m.created_at,
})));
});

// POST /api/queue/dead/:id/retry
app.post('/api/queue/dead/:id/retry', (c) => {
const id = parseInt(c.req.param('id'), 10);
const ok = retryDeadMessage(id);
if (!ok) return c.json({ error: 'dead message not found' }, 404);
const result = retryDeadMessage(id);
if (result === false) return c.json({ error: 'dead message not found' }, 404);
if (result === 'cap_exceeded') return c.json({ error: `Manual retry limit (${3}) reached for this message. Delete it if you want to discard it.` }, 429);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded literal ${3} in error message

The template literal ${3} embeds a bare numeric literal inside interpolation braces. It evaluates correctly today, but it will diverge from the real cap the moment MAX_MANUAL_RETRIES is changed in queues.ts. Once MAX_MANUAL_RETRIES is exported (see comment on line 114), use it here as well:

Suggested change
if (result === 'cap_exceeded') return c.json({ error: `Manual retry limit (${3}) reached for this message. Delete it if you want to discard it.` }, 429);
if (result === 'cap_exceeded') return c.json({ error: `Manual retry limit (${MAX_MANUAL_RETRIES}) reached for this message. Delete it if you want to discard it.` }, 429);

log('INFO', `[API] Dead message ${id} retried`);
return c.json({ ok: true });
});
Expand Down