fix(data-integrity): Replace unconditional commit with flush in LLMTraceRepository.save()
CI / push-validation (pull_request) Successful in 22s
CI / helm (pull_request) Successful in 31s
CI / lint (pull_request) Failing after 59s
CI / build (pull_request) Successful in 3m50s
CI / quality (pull_request) Successful in 4m20s
CI / typecheck (pull_request) Successful in 4m40s
CI / security (pull_request) Successful in 4m46s
CI / coverage (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 5m44s
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Successful in 6m52s
CI / e2e_tests (pull_request) Successful in 6m58s
CI / status-check (pull_request) Failing after 3s
CI / benchmark-publish (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped

This commit is contained in:
2026-04-22 07:45:52 +00:00
parent dade2a99d2
commit 187afc20d5
4 changed files with 34 additions and 12 deletions
+9
View File
@@ -10,6 +10,15 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **Git Worktree Sandbox Apply** (#4454): The `plan apply` command now merges
LLM-generated changes via `git merge` from an isolated worktree branch
instead of flat `shutil.copy2`. Displays spec-aligned Apply Summary
- **LLM Trace Repository UnitOfWork Compliance** (#7505): Fixed data integrity issue
in `LLMTraceRepository.save()` by replacing unconditional `session.commit()` with
`session.flush()` and adding optional session injection. The repository now respects
the UnitOfWork pattern: when called with an external session, it flushes changes
without committing (allowing the caller to control transaction boundaries); when
called without a session, it auto-commits for backward compatibility. This ensures
traces are not silently lost when used within a UnitOfWork transaction, and allows
proper rollback semantics for transactional consistency.
(plan ID, artifacts, insertions/deletions, project, timestamp), Sandbox
Cleanup panel, and `✓ OK Changes applied` footer. Non-git projects fall
back to the original flat file copy.
+4 -4
View File
@@ -269,16 +269,16 @@ Feature: LLM trace observability
Given LANGCHAIN_TRACING_V2 is set to "TRUE"
Then the langsmith_enabled check should return True
Scenario: Repository save trace with no tool calls stores null
Scenario: Repository save trace with no tool calls stores null
Given a SQLAlchemy in-memory repository
And a valid LLM trace
When I save the trace via the repository
Then the raw tool_calls_json in the database should be null
# --- UnitOfWork transaction boundary (Issue #7505) ----------------------
# --- UnitOfWork transaction boundary (Issue #7505) ----------------------
@tdd_issue_7505
Scenario: Repository save uses flush not commit within UnitOfWork
@tdd_issue_7505
Scenario: Repository save uses flush not commit within UnitOfWork
Given a SQLAlchemy in-memory repository
And a valid LLM trace
When I save the trace via the repository within a UnitOfWork transaction
+5 -3
View File
@@ -953,6 +953,8 @@ def step_raw_tool_calls_null(context: Context) -> None:
# ---------------------------------------------------------------------------
@when("I save the trace via the repository within a UnitOfWork transaction")
@when("I save the trace via the repository within a UnitOfWork transaction")
def step_save_within_uow(context: Context) -> None:
"""Save a trace within a UnitOfWork transaction to verify flush behavior."""
@@ -960,9 +962,9 @@ def step_save_within_uow(context: Context) -> None:
session = context.sqla_factory()
context.uow_session = session
# Save the trace using the SQLAlchemy repository
# The repository should use flush(), not commit()
context.sqla_repo.save(context.trace)
# Save the trace using the SQLAlchemy repository, passing the session
# so the repository uses flush() without commit()
context.sqla_repo.save(context.trace, session=session)
# At this point, the trace should be visible within the transaction
# but not yet committed to the database
@@ -46,16 +46,24 @@ class LLMTraceRepository:
return self._sf()
@database_retry
def save(self, trace: LLMTrace) -> None:
def save(self, trace: LLMTrace, session: Session | None = None) -> None:
"""Persist a single ``LLMTrace`` row.
Args:
trace: The trace to persist.
session: Optional SQLAlchemy session. If provided, the caller is
responsible for commit. If None, a session is obtained from
the factory and auto-committed.
Raises:
DatabaseError: On unrecoverable persistence failure.
ValueError: If trace is None.
"""
session = self._session()
if trace is None:
raise ValueError("trace must not be None")
own_session = session is None
s = session if session is not None else self._session()
try:
model = LLMTraceModel(
trace_id=trace.trace_id,
@@ -77,10 +85,13 @@ class LLMTraceRepository:
error=trace.error,
timestamp=trace.timestamp.isoformat(),
)
session.add(model)
session.flush()
s.add(model)
s.flush()
if own_session:
s.commit()
except (SQLAlchemyDatabaseError, OperationalError) as exc:
session.rollback()
if own_session:
s.rollback()
raise DatabaseError(f"Failed to save LLM trace: {exc}") from exc
@database_retry