fix: exempt the hidden canonical Bot Chat from the idle auto-archive sweep

The archived-holder carve-out in _set_session_title lets the next Bot open
claim the "Bot Chat" title from an archived canonical row. That is correct
for a deliberate sidebar archive, but archive_stale_sessions (the opt-in
sessions.auto_archive sweep) could also archive an idle hidden Bot Chat with
end_reason NULL, which unarchive_recoverable_session refuses; the next Bot
click then stripped the old row's title, retiring the bot's whole history
on an idle timer with no way back.

Skip the hidden canonical Bot Chat in the sweep SELECT using the same
hidden + exact-title predicate set_session_pinned already uses to protect
it, so only an explicit archive retires a Bot Chat. Reword the bot-mode doc
so it no longer promises the retired (still hidden) chat is reachable from
the archive view.

Review finding: auto_archive sweep could irreversibly retire the canonical Bot Chat; docs over-promised archive-view reachability.
This commit is contained in:
teknium1
2026-09-14 21:21:16 -07:00
committed by Teknium
parent a4fad54aa7
commit 4db4650a01
3 changed files with 22 additions and 3 deletions
+5 -2
View File
@@ -247,7 +247,9 @@ class SessionMaintenanceMixin:
latest message / ``started_at``); may archive unended sessions. ``archived = 0`` makes
repeats no-ops; only lineage tips (``end_reason <> 'compression'``) are candidates — a
stale tip archives its chain via :meth:`set_session_archived`, so an old compressed-away
root with a recent continuation is never matched."""
root with a recent continuation is never matched. The hidden canonical Bot Chat (same
predicate as :meth:`set_session_pinned`) is exempt: only a deliberate archive may retire
it, since archiving releases its registry title to the next Bot open."""
if idle_days is None or idle_days < 0:
return 0
cutoff = time.time() - float(idle_days) * 86400.0
@@ -258,9 +260,10 @@ class SessionMaintenanceMixin:
WHERE s.archived = 0
AND COALESCE(s.end_reason, '') <> 'compression'
{pin_clause}
AND NOT (COALESCE(s.hidden, 0) <> 0 AND COALESCE(s.title, '') = ?)
AND {_sql_session_last_active("s")} < ?
ORDER BY s.started_at ASC
""", (cutoff,))
""", (self.CANONICAL_BOT_CHAT_TITLE, cutoff))
for row in rows:
self.set_session_archived(row[0], True)
return len(rows)
@@ -78,6 +78,22 @@ def test_deliberately_archived_canonical_chat_releases_name_for_replacement(db):
assert row and row["id"] == "replacement"
def test_auto_archive_sweep_skips_the_canonical_chat(db):
"""Only a deliberate archive may retire a Bot Chat; the idle sweep must not
(it would strand an unrecoverable, soon-to-be-untitled row)."""
import time
forever = _make_canonical(db)
db.create_session("ordinary", source="desktop")
stale = time.time() - 10 * 86400
db._write_sql("UPDATE sessions SET started_at = ?, last_activity_at = ?", (stale, stale))
assert db.archive_stale_sessions(3) == 1
assert db.get_session("ordinary")["archived"]
assert not db.get_session(forever)["archived"]
assert db.get_session(forever)["title"] == SessionDB.CANONICAL_BOT_CHAT_TITLE
def test_auto_titler_still_cannot_touch_the_canonical_row(db):
# Pre-existing provenance contract, re-pinned here: user-authority title
# outranks derived/llm, so the turn-start auto-titler can never displace
+1 -1
View File
@@ -25,7 +25,7 @@ The roster shows one row per agent profile: avatar, latest-message preview, and
:::note The canonical Bot Chat is a forever-chat
Typing `/new` (or `/reset`) inside a Bot's canonical chat would fork the relationship into a scratch session — the one thing Bot Mode promises never happens. The composer reroutes it to `/compact` instead: fresh working context, same conversation. Regular sessions on the same profile keep full `/new` freedom.
Archiving a Bot Chat from the sidebar retires it: the next click on the Bot starts a fresh conversation that becomes the new canonical Bot Chat, and the archived one keeps its history (untitled) in the archive.
Archiving a Bot Chat from the sidebar retires it: the next click on the Bot starts a fresh conversation that becomes the new canonical Bot Chat. The retired chat stays archived and hidden — its history is preserved in the database, but it is no longer reachable from the Bot or the archive view. The automatic idle-archive sweep (`sessions.auto_archive`) never retires a Bot Chat; only an explicit archive does.
:::
### Organize bots into sections