feat(retry): NoteRepository — findFailedIds/countFailed/retryAllFailed/setNextRunAt (#2 v0.2.3)

This commit is contained in:
altair823
2026-05-02 03:15:05 +09:00
parent 821db4001d
commit 2e3f0edffd
2 changed files with 123 additions and 0 deletions

View File

@@ -156,6 +156,65 @@ export class NoteRepository {
tx();
}
findFailedIds(): string[] {
const rows = this.db
.prepare(
`SELECT id FROM notes WHERE ai_status='failed' AND deleted_at IS NULL ORDER BY updated_at DESC, id DESC`
)
.all() as Array<{ id: string }>;
return rows.map((r) => r.id);
}
countFailed(): number {
const row = this.db
.prepare(
`SELECT COUNT(*) AS c FROM notes WHERE ai_status='failed' AND deleted_at IS NULL`
)
.get() as { c: number };
return row.c;
}
/**
* 모든 ai_status='failed' (active) 노트를 'pending' 으로 reset 하고 pending_jobs 재투입.
* 단일 transaction. v0.2.3 #2 retryAllFailed.
*
* INSERT OR IGNORE 로 race 안전 (이미 pending_jobs row 존재 시 skip).
*/
retryAllFailed(now: string): { ids: string[] } {
const ids: string[] = [];
const tx = this.db.transaction(() => {
const rows = this.db
.prepare(`SELECT id FROM notes WHERE ai_status='failed' AND deleted_at IS NULL`)
.all() as Array<{ id: string }>;
if (rows.length === 0) return;
const reset = this.db.prepare(
`UPDATE notes SET ai_status='pending', ai_error=NULL, updated_at=? WHERE id=?`
);
const insert = this.db.prepare(
`INSERT OR IGNORE INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 0, ?)`
);
for (const r of rows) {
reset.run(now, r.id);
insert.run(r.id, now);
ids.push(r.id);
}
});
tx();
return { ids };
}
/**
* pending_jobs 의 next_run_at + last_error 만 갱신, attempts 변경 없음.
* v0.2.3 #2 — unreachable/timeout 무한 retry 시 사용 (incrementJobAttempt 와 별도 경로).
*/
setNextRunAt(noteId: string, nextRunAt: string, lastError: string): void {
this.db
.prepare(
`UPDATE pending_jobs SET next_run_at=?, last_error=? WHERE note_id=?`
)
.run(nextRunAt, lastError.slice(0, 500), noteId);
}
updateUserAiFields(
id: string,
fields: { title?: string; summary?: string; tags?: string[] }