feat(chat): invalidate saved chats on sources sync — versioned stamps, stale marker, Regenerate against the new index

This commit is contained in:
2026-08-30 23:39:15 -04:00
parent ea8e041189
commit 32b7bfd4b3
26 changed files with 2145 additions and 63 deletions
+42 -1
View File
@@ -23,7 +23,14 @@ Data model — see ``.agent/PLAN.md`` §Data Model:
14 shape) — phase 50; ``/api/chat`` stays
stateless; ``share_token`` (NULL = private,
``uuid4`` = publicly readable at
``/shared/<token>``) — phase 51.
``/shared/<token>``) — phase 51; ``sources_version``
(the KB generation the conversation was saved
against; 0 = the pre-counter KB, stale on the
first bump) — phase 53.
* ``sources_meta`` — single-row sources-version counter: which
generation of the knowledge base is current,
bumped exactly once per KB-changing sync so saved
chats can be marked stale (phase 53).
"""
from __future__ import annotations
@@ -146,6 +153,33 @@ class KbOverview(Base):
)
class SourcesMeta(Base):
"""Single-row sources-version counter (phase 53).
Exactly one row (``id = 1``, seeded by migration 0010 — the
``kb_overview`` id=1 precedent) holds the current **generation** of
the knowledge base. ``version`` is bumped exactly once per sync that
actually changed the KB (phase 53, task 02 — change-gated on
``added + updated + pruned > 0``), so it doubles as the invalidation
marker for saved chats: a ``saved_chats`` row stamped with an older
generation is *stale* — its answers predate the current index and
may be Regenerated against it (phase 53, tasks 03/05). The row is
seeded by the migration (not lazily on first bump), so
:func:`app.rag.sources_meta.current_sources_version` is a plain PK
read.
"""
__tablename__ = "sources_meta"
id: Mapped[int] = mapped_column(Integer, primary_key=True, server_default="1")
#: The KB generation. 0 = the pre-counter KB (everything indexed
#: before phase 53); incremented by one per KB-changing sync.
version: Mapped[int] = mapped_column(Integer, server_default="0")
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class GitSource(Base):
"""One admin-managed source (phase 35; kind discriminator, phase 38).
@@ -203,6 +237,13 @@ class SavedChat(Base):
share_token: Mapped[uuid.UUID | None] = mapped_column(
UUID(as_uuid=True), unique=True, nullable=True
)
#: The sources version (KB generation) the conversation was saved
#: against (phase 53): stamped by the API at save time. Existing
#: rows (saved before the counter existed) stamp ``0`` — "the
#: pre-counter KB" — and become stale on the first KB-changing sync
#: (stale = ``sources_version < current``, computed server-side by
#: the chats API, phase 53 task 03).
sources_version: Mapped[int] = mapped_column(Integer, server_default="0")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()