Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(ui): make chat policy-block message replacement atomic#1556

Merged
kovtcharov-amd merged 3 commits into
mainfrom
autofix/issue-987
Jun 11, 2026
Merged

fix(ui): make chat policy-block message replacement atomic#1556
kovtcharov-amd merged 3 commits into
mainfrom
autofix/issue-987

Conversation

@kovtcharov-amd

Copy link
Copy Markdown
Collaborator

The chat policy-block path in _chat_helpers.py replaced a stored message with a separate delete_message then add_message. A crash or exception between the DELETE and the INSERT left the session with no message at all — silent data loss (#987). Now both call sites use a single ChatDatabase.upsert_message, which runs the DELETE and INSERT inside one transaction that rolls back as a unit, so the original message always survives a failed replacement.

Closes #987.

Test plan

  • python -m pytest tests/unit/chat/ui/test_database.py -q — passes, including new upsert insert/replace, wrong-session-safety, metadata round-trip, and crash-rollback cases
  • python -m black --check src/gaia/ui/database.py src/gaia/ui/_chat_helpers.py tests/unit/chat/ui/test_database.py
  • Trigger a policy-blocked tool in the Agent UI; confirm the block message persists and is correctly replaced by the final response on the same turn

Replace the non-atomic delete_message + add_message pattern in
_chat_helpers.py with a single transactional ChatDatabase.upsert_message,
so a crash or exception between the DELETE and INSERT can no longer leave
a session without its policy-block / assistant message.

Closes #987
@github-actions github-actions Bot added the tests Test changes label Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Clean, well-scoped fix that closes a real data-loss bug — approve. The chat policy-block path previously replaced a stored message with a separate delete_message + add_message; a crash or exception between the two left the session with no message at all. This PR folds both into a single ChatDatabase.upsert_message that runs the DELETE and INSERT inside one transaction, so a failed replacement rolls back as a unit and the original message always survives. The change is tightly focused (one new method, two call-site swaps, five tests) and the test coverage is genuinely good.

I verified the core behavior by exercising upsert_message in isolation: insert-when-None, replace-existing (new id minted), wrong-session safety (an id from another session is never deleted), and the atomic-rollback path under a simulated INSERT crash all behave as the tests assert. The single most important thing: the fix correctly resolves the pre-existing "make this transactional before a multi-writer backend" TODO it replaces — no follow-up debt left behind.

Issues Found

🟢 Minor — add_message/upsert_message duplication (database.py:461, 505)
upsert_message is add_message plus an optional leading DELETE — the INSERT block, JSON serialization, and session-timestamp touch are copied verbatim. Not blocking (the duplication is small and each method reads clearly on its own), but add_message could delegate to keep them in sync if the INSERT columns ever change:

        """Add a message to a session. Returns message ID."""
        return self.upsert_message(
            session_id,
            None,
            role,
            content,
            rag_sources=rag_sources,
            agent_steps=agent_steps,
            tokens_prompt=tokens_prompt,
            tokens_completion=tokens_completion,
            inference_stats=inference_stats,
        )

(Apply only if you want the consolidation — keeping them separate is a defensible call.)

Strengths

  • The fix is the right shape. A single transaction is exactly how to make delete-then-insert atomic, and it reuses the existing _transaction() context manager (commit-on-success, rollback-and-reraise) rather than hand-rolling transaction handling.
  • Excellent test coverage for a bug fix. test_upsert_message_atomic_on_insert_failure simulates a crash between DELETE and INSERT with a flaky connection and asserts the original row survives — that's the actual regression being fixed, tested directly. The wrong-session-safety and metadata round-trip cases are also well chosen.
  • No silent fallbacks. The failure path re-raises; callers reassign the returned id, so the "new id is minted" semantics documented in the docstring are honored at both call sites.

Verdict

Approve. No blocking issues — the one nit above is optional. Two notes for the merge, not blockers: I couldn't run pytest/black in this environment (heavy deps unavailable), so please confirm the test-plan checkboxes locally before merge; the diff itself reads black-clean. No documentation change is warranted — upsert_message is an internal DB helper, not a public SDK/CLI surface.

@kovtcharov-amd kovtcharov-amd enabled auto-merge June 11, 2026 00:33
@kovtcharov-amd kovtcharov-amd added this pull request to the merge queue Jun 11, 2026
Merged via the queue into main with commit e9c5670 Jun 11, 2026
33 checks passed
@kovtcharov-amd kovtcharov-amd deleted the autofix/issue-987 branch June 11, 2026 00:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[policy-alert] Replace non-atomic delete+add in ChatDatabase with transactional upsert

2 participants