From 56826a3c0548a0a56f4278b8e96b743ecd56b877 Mon Sep 17 00:00:00 2001 From: Chaim Date: Tue, 21 Apr 2026 15:19:59 +0000 Subject: [PATCH] feat(kb): requeue-failed endpoint Moves every object under failed// back to inbox// 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) --- api/routes/admin_kb.py | 17 +++++++++++++++++ api/services/kb/s3.py | 19 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/api/routes/admin_kb.py b/api/routes/admin_kb.py index d370746..0063668 100644 --- a/api/routes/admin_kb.py +++ b/api/routes/admin_kb.py @@ -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// back to inbox// 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) diff --git a/api/services/kb/s3.py b/api/services/kb/s3.py index 0b7dc5f..c1f54e2 100644 --- a/api/services/kb/s3.py +++ b/api/services/kb/s3.py @@ -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()