Skip to content

fix(conversations): atomic item mutations with transactional message cache sync - #570

Merged
leseb merged 15 commits into
praxis-proxy:mainfrom
eoinfennessy:358-refactor-sync_conversation_messages
Sep 3, 2026
Merged

fix(conversations): atomic item mutations with transactional message cache sync#570
leseb merged 15 commits into
praxis-proxy:mainfrom
eoinfennessy:358-refactor-sync_conversation_messages

Conversation

@eoinfennessy

@eoinfennessy eoinfennessy commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds create_items_and_sync_messages and delete_item_and_sync_messages to the ConversationItemStore trait, combining position assignment, item mutation, and message cache rebuild into single database transactions. This eliminate races under concurrent appends and stale cache overwrites
  • Removes refresh_message_cache and sync_conversation_messages, which constructed a ConversationRecord with bogus field values just to thread tenant_id and conversation_id through to the sync logic
  • Adds a unique index on (tenant_id, conversation_id, position) as defense-in-depth against position collisions
  • Uses BEGIN IMMEDIATE (SQLite) and SELECT ... FOR UPDATE (PostgreSQL) to serialize concurrent writes

Test plan

  • 5 new SQLite tests covering position assignment, multi-batch appends, deletion with cache sync, empty batches, and nonexistent items
  • 2 new PostgreSQL tests covering position assignment and deletion with cache sync
  • Existing duplicate_positions test replaced with duplicate_position_rejected_by_unique_constraint
  • Full test suite passes (2005 tests)

Closes #358

@eoinfennessy
eoinfennessy requested review from a team and leseb July 27, 2026 09:17
Comment thread apis/src/store/tests.rs
Comment thread apis/src/openai/conversations/handlers.rs Outdated
Comment thread apis/src/store/sqlite.rs Outdated
@praxis-bot-app

Copy link
Copy Markdown

PR too large: 815 lines added (limit: 750, excludes Cargo files, tests, docs, examples, and benchmarks). Please split into smaller PRs. Add skip/pr-conventions label to override.

Comment thread apis/src/openai/conversations/filter.rs Outdated

@franciscojavierarceo franciscojavierarceo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for two merge-blocking correctness issues and one SQLite performance regression. The existing mixed-batch API thread at filter.rs:581 also remains valid and unresolved.\n\nI reproduced the legacy-duplicate initialization failure on SQLite. Targeted transactional tests otherwise passed locally, while the PR is currently also failing nightly rustfmt and the coverage threshold.

Comment thread apis/src/store/schemas.rs
Comment thread apis/src/store/sqlite.rs Outdated
Comment thread apis/src/store/sqlite.rs Outdated

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
Large 1
Medium 1

Solid architectural improvement. The transactional approach correctly eliminates the read-compute-write race on max_item_position that caused position collisions under concurrent appends. SELECT ... FOR UPDATE (PostgreSQL) and BEGIN IMMEDIATE (SQLite) are the right serialization strategies for each backend. The removal of the bogus ConversationRecord construction in refresh_message_cache / sync_conversation_messages is a clean simplification. Test coverage for the new methods is thorough, including the concurrent SQLite test that proves the locking works.

Comment thread apis/src/store/schemas.rs
Comment thread apis/src/openai/conversations/handlers.rs
@eoinfennessy
eoinfennessy enabled auto-merge July 28, 2026 21:57
@eoinfennessy eoinfennessy added the praxis-known-issue Allow `test-praxis-main` to continue on error; Use for known / transient compatibility issues label Jul 29, 2026

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
Medium 1

The transactional approach is well-designed and the serialization strategies are correct for each backend (SELECT ... FOR UPDATE for PostgreSQL, BEGIN IMMEDIATE for SQLite). Clean removal of the ConversationRecord construction hack in refresh_message_cache / sync_conversation_messages. The concurrent SQLite test is a good addition. Handler refactoring correctly simplifies the existence-check and mutation calls.

Comment thread apis/src/store/postgres.rs

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
(none new) 0

Strong improvement. The transactional approach correctly eliminates the TOCTOU race on max_item_position that caused position collisions under concurrent appends. Serialization strategies are appropriate for each backend: SELECT ... FOR UPDATE for PostgreSQL and BEGIN IMMEDIATE for SQLite. The *_rebuild_messages helpers properly detect concurrent conversation deletion via rows_affected() == 0 and propagate the error, rolling back the entire transaction so no orphaned items persist.

Handler refactoring is clean. The move from two separate operations (create_conversation_items + sync_conversation_messages) to a single create_items_and_sync_messages call removes the bogus ConversationRecord construction that existed only to thread tenant_id/conversation_id through to the sync logic. The handle_create_items and handle_delete_item existence checks are simplified to discard the record since it is no longer needed for the sync call.

Test coverage is thorough: position assignment, multi-batch continuation, empty batch no-op, missing conversation rollback (verifying items are cleaned up), deletion with cache sync, nonexistent item returns false, and a concurrent write test verifying distinct positions under contention. The PostgreSQL tests mirror the SQLite ones for the core paths. The rollback verification tests (*_missing_conversation_errors) are well-designed.

The unique index on (tenant_id, conversation_id, position) is good defense-in-depth. Previous review feedback about migration risk on existing data with duplicate positions remains the main outstanding consideration for the upgrade path.

No new findings at Medium or above.

@eoinfennessy

Copy link
Copy Markdown
Member Author

Hi @aslakknutsen @franciscojavierarceo, I have addressed your requested changes. PTAL when you get a chance

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
Medium 1

The transactional redesign is solid and the test coverage for the new methods is thorough (position assignment, multi-batch continuation, empty batch no-op, missing conversation rollback, deletion with cache sync, concurrent writes). The prior review findings remain the main outstanding items.

One new finding: the coverage exclusion added for store/postgres.rs is overly broad and suppresses coverage measurement for all existing and future PostgreSQL store code.

Comment thread Makefile

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 6 review -- 1 new finding (Medium).

The transactional approach is sound and the test coverage for the new methods is solid. One scope-consistency concern in the INSERT path.

Comment thread apis/src/store/postgres.rs Outdated

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
(none new) 0

Round 7 review -- no new findings at Medium or above.

The iterative work since the last review has been good: commit da27ad4 restored the existence guard in *_rebuild_messages via rows_affected() checks, and f747003 eliminated the empty-messages fallback in handle_create_conversation. The transactional design is sound, serialization strategies are correct per backend, and the test suite is thorough (position assignment, multi-batch continuation, concurrent writes, missing-conversation rollback for both create and delete paths).

The following prior findings remain the outstanding items for consideration before merge:

  1. [Large] Unique index migration risk on existing data with duplicate positions (schemas.rs)
  2. [Medium] PostgreSQL lock query result discarded without checking row existence -- pg_rebuild_messages catches it downstream, but early-out on locked.is_none() would avoid inserting items that get rolled back (postgres.rs)
  3. [Medium] INSERT binds item.tenant_id / item.conversation_id instead of the function parameters in both PostgreSQL and SQLite -- scope fields can silently diverge (postgres.rs, sqlite.rs)
  4. [Medium] Coverage exclusion suppresses the entire store/postgres.rs file, not just the new transactional methods (Makefile)

@leseb

leseb commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

@aslakknutsen @franciscojavierarceo PTAL to unblock this, thanks

@eoinfennessy

Copy link
Copy Markdown
Member Author

@aslakknutsen @franciscojavierarceo can you please review again to unblock this?

test-praxis-main now fails, but I don't think this is related to my changes.

@eoinfennessy
eoinfennessy added this pull request to the merge queue Aug 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Aug 27, 2026
@shaneutt
shaneutt self-requested a review as a code owner August 28, 2026 17:11
@leseb

leseb commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

@eoinfennessy please rebase

eoinfennessy and others added 12 commits September 3, 2026 13:23
…cache sync

Move position assignment, item insert/delete, and message cache rebuild
into single database transactions to eliminate races under concurrent
appends and stale cache overwrites.

This replaces refresh_message_cache and sync_conversation_messages,
which constructed a ConversationRecord with bogus field values just to
thread tenant_id and conversation_id through to the sync logic.

Closes praxis-proxy#358

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
Spawns two tasks that each append one item to the same conversation
concurrently, then asserts distinct positions and a coherent two-item
message cache. Consolidates make_file_store helpers to accept an
optional items_table parameter.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
…ssages

Add debug_assert! to SQLite and PostgreSQL implementations to validate
that all items share the same tenant_id and conversation_id, preventing
silent position misassignment from a future internal caller.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
Replace manual BEGIN IMMEDIATE / COMMIT / ROLLBACK raw queries with
pool.begin_with("BEGIN IMMEDIATE") to get SQLx's RAII rollback-on-drop
behavior. If the future is cancelled after BEGIN, or an error propagates
before COMMIT, the Transaction guard now automatically rolls back when
dropped — preventing a pooled connection from being returned with an
open write lock.

Aligns helper function signatures with the Postgres side by accepting
&mut Transaction instead of &mut PoolConnection.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
Signed-off-by: Eoin Fennessy <efenness@redhat.com>
…te_items_and_sync_messages

The method already assumed all items shared the same tenant_id and
conversation_id (extracting them from items.first() with a debug_assert).
Making them explicit parameters aligns the signature with
delete_item_and_sync_messages and removes the need for the runtime
assertion.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
The existing `conversation_item_methods_fail_without_items_table` test
verified six `ConversationItemStore` methods but omitted the two new
transactional methods added by this branch. Add assertions for
`create_items_and_sync_messages` and `delete_item_and_sync_messages`
to close the gap.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
The PostgreSQL store implementation mirrors the SQLite store but its
27 tests are all `#[ignore]` because they require a running database
that CI does not provide. Including postgres.rs in the coverage report
counts ~300 lines of untestable code against the threshold, masking
the actual test quality of the exercised SQLite path.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
…xis-proxy#578)

The CallTool error variant used a raw String URL and included the
third-party source error in its Display output, both of which could
leak credentials stored in URL query strings. Switch to McpDisplayUrl
(which strips query, fragment, and userinfo) and drop the source from
the formatted message, matching the existing pattern on Connection and
ListTools variants.

Signed-off-by: Sébastien Han <seb@redhat.com>
* fix(token_usage): Gemini candidatesTokenCount optional

Gemini omits candidatesTokenCount in safety-filtered responses where
no output candidates are generated. I expect the required u64 field
could cause deserialization to fail, silently losing the input token
count as well. Now defaults to zero when absent.

Signed-off-by: Shane Utt <shaneutt@linux.com>

* fix(build): fix package name in filter discovery

Signed-off-by: Shane Utt <shaneutt@linux.com>

* fix(a2a): flush pending SSE state before clearing at end-of-stream

When a provider omits the trailing blank line before closing the
connection the SSE buffers may contain a complete but undispatched
event. The filter cleared this state without processing it, silently
dropping the last task route. This updates it to flush the state
and prior to clearing so these buffered SSE payloads will get
dispatched before we clean up.

Signed-off-by: Shane Utt <shaneutt@linux.com>

* perf(apis): replace allocs in URL path matching

Multiple sites collected path.split('/') into a heap-allocated
Vec<&str> just to pattern-match fixed-shape URL segments. These
run on every request for matched routes. This patch replaces those
with strip_prefix and strip_suffix chains that avoid the heap
allocation entirely.

Signed-off-by: Shane Utt <shaneutt@linux.com>

* fix(mcp): block unique-local IPv6 in SSRF checks

is_ssrf_sensitive did not check unique-local IPv6 (fc00::/7), so
addresses like fd00:ec2::23 (AWS ECS credential endpoint over IPv6)
bypassed SSRF protection.

Signed-off-by: Shane Utt <shaneutt@linux.com>

* fix(mcp): block cookie and fwd headers in mcp req

is_blocked_mcp_header did not block cookie, set-cookie, or
forwarded/x-forwarded-* headers. While these come from operator
configs rather than end-user HTTP requests, accidental pass-through
could leak stuff.

Signed-off-by: Shane Utt <shaneutt@linux.com>

* refactor: extract utils from store and rehydrate

There were some function pairs that were duplicated across filters, this
consolidates those into a single implementation both use.

Signed-off-by: Shane Utt <shaneutt@linux.com>

---------

Signed-off-by: Shane Utt <shaneutt@linux.com>
The refactor of sync_conversation_messages into
create_items_and_sync_messages / delete_item_and_sync_messages
dropped the pre-refactor existence guard: the message-cache UPDATE
result was discarded, so a conversation deleted concurrently between
the handler's existence check and the transaction would silently
succeed with a zero-row update.

Restore the guard at a single choke point per backend by checking
rows_affected on the UPDATE in pg_rebuild_messages and
sqlite_rebuild_messages, returning the original
"conversation disappeared during message sync" error. The enclosing
transaction rolls back on the propagated error, so no item mutations
persist. This covers both create and delete paths and works for the
SQLite path, which uses BEGIN IMMEDIATE and has no row lock to inspect.

Add SQLite tests for both paths and mirrored ignored PostgreSQL tests.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
create_items_and_sync_messages binds item.tenant_id/item.conversation_id
in the per-item INSERT while the lock, MAX(position), and rebuild
queries in the same transaction use the tenant_id/conversation_id
function parameters. If a caller ever passed items whose scope fields
diverged from the parameters, rows would be inserted outside the
locked/synced scope. Bind the function parameters instead so the
invariant can't be violated.

Signed-off-by: Eoin Fennessy <efenness@redhat.com>

rh-pre-commit.version: 2.4.0
rh-pre-commit.check-secrets: ENABLED
@eoinfennessy
eoinfennessy force-pushed the 358-refactor-sync_conversation_messages branch from 17a8c9c to ad90f9e Compare September 3, 2026 12:31
@praxis-bot-app

praxis-bot-app Bot commented Sep 3, 2026

Copy link
Copy Markdown

Missing Signed-off-by: eeccf2f, ad90f9e. All commits require sign-off (via git commit --signoff).

Signed-off-by: Eoin Fennessy <efenness@redhat.com>
Signed-off-by: Eoin Fennessy <efenness@redhat.com>
@eoinfennessy
eoinfennessy force-pushed the 358-refactor-sync_conversation_messages branch from ad90f9e to 7b39391 Compare September 3, 2026 12:33
Signed-off-by: Eoin Fennessy <efenness@redhat.com>
@eoinfennessy
eoinfennessy force-pushed the 358-refactor-sync_conversation_messages branch from 94e21ef to e4d62ca Compare September 3, 2026 12:43

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(conversations): atomic item mutations with transactional message cache sync

Severity Count
(none new) 0

Round 8 review -- no new findings at Medium or above.

The two actionable findings from Round 7 have been addressed:

  • [Fixed] rows_affected() check restored in pg_rebuild_messages and sqlite_rebuild_messages (9c9e8ff). A conversation deleted concurrently between the handler's existence check and the transaction now produces "conversation disappeared during message sync" and rolls back item mutations. Both create and delete paths are covered, with SQLite tests for each and mirrored #[ignore]d PostgreSQL tests.
  • [Fixed] INSERT in create_items_and_sync_messages now binds the function parameters tenant_id / conversation_id instead of the item record fields (345ad3d). The scope invariant is enforced at the query level across both backends.

Remaining prior findings (unchanged from Round 7):

  1. [Large] Unique index migration risk on existing data with duplicate positions (schemas.rs)
  2. [Medium] Coverage exclusion suppresses the entire store/postgres.rs file (Makefile)

@eoinfennessy
eoinfennessy added this pull request to the merge queue Sep 3, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to no response for status checks Sep 3, 2026
@leseb
leseb added this pull request to the merge queue Sep 3, 2026
Merged via the queue into praxis-proxy:main with commit 1ef8a53 Sep 3, 2026
32 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

praxis-known-issue Allow `test-praxis-main` to continue on error; Use for known / transient compatibility issues skip/pr-conventions

Projects

Development

Successfully merging this pull request may close these issues.

fix(conversations): refresh_message_cache constructs misleading partial ConversationRecord

6 participants