feat: recover missing conversations, fix media attachment, add manual inbox tab

Missing conversations: recipient Reddit accounts that are deleted or suspended
left conversations unaccounted for. A new RecipientAccountStatusService polls
Reddit for the users behind cached conversations and records their account
status durably, with progressive backfill and delta-media fixes in ChatService
so history and images are no longer dropped mid-sync.

Media: attachMedia now surfaces download failures as MediaAttachmentError so the
sync cursor can be retried instead of silently losing an image, recognizes image
bytes when Matrix returns a generic content type, and keeps separate gallery
occurrences when the same asset appears in more than one event.

Manual inbox tab: the conversations list now carries the reply-loop status, so
the inbox can separate conversations the operator is driving by hand from the
ones automation is still handling. A Manual filter pill and a per-row chip
explain why each conversation was handed over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jamie BruntonandClaude Opus 5 committed 2026-08-10 11:03:33 +01:00
1 parent 725b5fc4e6
commit ac12a3146a
35 files changed
+1164 -129

No files matched your search

+31
View File
@@ -425,6 +425,37 @@ export function runMigrations(db: Database.Database): void {
CREATE INDEX IF NOT EXISTS idx_conversation_read_state_status
ON conversation_read_state(account_id, status, snoozed_until);
`),
() => db.exec(`
CREATE TABLE reddit_media_occurrences (
account_id TEXT NOT NULL,
media_id TEXT NOT NULL,
chat_username TEXT,
chat_room_id TEXT,
sender TEXT NOT NULL,
timestamp INTEGER NOT NULL,
original_url TEXT NOT NULL,
file_name TEXT NOT NULL,
relative_path TEXT NOT NULL,
created_at INTEGER NOT NULL,
PRIMARY KEY (account_id, media_id),
FOREIGN KEY (account_id) REFERENCES reddit_accounts(account_id) ON DELETE CASCADE
);
INSERT INTO reddit_media_occurrences SELECT * FROM reddit_media;
DROP TABLE reddit_media;
ALTER TABLE reddit_media_occurrences RENAME TO reddit_media;
CREATE INDEX idx_reddit_media_timestamp ON reddit_media(account_id, timestamp DESC);
CREATE INDEX idx_reddit_media_room ON reddit_media(account_id, chat_room_id);
CREATE INDEX idx_reddit_media_original_url ON reddit_media(account_id, original_url);
`),
() => db.exec(`
CREATE TABLE IF NOT EXISTS reddit_recipient_account_status (
username TEXT PRIMARY KEY COLLATE NOCASE,
status TEXT NOT NULL CHECK(status IN ('active', 'suspended', 'deleted')),
checked_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_reddit_recipient_account_status_due
ON reddit_recipient_account_status(checked_at);
`),
];
let currentVersion = db.pragma('user_version', { simple: true }) as number;