Describe the bug
A single failing callback in _flushQueue() permanently poisons the entire queue. When a queued task's callback throws an error, the failing row is never dequeued from the SQLite table, and the error is silently swallowed. On the next flush, the same failing row is picked up again, creating an infinite retry loop that blocks all subsequent queued tasks.
To Reproduce
- Create an Agent subclass with a method that can throw an error
- Call
this.queue("myMethod", payload) where myMethod will throw
- Observe that
_flushQueue() runs, the callback throws, and the error is caught by .catch() on line 712 with only a console.error
- Observe that the failing row remains in
cf_agents_queues because this.dequeue(row.id) on line 740 never executes
- Next time
_flushQueue() is triggered (e.g., by another queue() call), the same failing row is fetched first (ORDER BY created_at ASC), throws again, and blocks all items behind it
Expected behavior
- A failing callback should not block the entire queue. The failing item should either be dequeued (dead-lettered) or skipped so that subsequent items can still be processed.
- The error from
_flushQueue() should propagate or be exposed in a meaningful way, rather than being silently swallowed with console.error.
Screenshots
N/A — the bug is in the source code of _flushQueue():
// line 717-745 in dist/src-i_UcyBYf.js
async _flushQueue() {
if (this._flushingQueue) return;
this._flushingQueue = true;
while (true) {
const result = this.sql`
SELECT * FROM cf_agents_queues ORDER BY created_at ASC
`;
if (!result || result.length === 0) break;
for (const row of result || []) {
const callback = this[row.callback];
if (!callback) {
console.error(`callback ${row.callback} not found`);
continue; // <-- missing callback is skipped, but...
}
const { connection, request, email } = agentContext.getStore() || {};
await agentContext.run({ agent: this, connection, request, email }, async () => {
await callback.bind(this)(JSON.parse(row.payload), row); // <-- if this throws...
await this.dequeue(row.id); // <-- ...this never runs, row stays in the table
});
}
}
this._flushingQueue = false;
}
There are two issues:
- No try-catch around the callback invocation — if
callback.bind(this)(...) throws, this.dequeue(row.id) is skipped, the error breaks out of the while(true) loop, and the row permanently stays in cf_agents_queues.
- Error is silently swallowed at the call site —
this._flushQueue().catch((e) => { console.error(...) }) means the caller of queue() has no way to know the queue is stuck.
Version:
agents@0.3.10
Additional context
A minimal fix would be to wrap the callback invocation in a try-catch and dequeue (or mark as failed) the row regardless of whether the callback succeeds:
for (const row of result || []) {
// ...
try {
await agentContext.run({...}, async () => {
await callback.bind(this)(JSON.parse(row.payload), row);
});
} catch (e) {
console.error(`Queue callback ${row.callback} failed for row ${row.id}:`, e);
} finally {
await this.dequeue(row.id);
}
}
Ideally the SDK would also support a dead-letter / retry-with-limit mechanism so users can handle failed queue items gracefully rather than losing them silently. (linking issue #874 since its going to be resolved with this)
Describe the bug
A single failing callback in
_flushQueue()permanently poisons the entire queue. When a queued task's callback throws an error, the failing row is never dequeued from the SQLite table, and the error is silently swallowed. On the next flush, the same failing row is picked up again, creating an infinite retry loop that blocks all subsequent queued tasks.To Reproduce
this.queue("myMethod", payload)wheremyMethodwill throw_flushQueue()runs, the callback throws, and the error is caught by.catch()on line 712 with only aconsole.errorcf_agents_queuesbecausethis.dequeue(row.id)on line 740 never executes_flushQueue()is triggered (e.g., by anotherqueue()call), the same failing row is fetched first (ORDER BY created_at ASC), throws again, and blocks all items behind itExpected behavior
_flushQueue()should propagate or be exposed in a meaningful way, rather than being silently swallowed withconsole.error.Screenshots
N/A — the bug is in the source code of
_flushQueue():There are two issues:
callback.bind(this)(...)throws,this.dequeue(row.id)is skipped, the error breaks out of thewhile(true)loop, and the row permanently stays incf_agents_queues.this._flushQueue().catch((e) => { console.error(...) })means the caller ofqueue()has no way to know the queue is stuck.Version:
agents@0.3.10Additional context
A minimal fix would be to wrap the callback invocation in a try-catch and dequeue (or mark as failed) the row regardless of whether the callback succeeds:
Ideally the SDK would also support a dead-letter / retry-with-limit mechanism so users can handle failed queue items gracefully rather than losing them silently. (linking issue #874 since its going to be resolved with this)