Skip to content

feat(collab): E1 — DM schema migration (remote_msg_id, delivered_at, peer_outbox)#2047

Open
hognek wants to merge 4 commits into
jaylfc:devfrom
hognek:feat/collab-e1-dm-schema-outbox
Open

feat(collab): E1 — DM schema migration (remote_msg_id, delivered_at, peer_outbox)#2047
hognek wants to merge 4 commits into
jaylfc:devfrom
hognek:feat/collab-e1-dm-schema-outbox

Conversation

@hognek

@hognek hognek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements E1 of the cross-user collaboration epic (#2012): human DM schema migration and peer outbox for store-and-forward delivery.

Closes #2020.

Changes

Schema migration (boot-brick safe)

  • chat_messages: Added remote_msg_id TEXT and delivered_at REAL columns via guarded _post_init ALTER TABLE pattern (same as existing deleted_at/expires_at)
  • chat_messages: Added UNIQUE INDEX on (channel_id, remote_msg_id) WHERE remote_msg_id IS NOT NULL for idempotent remote delivery
  • The index is created in _post_init (not SCHEMA) per BaseStore boot-brick rule

send_remote_message()

  • New method on ChatMessageStore — atomic INSERT with dedup
  • Catches IntegrityError on duplicate (channel_id, remote_msg_id) and returns None
  • Sets delivered_at on receipt, author_id namespaced as hub:{username}

PeerOutboxStore

  • New store: peer_outbox table with exponential backoff retry schedule (60s → 120s → 300s → 600s → 1800s cap)
  • Methods: enqueue, dequeue_due, mark_sent, mark_failed, purge_for_contact, count_for_contact
  • Attached at app.state.peer_outbox

Tests

  • 18 new tests in tests/test_chat_remote_dm.py
  • Upgrade migration tested against pre-change v0 DB (boot-brick regression gate)
  • Dedup: duplicate remote_msg_id returns None, same remote_msg_id different channel both succeed
  • Outbox: enqueue/dequeue, sent removal, backoff retry, purge, count, limit, future retry

All tests pass

100 passed (18 new + 82 related/regression)

Summary by CodeRabbit

  • New Features

    • Added support for receiving remote direct messages with duplicate detection and preserved message metadata.
    • Added a persistent peer delivery outbox for queued messages, retry scheduling, delivery tracking, and cleanup.
    • Existing chat databases are upgraded automatically while preserving stored messages.
    • Added reliable startup and shutdown handling for the peer delivery outbox.
  • Bug Fixes

    • Prevented duplicate remote messages from being stored.
    • Added bounded retries with backoff for messages that cannot be delivered.

…t, peer_outbox

- Add remote_msg_id + delivered_at columns to chat_messages via
  guarded _post_init migration (boot-brick safe)
- Add UNIQUE index on (channel_id, remote_msg_id) for idempotent
  remote message delivery
- Add send_remote_message() method with IntegrityError dedup
- Add PeerOutboxStore with store-and-forward, exponential backoff,
  purge-on-revoke
- Wire up PeerOutboxStore in app.py lifespan + state
- Add 18 tests: upgrade migration, dedup, outbox operations

Refs: jaylfc#2020, epic jaylfc#2012
@hognek
hognek marked this pull request as ready for review July 19, 2026 12:51
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@hognek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 12ac6696-e062-4d47-9d34-772172b91fb2

📥 Commits

Reviewing files that changed from the base of the PR and between a20e863 and 5c2b0d3.

📒 Files selected for processing (2)
  • tinyagentos/chat/message_store.py
  • tinyagentos/chat/peer_outbox.py
📝 Walkthrough

Walkthrough

Changes

The chat store now supports schema migration and deduplication for remotely received messages. A new SQLite-backed peer outbox provides queued delivery, retries, purging, and counting, and is initialized and exposed through the application lifecycle. Tests cover migrations, message handling, outbox operations, and reopening existing databases.

Remote DM delivery

Layer / File(s) Summary
Remote message schema and insertion path
tests/test_chat_remote_dm.py, tinyagentos/chat/message_store.py
Adds remote_msg_id and delivered_at, a channel-scoped partial unique index, remote message insertion, and migration and deduplication tests.
Peer outbox queue and retry operations
tinyagentos/chat/peer_outbox.py, tests/test_chat_remote_dm.py
Adds the outbox schema and implements enqueue, due retrieval, send removal, retry backoff, contact purging, counts, and lifecycle tests.
Application outbox lifecycle wiring
tinyagentos/app.py
Creates, initializes, exposes, and closes the peer outbox store through the application lifecycle.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers remote DM migration, deduplication, outbox, and migration tests, but the linked issue also requires dm-remote channel support and namespaced author IDs, which aren't shown. Add the dm-remote chat_channels support and confirm author_id namespacing as hub:{name}; ensure the implementation matches all #2020 acceptance criteria.
✅ Passed checks (4 passed)
Check name Status Explanation
Out of Scope Changes check ✅ Passed All described changes relate to the E1 DM schema migration, remote message handling, outbox delivery, or tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: E1 DM schema migration plus peer outbox support.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

# Dedup index MUST be created here (not in SCHEMA) because it references
# columns added by _post_init — per BaseStore boot-brick rule.
try:
await self._db.execute(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: CREATE UNIQUE INDEX failure is silently swallowed by the bare except Exception: pass.

If the index cannot be created (e.g. a future backfill/migration introduces two rows with the same non-NULL remote_msg_id, or a partial-index corruption), the app boots without the dedup index. send_remote_message would then silently double-insert the same remote message with different ids, breaking idempotency. Consider logging the failure and/or verifying the index exists after _post_init so a missing index is a loud boot error rather than a silent correctness regression.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/chat/message_store.py Outdated
)
await self._db.commit()
return await self.get_message(msg_id)
except sqlite3.IntegrityError:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: The blanket except sqlite3.IntegrityError: return None conflates a genuine duplicate with any other constraint violation (e.g. a NULL in the NOT NULL columns channel_id/author_id/content, or a malformed value).

All of these paths are reported to the caller as "duplicate already delivered" (None), masking real insert failures and dropping the message without surfacing an error. The only realistic IntegrityError source today is the dedup index, but this is fragile: tightening any NOT NULL/column constraint later would turn hard errors into silent no-ops. Prefer catching only the dedup case (e.g. inspect e.sqlite_errorname == 'CONSTRAINT_UNIQUE' / the index name) and re-raising or returning an error sentinel for other violations.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

self,
channel_id: str,
author_id: str,
contact_id: str,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: The contact_id parameter is accepted but never used — it is not persisted in the chat_messages row nor referenced anywhere in the method.

Callers may assume it is stored for later routing/audit, but it is silently discarded. Either persist it (e.g. in metadata or a dedicated column) or remove the parameter to avoid implying behavior that does not happen.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

"complete",
json.dumps(metadata or {}),
now,
remote_msg_id,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: remote_msg_id is taken directly from the caller with no non-null/non-empty validation, yet the dedup index is partial (WHERE remote_msg_id IS NOT NULL).

If a caller passes remote_msg_id=None (the signature types it as str but enforces nothing at runtime), the value is stored as NULL and the unique index is bypassed — dedup is silently disabled and duplicate remote deliveries are inserted. Add a guard that rejects/raises on falsy remote_msg_id, or document loudly that None intentionally disables dedup.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/chat/peer_outbox.py Outdated
row = await cursor.fetchone()
if row is None:
return
new_attempts = row[0] + 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: There is no maximum-attempt / terminal-failure state. mark_failed increments attempts and pushes next_retry_at forward forever (the delay caps at 1800s but attempts grows unbounded).

A message whose peer is permanently unreachable will be retried indefinitely and the peer_outbox row is never removed or flagged dead, so the queue grows without bound and count_for_contact / dequeue_due keep returning it. Consider a max_attempts after which the row is moved to a dead-letter state (or purged) so the outbox does not accumulate undead rows.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

"""

# Exponential backoff schedule: 60, 120, 300, 600, 1800 (cap)
_BACKOFF_SECONDS = (0, 60, 120, 300, 600, 1800)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: The leading 0 in _BACKOFF_SECONDS = (0, 60, 120, 300, 600, 1800) is dead code.

attempts starts at 0 and the first mark_failed sets new_attempts = 1, so index 0 is never read; the effective schedule is indices 1–5. The module docstring and the comment on line 29 both describe the schedule as "60, 120, 300, 600, 1800" (5 values), which does not match the 6-tuple here. Drop the 0 and align the tuple with the documented schedule to avoid confusion.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No New Issues Found | Recommendation: Merge

Overview

The incremental diff (1869a295c2b0d3) touched 1 file: tinyagentos/chat/peer_outbox.py. The mark_failed rewrite from a BEGIN/read-then-write transaction to an atomic UPDATE ... RETURNING is correct and resolves the prior WARNING about no terminal-failure path and the prior race-condition finding.

Changes Verified (1 file)
  • tinyagentos/chat/peer_outbox.py
    • Lines 91-122: mark_failed now performs the attempt increment atomically via UPDATE ... RETURNING attempts. The terminal-failure path (new_attempts >= _MAX_ATTEMPTS → delete) is preserved and consistent with the prior row[0] + 1 semantics (terminal at 10 retries). The row is None (row already gone) guard correctly returns False. Backoff index math min(new_attempts, len(_BACKOFF_SECONDS) - 1) remains correct (max index 5 = 1800s). The race between concurrent drain passes that the prior finding flagged is eliminated because the increment now happens inside SQLite rather than in Python.
Prior Findings Resolved by This Update
  • peer_outbox.py:96 no max-attempts terminal path — WARNING: resolved; mark_failed deletes the row after _MAX_ATTEMPTS retries (still present at 5c2b0d3).
  • peer_outbox.py:103 read-then-write race — resolved; increment is now atomic via UPDATE ... RETURNING.
Files Reviewed (1 file, incremental)
  • tinyagentos/chat/peer_outbox.py - 0 new issues
Previous Review Summaries (3 snapshots, latest commit 1869a29)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 1869a29)

Status: No New Issues Found | Recommendation: Merge

Overview

The incremental diff (a20e8631869a29) touched 2 files. No new issues were introduced; prior findings were resolved or tightened.

Changes Verified (2 files)
  • tinyagentos/chat/message_store.py
    • Line 218: delivered_at now uses time.time() instead of now. now was created_at when a sender timestamp was supplied, so delivered_at was incorrectly set to the sender's created_at. This is now the receiver's actual receipt time — a genuine correctness fix.
    • Line 226: Dedup guard tightened to require both "UNIQUE" and "remote_msg_id" in the exception string. This is strictly more correct than the prior single-substring check (still substring-based — see pre-existing SUGGESTION, not duplicated).
  • tinyagentos/chat/peer_outbox.py
    • mark_failed: Wrapped the read-then-write in an explicit BEGIN/transaction with proper ROLLBACK on the not-found path and on exceptions. This resolves the prior WARNING about no terminal-failure/max-attempts path. Backoff index math and _MAX_ATTEMPTS = 10 remain consistent.
Prior Findings Resolved by This Update
  • peer_outbox.py (no max-attempts terminal path) — WARNING: resolved; mark_failed now deletes the row after _MAX_ATTEMPTS retries.
  • message_store.py:226 dedup guard — SUGGESTION: tightened (still substring-based; pre-existing comment retained).
Files Reviewed (2 files, incremental)
  • tinyagentos/chat/message_store.py - 0 new issues
  • tinyagentos/chat/peer_outbox.py - 0 new issues

Previous review (commit a20e863)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/chat/message_store.py 226 Dedup detection still relies on fragile substring-matching of the exception message rather than sqlite_errorname/index name
Previously Reported Issues — Resolved in This Update

All 6 findings from the prior review commit (3695296036023093dac210df8756dde40cc2a998) are now addressed:

  • message_store.py:109 — index-creation failure now logs a warning instead of pass
  • message_store.py (blanket IntegrityError) — now re-raises non-dedup constraint violations ✔
  • message_store.py:166contact_id is now persisted into metadata.contact_id
  • message_store.py:217remote_msg_id now validated for non-empty string (ValueError) ✔
  • peer_outbox.py:103_MAX_ATTEMPTS terminal-failure path added (row deleted after 10 retries) ✔
  • peer_outbox.py:32 — leading 0 sentinel now documented as intentional ✔
Files Reviewed (4 files)
  • tinyagentos/chat/message_store.py - 0 new issues (1 prior resolved)
  • tinyagentos/chat/peer_outbox.py - 0 new issues (prior resolved)
  • tests/test_chat_remote_dm.py - 0 issues (verified new tests match implementation)
  • tinyagentos/app.py - 0 issues (wiring only)

Fix these issues in Kilo Cloud

Previous review (commit 3695296)

Status: 6 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 3
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/chat/message_store.py 109 CREATE UNIQUE INDEX failure silently swallowed by except Exception: pass — app can boot without dedup index, causing silent double-delivery
tinyagentos/chat/message_store.py 211 Blanket except sqlite3.IntegrityError: return None conflates genuine duplicates with any other constraint violation, masking real insert failures
tinyagentos/chat/peer_outbox.py 96 No max-attempts / terminal-failure state — failed rows retry forever, outbox grows unbounded

SUGGESTION

File Line Issue
tinyagentos/chat/message_store.py 163 contact_id parameter accepted but never used/persisted
tinyagentos/chat/message_store.py 205 remote_msg_id not validated for None/empty — partial index silently disables dedup
tinyagentos/chat/peer_outbox.py 30 Leading 0 in _BACKOFF_SECONDS is dead code and mismatches documented schedule
Files Reviewed (4 files)
  • tinyagentos/chat/message_store.py - 4 issues
  • tinyagentos/chat/peer_outbox.py - 2 issues
  • tinyagentos/app.py - 0 issues (wiring only)
  • tests/test_chat_remote_dm.py - 0 issues (verified, patterns match stores)

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 42.1K · Output: 2.2K · Cached: 102K

- Log warning on dedup index creation failure instead of silently passing
- Validate remote_msg_id non-empty before INSERT (raises ValueError)
- Store contact_id in message metadata for downstream consumers
- Narrow IntegrityError catch to only suppress dedup violations;
  re-raise structural violations (e.g. NOT NULL)
- Add _MAX_ATTEMPTS=10 terminal-failure guard to peer_outbox;
  mark_failed returns bool, deletes row at cap
- Document _BACKOFF_SECONDS sentinel index 0
Comment thread tinyagentos/chat/message_store.py Outdated
except sqlite3.IntegrityError as exc:
# Re-raise on structural integrity violations (e.g. NOT NULL on
# channel_id) — only the dedup index violation is benign.
if "remote_msg_id" not in str(exc):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: Dedup detection relies on substring-matching the exception message ("remote_msg_id" not in str(exc)).

This is brittle: it assumes SQLite always renders the violating column name in the error string for the unique constraint. If the index/column is renamed, or a future SQLite build changes the message wording, a genuine duplicate would no longer be recognized as benign and would instead hit the raise branch (surfacing as an unhandled error), or a non-dedup IntegrityError whose message happens to contain the substring would be silently swallowed as a duplicate. Prefer inspecting exc.sqlite_errorname (e.g. == 'CONSTRAINT_UNIQUE') and/or checking the index name explicitly for a robust distinction.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tinyagentos/chat/message_store.py (1)

159-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

contact_id parameter is accepted but never used.

contact_id is a required positional parameter (line 163) yet the method body never reads it — it's not stored, not validated against author_id, and not used to scope anything. As written it's dead code that could mislead future maintainers into assuming some contact-based check happens here.

Either remove the parameter or wire it into the intended behavior (e.g. validating author_id corresponds to contact_id, or persisting it for audit/outbox correlation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/chat/message_store.py` around lines 159 - 216, The
send_remote_message method accepts contact_id but never uses it; remove this
parameter if it is not required by the API, or implement the intended
contact-based validation or persistence so it meaningfully affects message
handling. Update all callers consistently and preserve the existing
deduplication and commit behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tinyagentos/chat/message_store.py`:
- Around line 182-216: In the message insertion flow, decouple the timestamps
used by the created_at and delivered_at columns: preserve the caller-supplied
created_at value for historical creation time, but compute delivered_at from the
current wall-clock time when the hub receives the message. Update the variables
and INSERT parameters in the surrounding message-store method so the inline
receipt-time behavior remains accurate.
- Around line 95-116: Update the schema migration logic in _post_init to stop
swallowing failures from CREATE UNIQUE INDEX idx_chat_messages_remote_dedup,
allowing unexpected database errors to propagate. Narrow the exception handling
for the remote_msg_id and delivered_at ALTER TABLE statements to ignore only the
expected “column already exists” condition, while surfacing or logging all other
failures.

In `@tinyagentos/chat/peer_outbox.py`:
- Around line 88-103: Update mark_failed to perform the attempts increment
atomically in SQLite rather than reading attempts and writing a computed value
in separate statements. Use an UPDATE ... RETURNING flow to obtain the
incremented attempts value, calculate next_retry_at from that value, and
preserve the existing no-op behavior when outbox_id does not exist.

---

Nitpick comments:
In `@tinyagentos/chat/message_store.py`:
- Around line 159-216: The send_remote_message method accepts contact_id but
never uses it; remove this parameter if it is not required by the API, or
implement the intended contact-based validation or persistence so it
meaningfully affects message handling. Update all callers consistently and
preserve the existing deduplication and commit behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f49b87ea-c6b6-4079-b007-4fc67af11c7f

📥 Commits

Reviewing files that changed from the base of the PR and between 992d797 and 3695296.

📒 Files selected for processing (4)
  • tests/test_chat_remote_dm.py
  • tinyagentos/app.py
  • tinyagentos/chat/message_store.py
  • tinyagentos/chat/peer_outbox.py

Comment thread tinyagentos/chat/message_store.py Outdated
Comment on lines +95 to +116
# E1 collab: remote message dedup columns
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN remote_msg_id TEXT")
await self._db.commit()
except Exception:
pass
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN delivered_at REAL")
await self._db.commit()
except Exception:
pass
# Dedup index MUST be created here (not in SCHEMA) because it references
# columns added by _post_init — per BaseStore boot-brick rule.
try:
await self._db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_dedup "
"ON chat_messages(channel_id, remote_msg_id) "
"WHERE remote_msg_id IS NOT NULL"
)
await self._db.commit()
except Exception:
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Silent failure of the dedup unique index defeats the PR's core guarantee.

The CREATE UNIQUE INDEX ... idx_chat_messages_remote_dedup is the sole mechanism enforcing idempotent remote delivery. Wrapping it in a blanket except Exception: pass means any failure other than "already exists" (disk/permission/syntax issue) silently leaves the index missing, and send_remote_message's dedup would then silently stop working with zero signal to operators. Same concern applies to the two ALTER TABLE guards above it.

🐛 Proposed fix: narrow the except and log unexpected failures
-        try:
-            await self._db.execute("ALTER TABLE chat_messages ADD COLUMN remote_msg_id TEXT")
-            await self._db.commit()
-        except Exception:
-            pass
-        try:
-            await self._db.execute("ALTER TABLE chat_messages ADD COLUMN delivered_at REAL")
-            await self._db.commit()
-        except Exception:
-            pass
+        try:
+            await self._db.execute("ALTER TABLE chat_messages ADD COLUMN remote_msg_id TEXT")
+            await self._db.commit()
+        except sqlite3.OperationalError as exc:
+            if "duplicate column" not in str(exc).lower():
+                raise
+        try:
+            await self._db.execute("ALTER TABLE chat_messages ADD COLUMN delivered_at REAL")
+            await self._db.commit()
+        except sqlite3.OperationalError as exc:
+            if "duplicate column" not in str(exc).lower():
+                raise
         # Dedup index MUST be created here (not in SCHEMA) because it references
         # columns added by _post_init — per BaseStore boot-brick rule.
-        try:
-            await self._db.execute(
-                "CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_dedup "
-                "ON chat_messages(channel_id, remote_msg_id) "
-                "WHERE remote_msg_id IS NOT NULL"
-            )
-            await self._db.commit()
-        except Exception:
-            pass
+        await self._db.execute(
+            "CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_dedup "
+            "ON chat_messages(channel_id, remote_msg_id) "
+            "WHERE remote_msg_id IS NOT NULL"
+        )
+        await self._db.commit()

CREATE UNIQUE INDEX IF NOT EXISTS is already idempotent, so it doesn't need a try/except at all — a real failure here should surface loudly rather than be swallowed.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# E1 collab: remote message dedup columns
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN remote_msg_id TEXT")
await self._db.commit()
except Exception:
pass
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN delivered_at REAL")
await self._db.commit()
except Exception:
pass
# Dedup index MUST be created here (not in SCHEMA) because it references
# columns added by _post_init — per BaseStore boot-brick rule.
try:
await self._db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_dedup "
"ON chat_messages(channel_id, remote_msg_id) "
"WHERE remote_msg_id IS NOT NULL"
)
await self._db.commit()
except Exception:
pass
# E1 collab: remote message dedup columns
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN remote_msg_id TEXT")
await self._db.commit()
except sqlite3.OperationalError as exc:
if "duplicate column" not in str(exc).lower():
raise
try:
await self._db.execute("ALTER TABLE chat_messages ADD COLUMN delivered_at REAL")
await self._db.commit()
except sqlite3.OperationalError as exc:
if "duplicate column" not in str(exc).lower():
raise
# Dedup index MUST be created here (not in SCHEMA) because it references
# columns added by _post_init — per BaseStore boot-brick rule.
await self._db.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS idx_chat_messages_remote_dedup "
"ON chat_messages(channel_id, remote_msg_id) "
"WHERE remote_msg_id IS NOT NULL"
)
await self._db.commit()
🧰 Tools
🪛 Ruff (0.15.21)

[error] 99-100: try-except-pass detected, consider logging the exception

(S110)


[warning] 99-99: Do not catch blind exception: Exception

(BLE001)


[error] 104-105: try-except-pass detected, consider logging the exception

(S110)


[warning] 104-104: Do not catch blind exception: Exception

(BLE001)


[error] 115-116: try-except-pass detected, consider logging the exception

(S110)


[warning] 115-115: Do not catch blind exception: Exception

(BLE001)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tinyagentos/chat/message_store.py` around lines 95 - 116, Update the schema
migration logic in _post_init to stop swallowing failures from CREATE UNIQUE
INDEX idx_chat_messages_remote_dedup, allowing unexpected database errors to
propagate. Narrow the exception handling for the remote_msg_id and delivered_at
ALTER TABLE statements to ignore only the expected “column already exists”
condition, while surfacing or logging all other failures.

Source: Linters/SAST tools

Comment thread tinyagentos/chat/message_store.py
Comment thread tinyagentos/chat/peer_outbox.py Outdated
hognek added 2 commits July 19, 2026 15:02
- Use time.time() for delivered_at (actual receipt time), separate from
  sender's created_at — per CR spec conformance
- Narrow IntegrityError suppression to UNIQUE + remote_msg_id only
  (re-raise all other constraint violations like NOT NULL)
- Wrap mark_failed read+write in transaction to eliminate
  read-then-write race on attempts counter
Replaces the read-then-write race pattern with an atomic SQLite
UPDATE ... RETURNING increment so concurrent drain passes cannot
undercount attempts.  Keeps the _MAX_ATTEMPTS terminal-failure
path.  Resolves CodeRabbit finding on PR jaylfc#2047.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant