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

+18 -2
View File
@@ -13,7 +13,11 @@ import {
ActivityLogRepository,
RecipientHistoryRepository,
} from '../database/repositories.js';
import { AccountContentRepository, RedditAccountsRepository } from '../database/account-repositories.js';
import {
AccountContentRepository,
RedditAccountsRepository,
RedditRecipientAccountStatusRepository,
} from '../database/account-repositories.js';
import { RedditApiClient, RedditRateLimitError, type RedditPost } from '../reddit/api.js';
import { RedditRequestCoordinator } from '../reddit/request-coordinator.js';
import { OpenRouterClient } from '../ai/openrouter.js';
@@ -48,6 +52,7 @@ import { ConversationAutomationService } from './conversation-automation.js';
import { AiUsageRepository } from '../database/ai-usage-repository.js';
import { MediaLibraryRepository } from '../database/media-library-repository.js';
import { MediaLibraryExchangeService } from './media-library-exchange.js';
import { RecipientAccountStatusService } from './recipient-account-status.js';
export interface ConcreteRuntimeFactoryOptions {
config: Config;
@@ -212,6 +217,14 @@ async function buildRuntime(
);
await chatService.loadCache();
const mediaSyncService = new MediaSyncService(chatService, logger, accountConfig.media.syncIntervalMs, accountAuth);
const recipientAccountStatusRepo = new RedditRecipientAccountStatusRepository(db);
const recipientAccountStatusService = new RecipientAccountStatusService(
chatService,
redditApi,
recipientAccountStatusRepo,
logger,
accountAuth,
);
const responseChecker = new ResponseCheckerService(
chatPage, messagesRepo, successfulMessagesRepo, postsRepo, logger,
accountAuth, conversationOutcomesRepo,
@@ -250,10 +263,11 @@ async function buildRuntime(
browserProfileDir, browserClient, accountAuth, matrixClient, chatCache, redditApi,
postsRepo, messagesRepo, successfulMessagesRepo, deliveryOutboxRepo,
conversationStateRepo, conversationReadStateRepo, conversationOutcomesRepo,
redditUsersRepo, accountContentRepo,
redditUsersRepo, accountContentRepo, recipientAccountStatusRepo,
activityLogRepo,
monitorService, processorService, responseChecker, conversationAutomation, chatService, mediaService,
mediaSyncService, mediaExchange, rateLimiter,
recipientAccountStatusService,
};
const lifecycle = new AccountRuntimeLoop(
resources, accountConfig, logger, isBotRunning, recipientDeduplicator,
@@ -293,6 +307,7 @@ class AccountRuntimeLoop {
if (this.loop) return;
this.controller = new AbortController();
this.resources.mediaSyncService.start();
this.resources.recipientAccountStatusService.start();
void this.resources.chatService.runStartupSync()
.then(() => this.resources.chatService.enrichUsernames())
.catch((error) => this.logger.error(`[account ${this.resources.accountAuth.getSnapshot().redditUsername}] startup sync failed:`, error));
@@ -302,6 +317,7 @@ class AccountRuntimeLoop {
async stop(): Promise<void> {
this.controller?.abort();
this.resources.mediaSyncService.stop();
this.resources.recipientAccountStatusService.stop();
await this.loop?.catch((error) => {
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error;
});