BUG-HUNT: [data-integrity] SessionService.append_message() sequence number based on count — not atomic, causes duplicate sequences under concurrency #7411

Open
opened 2026-04-10 19:06:20 +00:00 by HAL9000 · 0 comments
Owner

Bug Report: Data Integrity — Non-Atomic Message Sequence Assignment

Severity Assessment

  • Impact: Concurrent append_message() calls to the same session will produce duplicate sequence numbers, breaking message ordering and history reconstruction
  • Likelihood: Medium — any session with concurrent callers (e.g., async workers and the main thread both appending messages)
  • Priority: High

Location

  • File: src/cleveragents/application/services/session_service.py
  • Function: PersistentSessionService.append_message()
  • Lines: 151–160

Description

The message sequence number is computed by counting existing messages and using the count as the next sequence number:

count = self._message_repo.count_for_session(session_id)
message = SessionMessage(
    message_id=str(ULID()),
    ...
    sequence=count,  # <-- race: two concurrent threads get the same count
    ...
)
self._message_repo.append(session_id, message)

This is a classic read-modify-write race condition. If two threads call append_message() simultaneously for the same session:

  1. Thread A: count_for_session() → returns 5
  2. Thread B: count_for_session() → returns 5
  3. Thread A: creates message with sequence=5, appends
  4. Thread B: creates message with sequence=5, appends

Result: two messages with sequence=5, session history is corrupt and unorerable.

Evidence

# src/cleveragents/application/services/session_service.py, lines 151-160
# Determine next sequence number
count = self._message_repo.count_for_session(session_id)  # <-- reads count

message = SessionMessage(
    message_id=str(ULID()),
    role=role,
    content=content,
    sequence=count,          # <-- uses count as sequence (race window!)
    timestamp=datetime.now(),
    metadata=metadata or {},
)
self._message_repo.append(session_id, message)  # <-- concurrent threads both append with same seq

Expected Behavior

Each message appended to a session should have a unique, monotonically increasing sequence number.

Actual Behavior

Under concurrent access, two messages can receive the same sequence number.

Suggested Fix

Use a database-level approach:

  1. Use SELECT MAX(sequence) + 1 FOR UPDATE within a transaction
  2. Or use a database-native auto-increment sequence column
  3. Or serialize append_message() with a per-session lock (e.g., from LockService)
  4. Alternatively, use ULID as ordering key instead of integer sequence (ULIDs are time-ordered)

Category

concurrency

TDD Note

After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with tags: @tdd_issue, @tdd_issue_, @tdd_expected_fail.


Automated by CleverAgents Bot
Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor

## Bug Report: Data Integrity — Non-Atomic Message Sequence Assignment ### Severity Assessment - **Impact**: Concurrent `append_message()` calls to the same session will produce duplicate sequence numbers, breaking message ordering and history reconstruction - **Likelihood**: Medium — any session with concurrent callers (e.g., async workers and the main thread both appending messages) - **Priority**: High ### Location - **File**: `src/cleveragents/application/services/session_service.py` - **Function**: `PersistentSessionService.append_message()` - **Lines**: 151–160 ### Description The message sequence number is computed by counting existing messages and using the count as the next sequence number: ```python count = self._message_repo.count_for_session(session_id) message = SessionMessage( message_id=str(ULID()), ... sequence=count, # <-- race: two concurrent threads get the same count ... ) self._message_repo.append(session_id, message) ``` This is a classic read-modify-write race condition. If two threads call `append_message()` simultaneously for the same session: 1. Thread A: `count_for_session()` → returns 5 2. Thread B: `count_for_session()` → returns 5 3. Thread A: creates message with `sequence=5`, appends 4. Thread B: creates message with `sequence=5`, appends Result: two messages with `sequence=5`, session history is corrupt and unorerable. ### Evidence ```python # src/cleveragents/application/services/session_service.py, lines 151-160 # Determine next sequence number count = self._message_repo.count_for_session(session_id) # <-- reads count message = SessionMessage( message_id=str(ULID()), role=role, content=content, sequence=count, # <-- uses count as sequence (race window!) timestamp=datetime.now(), metadata=metadata or {}, ) self._message_repo.append(session_id, message) # <-- concurrent threads both append with same seq ``` ### Expected Behavior Each message appended to a session should have a unique, monotonically increasing sequence number. ### Actual Behavior Under concurrent access, two messages can receive the same sequence number. ### Suggested Fix Use a database-level approach: 1. Use `SELECT MAX(sequence) + 1 FOR UPDATE` within a transaction 2. Or use a database-native auto-increment sequence column 3. Or serialize `append_message()` with a per-session lock (e.g., from `LockService`) 4. Alternatively, use `ULID` as ordering key instead of integer sequence (ULIDs are time-ordered) ### Category concurrency ### TDD Note After this bug issue is verified, a corresponding Type/Testing issue will be created for TDD with tags: @tdd_issue, @tdd_issue_<this-issue-number>, @tdd_expected_fail. --- **Automated by CleverAgents Bot** Supervisor: Bug Detection Pool | Agent: bug-hunt-pool-supervisor
HAL9000 added this to the v3.2.0 milestone 2026-04-10 19:35:38 +00:00
HAL9000 self-assigned this 2026-04-11 03:21:08 +00:00
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#7411
No description provided.