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

@@ -29,12 +29,21 @@ class ChannelStore {
/** Reconcile a server frame into the cache. */
fun onFrame(frame: Frame) {
when (frame.type) {
TYPE_CHANNEL_CREATED, TYPE_CHANNEL_RENAMED -> upsert(frame)
TYPE_CHANNEL_DELETED -> remove(frame)
TYPE_CHANNEL_CREATED, TYPE_CHANNEL_RENAMED -> {
upsert(frame)
}
TYPE_CHANNEL_DELETED -> {
remove(frame)
}
TYPE_CHANNEL_LIST -> {
frame.payloadAs<ChannelListPayload>()?.let { setAll(it.channels) }
}
else -> Unit
else -> {
Unit
}
}
}
@@ -48,7 +57,10 @@ class ChannelStore {
private fun remove(frame: Frame) {
val p = frame.payloadAs<ChannelDeletedPayload>() ?: return
_channels.value = _channels.value.filter { it.chatId != p.chatId }
// A channel delete also removes its threads (child entries); a thread
// delete removes just that thread. Both are hard deletes server-side.
_channels.value =
_channels.value.filter { it.chatId != p.chatId && it.parentChatId != p.chatId }
}
private fun sorted(list: List<ChannelInfo>): List<ChannelInfo> =
@@ -62,20 +74,16 @@ class ChannelStore {
// ── UI helpers ────────────────────────────────────────────────────────
/** Channels (default + user channels) for the drawer/rail. */
fun channelsForDrawer(): List<ChannelInfo> =
_channels.value.filter { it.kind != "thread" }
fun channelsForDrawer(): List<ChannelInfo> = _channels.value.filter { it.kind != "thread" }
/** Threads under a channel, for the topic switcher. */
fun threadsFor(chatId: String): List<ChannelInfo> =
_channels.value.filter { it.kind == "thread" && it.parentChatId == chatId }
fun threadsFor(chatId: String): List<ChannelInfo> = _channels.value.filter { it.kind == "thread" && it.parentChatId == chatId }
fun byId(chatId: String): ChannelInfo? =
_channels.value.firstOrNull { it.chatId == chatId }
fun byId(chatId: String): ChannelInfo? = _channels.value.firstOrNull { it.chatId == chatId }
fun defaultChannel(): ChannelInfo? =
_channels.value.firstOrNull { it.isDefault } ?: _channels.value.firstOrNull()
fun defaultChannel(): ChannelInfo? = _channels.value.firstOrNull { it.isDefault } ?: _channels.value.firstOrNull()
fun clear() {
_channels.value = emptyList()
}
}
}
@@ -839,7 +839,7 @@ fun ChatScreen(controller: IrisController) {
onDismissRequest = { deleteThread = null },
title = { Text("Delete topic?") },
text = {
Text("Delete \"${t.name}\"? It will be removed from this channel.")
Text("Delete \"${t.name}\"? The topic and all its messages will be permanently deleted.")
},
confirmButton = {
TextButton(onClick = {
@@ -861,9 +861,9 @@ fun ChatScreen(controller: IrisController) {
text = {
Text(
if (n == 1) {
"This message will be deleted for all devices."
"This message will be permanently deleted for all devices and can no longer be found in search."
} else {
"These messages will be deleted for all devices."
"These messages will be permanently deleted for all devices and can no longer be found in search."
},
)
},
@@ -1049,7 +1049,7 @@ fun ChatScreen(controller: IrisController) {
AlertDialog(
onDismissRequest = { channelToDelete = null },
title = { Text("Delete channel?") },
text = { Text("Delete \"${ch.name}\"? Its history stays available for search.") },
text = { Text("Delete \"${ch.name}\"? The channel and all its messages will be permanently deleted.") },
confirmButton = {
TextButton(onClick = {
channelToDelete = null