feat(kb): requeue-failed endpoint

Moves every object under failed/<kind>/ back to inbox/<kind>/ for retry
after a transient upstream failure (e.g. Voyage 401 before a key rotation).

Refs Task Master #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 15:19:59 +00:00
parent 5ddacbe573
commit 56826a3c05
2 changed files with 36 additions and 0 deletions
+17
View File
@@ -84,6 +84,23 @@ def _derive_metadata(filename: str, kind: str) -> dict:
return {"title": stem, "identifier": None}
@router.post("/requeue-failed")
async def requeue_failed(request: Request):
"""Move everything under failed/<kind>/ back to inbox/<kind>/ for retry."""
_verify_admin(request)
items = kb_s3.list_failed()
moved = []
for item in items:
src = item["key"]
dst = f"inbox/{item['kind']}/{item['filename']}"
try:
kb_s3.move(src, dst)
moved.append({"from": src, "to": dst})
except Exception as e:
moved.append({"from": src, "error": str(e)})
return {"requeued": len([m for m in moved if "to" in m]), "items": moved}
@router.post("/scan-inbox")
async def scan_inbox(request: Request):
_verify_admin(request)
+19
View File
@@ -95,6 +95,25 @@ def mark_processed(src_key: str, filename: str, kind: str) -> str:
return dst
def list_failed() -> list[dict]:
s3 = _client()
bucket = _bucket()
items: list[dict] = []
for kind in _KINDS:
prefix = f"failed/{kind}/"
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get("Contents", []) or []:
key = obj["Key"]
if key.endswith("/"):
continue
filename = key[len(prefix):]
if not filename:
continue
items.append({"key": key, "kind": kind, "filename": filename, "size": obj["Size"]})
return items
def mark_failed(src_key: str, filename: str, kind: str, reason: str) -> str:
s3 = _client()
bucket = _bucket()