Make thread/channel/message deletion complete (hard delete)

Deleting a thread, channel, or message was a no-op/soft-delete: messages
were only dropped from the plugin outbox (still in hermes' session store,
hence searchable/recoverable) and channels/threads were merely archived.

Now deletion is complete and non-recoverable, with no search trace:

- purge.py (new): hard-delete from hermes' session store (state.db).
  delete_lane wipes a channel's/thread's sessions + messages; deleting a
  messages row also drops it from the FTS5 index via the delete triggers.
  delete_message removes one message, matched by (session, role, exact
  content, closest timestamp) since plugin m_<hex> ids aren't persisted.
- channels.py: delete() hard-deletes the row (and a channel's child
  threads) instead of archiving.
- outbox.py: add delete_lane() (wipe all frames for a lane) and
  message_info() (read a message's final role/text/ts for the match).
- adapter.py: on_channel_delete wipes outbox + session store;
  on_message_delete purges the session-store row per message.
- App: delete confirmations no longer claim history stays for search;
  ChannelStore removes a channel's threads on channel delete.
- Docs updated to describe hard deletion.
This commit is contained in:
ARIA committed 2026-08-21 19:47:55 +02:00
1 parent 9286937e2d
commit 17bf41a0b9
11 files changed
+373 -51

No files matched your search

+14 -6
View File
@@ -293,20 +293,28 @@ class ChannelDirectory:
return self.get(chat_id)
def delete(self, chat_id: str) -> dict[str, Any] | None:
"""Soft-delete (archive) a channel. History stays for search.
"""Hard-delete a channel or thread (and, for a channel, its threads).
The default channel cannot be deleted. Returns the (archived) entry,
or ``None`` when the id is unknown / is the default.
The row is removed from the directory entirely -- not recoverable. The
caller (adapter) is responsible for wiping the lane's history from the
outbox and the hermes session store so no search trace survives.
The default channel cannot be deleted. Returns the (deleted) entry, or
``None`` when the id is unknown / is the default.
"""
with self._lock:
row = self._conn.execute(
"SELECT is_default FROM channels WHERE chat_id = ?", (chat_id,)
"SELECT * FROM channels WHERE chat_id = ?", (chat_id,)
).fetchone()
if row is None or row["is_default"]:
return None
self._conn.execute("UPDATE channels SET archived = 1 WHERE chat_id = ?", (chat_id,))
entry = _row_to_entry(row)
self._conn.execute("DELETE FROM channels WHERE chat_id = ?", (chat_id,))
# A channel takes its threads with it.
if entry["kind"] != KIND_THREAD:
self._conn.execute("DELETE FROM channels WHERE parent_chat_id = ?", (chat_id,))
self._conn.commit()
return self.get(chat_id)
return entry
# ── reads ─────────────────────────────────────────────────────────────