odysseus-dev/odysseus · error · HTTPException

Tidy failed: {e}

Error message

Tidy failed: {e}

What it means

The 500 handler for POST /api/documents/tidy: any exception while scanning user documents, fixing empty titles, deleting broken/empty docs, or committing is caught, rolled back, logged as 'Document tidy failed: {e}', and returned as 'Tidy failed: {e}'. Because the handler formats the exception into the message, the appended text identifies the root cause.

Source

Thrown at routes/document/document_routes.py:962

                .filter(Document.is_active == False)
                .filter((Document.current_content == None) | (Document.current_content == ""))
            )
            inactive_q = _owner_session_filter(inactive_q, user)
            inactive_docs = inactive_q.all()
            for doc in inactive_docs:
                db.delete(doc)
            deleted += len(inactive_docs)

            db.commit()
            return {
                "fixed_titles": fixed_titles,
                "deleted": deleted,
                "message": f"Fixed {fixed_titles} title{'s' if fixed_titles != 1 else ''}, removed {deleted} empty document{'s' if deleted != 1 else ''}",
            }
        except Exception as e:
            db.rollback()
            logger.error(f"Document tidy failed: {e}")
            raise HTTPException(500, f"Tidy failed: {e}")
        finally:
            db.close()

    # ---- POST /api/documents/ai-tidy — AI-powered cleanup of junk/test documents ----
    @router.post("/api/documents/ai-tidy")
    async def ai_tidy_documents(request: Request) -> Dict[str, Any]:
        """Use AI to judge if documents are junk/test/accidental, then delete them.
        Caches verdicts so previously-reviewed docs are skipped."""
        from src.task_endpoint import resolve_task_endpoint
        from src.endpoint_resolver import resolve_endpoint
        from src.llm_core import llm_call_async

        user = get_current_user(request)
        url, model, headers = resolve_task_endpoint(owner=user or None)
        if not url or not model:
            # Fall back to default endpoint
            url, model, headers = resolve_endpoint("default", owner=user or None)
        if not url or not model:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Read the '{e}' suffix in the 500 response and the server log line 'Document tidy failed: ...' for the true exception.
  2. If it is a lock/timeout, ensure only one tidy runs at a time and increase the DB timeout or enable WAL for SQLite.
  3. If a specific row breaks iteration, fix or remove that row (the log usually includes its identifier) and re-run tidy.
  4. Run tidy when user write traffic is low to avoid races with concurrent edits.

Example fix

# before
resp = requests.post(f"{base}/api/documents/tidy")
resp.raise_for_status()

# after
resp = requests.post(f"{base}/api/documents/tidy")
if resp.status_code == 500:
    logging.warning("tidy failed: %s", resp.json().get("detail"))
    # safe to retry later — rollback left data intact
    schedule_retry(delay=60)
Defensive patterns

Strategy: retry

Try / catch

try:
    r = requests.post(f"{base}/api/documents/tidy", timeout=120)
except requests.HTTPError as e:
    if e.response.status_code == 500:
        logging.warning("tidy failed: %s", e.response.json().get("detail"))
        schedule_retry(delay=60)  # rollback kept data intact; retry is safe
    else:
        raise

Prevention

When it happens

Trigger: A broken document row (e.g. NULL session_id confusing the outerjoin/filter chain); DB lock or connection drop during the batch commit; a delete cascading into a table with a restricting foreign key; transiently invalid data (unparseable timestamps) raising during iteration.

Common situations: Running tidy concurrently with active document edits in another session; database behind a connection pool that timed out during the long batch; rows inserted by an older app version violating current model expectations.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/da7ddbb512b1bcbc. Report an issue: GitHub.