BUG-HUNT: [error-handling] agents project create --invariant silently loses invariant data if DB write fails — project is created but invariants are not persisted #6466

Open
opened 2026-04-09 21:07:07 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: [error-handling] — _store_project_extras Has No Exception Handling — Invariant Data Lost Silently on DB Failure

Severity Assessment

  • Impact: When agents project create is run with --invariant or --invariant-actor flags, the project is created successfully (committed), but the invariant data is written via a separate raw SQLAlchemy session in _store_project_extras() which has no exception handling. If this second write fails (e.g., DB file locked, connection error, session conflict), the exception propagates as a raw Python traceback — but the project was already committed. The user sees an error but the project exists without invariants. Attempting to re-run the create will fail with "project already exists".
  • Likelihood: Low in normal operation, higher in concurrent environments or on file-locked SQLite databases.
  • Priority: Medium

Location

  • File: src/cleveragents/cli/commands/project.py
  • Function/Class: _store_project_extras() and create() command
  • Lines: _store_project_extras() at lines 95–136; call site at lines 601–608

Description

_store_project_extras() opens a new SQLAlchemy engine + session (bypassing the DI container's session factory) to execute raw SQL UPDATE statements. The function has a try/finally that only ensures session.close() is called, but no exception handling around the SQL operations themselves:

def _store_project_extras(...) -> None:
    ...
    session = sessionmaker(bind=engine, expire_on_commit=False)()
    try:
        ...
        if updates:
            sql = text(f"UPDATE ns_projects SET {', '.join(updates)} WHERE ...")
            session.execute(sql, params)   # ← can raise SQLAlchemyError
            session.commit()               # ← can raise SQLAlchemyError
    finally:
        session.close()   # ← only guarantees cleanup, NOT error handling

If session.execute() or session.commit() raises a SQLAlchemyError (e.g., OperationalError: database is locked, IntegrityError), the exception propagates uncaught from _store_project_extras() into the create() command.

The create() command calls _store_project_extras() outside the existing try/except DatabaseError block that wraps repo.create():

try:
    repo.create(project)           # ← committed to DB
except DatabaseError as exc:
    err_console.print(...)
    raise typer.Exit(1) from exc

# OUTSIDE the try block — no error handling:
if invariant or invariant_actor:
    _store_project_extras(         # ← can raise SQLAlchemyError
        project.namespaced_name,
        invariant_texts=invariant,
        inv_actor=invariant_actor,
    )

This creates a partial state: the project exists in the DB but has no invariants. The user sees a raw traceback and doesn't know if their project was created. Re-running with the same name will fail with a "project already exists" error.

Expected Behavior

If _store_project_extras() fails, the CLI should either:

  1. Roll back the project creation (transactional atomicity), OR
  2. Report a clear warning that the project was created but invariants could not be saved, and suggest agents project update to retry setting them.

No raw traceback should be exposed.

Actual Behavior

SQLAlchemy exception propagates as a raw traceback. The project exists without invariants. The user doesn't know whether to re-run or use project update.

Evidence

# src/cleveragents/cli/commands/project.py lines 95–135
def _store_project_extras(...) -> None:
    ...
    session = sessionmaker(bind=engine, expire_on_commit=False)()
    try:
        ...
        if updates:
            sql = text(...)
            session.execute(sql, params)   # ← can raise
            session.commit()               # ← can raise
    finally:
        session.close()    # ← only cleanup, not error handling
    # No except block!
# src/cleveragents/cli/commands/project.py lines 594–608 — call site
    try:
        repo.create(project)   # project committed here
    except DatabaseError as exc:
        ...

    if invariant or invariant_actor:
        _store_project_extras(...)    # ← called OUTSIDE try block!

Suggested Fix

Wrap the _store_project_extras() call in a try/except that provides a user-friendly warning instead of exposing the traceback:

    if invariant or invariant_actor:
        try:
            _store_project_extras(
                project.namespaced_name,
                invariant_texts=invariant,
                inv_actor=invariant_actor,
            )
        except Exception as exc:
            err_console.print(
                f"[yellow]Warning: Project '{project.namespaced_name}' was created "
                f"but invariants could not be saved: {exc}[/yellow]"
            )
            err_console.print(
                "[yellow]Use 'agents project update' to set invariants.[/yellow]"
            )

Long-term: consolidate invariant persistence into the main repo.create() transaction to ensure atomicity.

Category

error-handling

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_, and @tdd_expected_fail to prove the bug exists before fixing it.


Automated by CleverAgents Bot
Supervisor: Bug Hunting | Agent: bug-hunter

## Bug Report: [error-handling] — `_store_project_extras` Has No Exception Handling — Invariant Data Lost Silently on DB Failure ### Severity Assessment - **Impact**: When `agents project create` is run with `--invariant` or `--invariant-actor` flags, the project is created successfully (committed), but the invariant data is written via a **separate raw SQLAlchemy session** in `_store_project_extras()` which has no exception handling. If this second write fails (e.g., DB file locked, connection error, session conflict), the exception propagates as a raw Python traceback — but the project was already committed. The user sees an error but the project exists without invariants. Attempting to re-run the create will fail with "project already exists". - **Likelihood**: Low in normal operation, higher in concurrent environments or on file-locked SQLite databases. - **Priority**: Medium ### Location - **File**: `src/cleveragents/cli/commands/project.py` - **Function/Class**: `_store_project_extras()` and `create()` command - **Lines**: `_store_project_extras()` at lines 95–136; call site at lines 601–608 ### Description `_store_project_extras()` opens a **new** SQLAlchemy engine + session (bypassing the DI container's session factory) to execute raw SQL `UPDATE` statements. The function has a `try/finally` that only ensures `session.close()` is called, but **no exception handling** around the SQL operations themselves: ```python def _store_project_extras(...) -> None: ... session = sessionmaker(bind=engine, expire_on_commit=False)() try: ... if updates: sql = text(f"UPDATE ns_projects SET {', '.join(updates)} WHERE ...") session.execute(sql, params) # ← can raise SQLAlchemyError session.commit() # ← can raise SQLAlchemyError finally: session.close() # ← only guarantees cleanup, NOT error handling ``` If `session.execute()` or `session.commit()` raises a `SQLAlchemyError` (e.g., `OperationalError: database is locked`, `IntegrityError`), the exception propagates uncaught from `_store_project_extras()` into the `create()` command. The `create()` command calls `_store_project_extras()` **outside** the existing `try/except DatabaseError` block that wraps `repo.create()`: ```python try: repo.create(project) # ← committed to DB except DatabaseError as exc: err_console.print(...) raise typer.Exit(1) from exc # OUTSIDE the try block — no error handling: if invariant or invariant_actor: _store_project_extras( # ← can raise SQLAlchemyError project.namespaced_name, invariant_texts=invariant, inv_actor=invariant_actor, ) ``` This creates a **partial state**: the project exists in the DB but has no invariants. The user sees a raw traceback and doesn't know if their project was created. Re-running with the same name will fail with a "project already exists" error. ### Expected Behavior If `_store_project_extras()` fails, the CLI should either: 1. Roll back the project creation (transactional atomicity), OR 2. Report a clear warning that the project was created but invariants could not be saved, and suggest `agents project update` to retry setting them. No raw traceback should be exposed. ### Actual Behavior SQLAlchemy exception propagates as a raw traceback. The project exists without invariants. The user doesn't know whether to re-run or use `project update`. ### Evidence ```python # src/cleveragents/cli/commands/project.py lines 95–135 def _store_project_extras(...) -> None: ... session = sessionmaker(bind=engine, expire_on_commit=False)() try: ... if updates: sql = text(...) session.execute(sql, params) # ← can raise session.commit() # ← can raise finally: session.close() # ← only cleanup, not error handling # No except block! ``` ```python # src/cleveragents/cli/commands/project.py lines 594–608 — call site try: repo.create(project) # project committed here except DatabaseError as exc: ... if invariant or invariant_actor: _store_project_extras(...) # ← called OUTSIDE try block! ``` ### Suggested Fix Wrap the `_store_project_extras()` call in a try/except that provides a user-friendly warning instead of exposing the traceback: ```python if invariant or invariant_actor: try: _store_project_extras( project.namespaced_name, invariant_texts=invariant, inv_actor=invariant_actor, ) except Exception as exc: err_console.print( f"[yellow]Warning: Project '{project.namespaced_name}' was created " f"but invariants could not be saved: {exc}[/yellow]" ) err_console.print( "[yellow]Use 'agents project update' to set invariants.[/yellow]" ) ``` Long-term: consolidate invariant persistence into the main `repo.create()` transaction to ensure atomicity. ### Category error-handling ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD. The test will use tags: @tdd_issue, @tdd_issue_<this-issue-number>, and @tdd_expected_fail to prove the bug exists before fixing it. --- **Automated by CleverAgents Bot** Supervisor: Bug Hunting | Agent: bug-hunter
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6466
No description provided.