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
+16 -2
View File
@@ -387,7 +387,7 @@ def _drop_absent_share_url(model: BaseModel, handler: SerializerFunctionWrapHand
class SavedChatOut(BaseModel):
"""One saved chat, full payload (create/get/put response, phase 50;
``share_url``, phase 51 task 02).
``share_url``, phase 51 task 02; ``stale``, phase 53 task 03).
``messages`` round-trips the ``bor.chat.v1`` record list losslessly
— the restore path is pixel-identical by construction.
@@ -395,6 +395,13 @@ class SavedChatOut(BaseModel):
``share_url`` (phase 51): ``"/shared/<token>"`` while the chat is
shared, ABSENT from the JSON when unshared (``None`` → dropped by
:func:`_drop_absent_share_url` — no ``null`` in the wire shape).
``stale`` (phase 53): true iff the row's ``sources_version`` stamp
is behind the current ``sources_meta`` generation — the answer
predates the latest KB-changing sync. Computed server-side (the
client never does staleness math); ``SharedChatOut`` deliberately
carries no staleness surface (the public snapshot is frozen by
design, phase 51).
"""
id: uuid.UUID
@@ -404,6 +411,9 @@ class SavedChatOut(BaseModel):
message_count: int
messages: list[ChatMessage]
share_url: str | None = None
#: Required (no default): the API must always compute staleness
#: server-side — there is no wire shape without the flag.
stale: bool
@model_serializer(mode="wrap")
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Any:
@@ -418,7 +428,9 @@ class SavedChatRow(BaseModel):
(Title, Messages count, Updated). ``share_url`` is populated here so
the History page's Share column renders straight from ``GET
/api/chats`` — no second fetch per row (``None`` → absent, the same
omission rule as :class:`SavedChatOut`).
omission rule as :class:`SavedChatOut`). ``stale`` (phase 53) feeds
the History page's Stale column the same way: one ``GET`` powers
every column.
"""
id: uuid.UUID
@@ -426,6 +438,8 @@ class SavedChatRow(BaseModel):
updated_at: datetime
message_count: int
share_url: str | None = None
#: Required (no default) — see :attr:`SavedChatOut.stale`.
stale: bool
@model_serializer(mode="wrap")
def _serialize(self, handler: SerializerFunctionWrapHandler) -> Any: