From 648268b2720065c620f3320f838f50b575db3b66 Mon Sep 17 00:00:00 2001 From: Groene AI <270696204+groeneai@users.noreply.github.com> Date: Mon, 30 Mar 2026 17:27:13 +0000 Subject: [PATCH] Fix source messages silently dropping updates from mutable sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub notifications are mutable — when someone @mentions you on a PR you authored, GitHub updates the existing notification's `reason` field from "author" to "mention" (same notification ID). The previous INSERT OR IGNORE silently dropped these updates since the (source, id) primary key already existed, making mentions invisible to consumers. Now checks if an existing record has different metadata or content before inserting. If changed, deletes the old record and re-inserts with a new rowid so consumer cursors see it as a new unread message. Unchanged records are still silently skipped (no noise). Co-Authored-By: Claude Opus 4.6 --- nerve/db/sources.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/nerve/db/sources.py b/nerve/db/sources.py index b9ee3373b..1d6f43088 100644 --- a/nerve/db/sources.py +++ b/nerve/db/sources.py @@ -109,7 +109,14 @@ async def insert_source_messages( source: str, ttl_days: int = 7, ) -> int: - """Bulk insert source records into the inbox. Returns count inserted.""" + """Bulk insert source records into the inbox. Returns count inserted. + + If a record with the same (source, id) already exists but has different + metadata or content, the old record is deleted and re-inserted with a + new rowid. This ensures mutable sources (e.g. GitHub notifications whose + ``reason`` field changes from "author" to "mention") surface as new + messages for consumer cursors. + """ import logging logger = logging.getLogger(__name__) now = datetime.now(timezone.utc) @@ -119,13 +126,40 @@ async def insert_source_messages( async with self._atomic(): for r in records: try: + new_metadata = json.dumps(r.metadata) if r.metadata else None + + # Check if this record already exists + async with self.db.execute( + "SELECT metadata, content FROM source_messages " + "WHERE source = ? AND id = ?", + (source, r.id), + ) as cursor: + existing = await cursor.fetchone() + + if existing: + old_metadata, old_content = existing[0], existing[1] + if old_metadata == new_metadata and old_content == r.content: + # Nothing changed — skip silently + continue + # Metadata or content changed — delete old record so the + # re-insert gets a new (higher) rowid, making it visible + # to consumer cursors that already read the old version. + await self.db.execute( + "DELETE FROM source_messages WHERE source = ? AND id = ?", + (source, r.id), + ) + logger.info( + "Source message %s/%s updated (metadata changed) — re-inserting", + source, r.id, + ) + await self.db.execute( - "INSERT OR IGNORE INTO source_messages " + "INSERT INTO source_messages " "(id, source, record_type, summary, content, raw_content, timestamp, metadata, created_at, expires_at) " "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (r.id, source, r.record_type, r.summary, r.content, getattr(r, 'raw_content', None), - r.timestamp, json.dumps(r.metadata) if r.metadata else None, + r.timestamp, new_metadata, now_iso, expires), ) inserted += 1