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:
1 parent
725b5fc4e6
commit
ac12a3146a
35 files changed
+1164
-129
No files matched your search
+9
-1
@@ -96,7 +96,15 @@ Legacy conversations may make at most one optional autonomous media request, inc
|
||||
|
||||
`ChatService` communicates with Reddit's Matrix homeserver, maintains an account-scoped SQLite-backed cache, resolves `t2_` identities to Reddit usernames, and emits sync progress/new-reply callbacks. Startup chooses full or delta sync from persisted cursor state. Operators can explicitly run tracked sync jobs through the versioned API.
|
||||
|
||||
`MediaService` resolves Matrix media, downloads it into the persistent data directory, and writes an account-scoped media index. `MediaSyncService` schedules incremental work only while the account is authenticated. The media listing reads persisted indexes for every stored account, including accounts whose runtimes are stopped or require login, and exposes received items only: records attributed to the owning Reddit/Matrix account are excluded, including historical self-sender forms. Private media is served behind the same authorization boundary as the dashboard and APIs.
|
||||
Each authenticated account runtime also checks the Reddit profiles represented in its cached conversations every four hours. Profile results are shared process-wide by case-insensitive username, so a recently checked recipient is not looked up again by another account runtime. A valid profile is recorded as active, Reddit's explicit suspension marker is recorded separately, and only an HTTP 404 is recorded as deleted; authentication, rate-limit, network, server, and malformed-response failures do not change the previous state. The dashboard removes deleted recipients from actionable conversation queues and exposes them through the dedicated **Account deleted** bucket.
|
||||
|
||||
`MediaService` resolves Matrix media, downloads it into the persistent data directory, and writes an account-scoped media index. Each Matrix message occurrence gets its own gallery record, while repeated occurrences of the same remote asset reuse one private file. Generic download content types are verified from the image bytes before an item is rejected as unsupported.
|
||||
|
||||
`MediaSyncService` schedules incremental work only while the account is authenticated. Incremental sync consumes the rooms changed since the durable Matrix cursor; the reported count is the number of rooms Matrix actually returned, not an assumed inbox limit. The cursor advances only after media attachment and cache persistence succeed, so a transient media failure is retried. When Matrix marks a room timeline as limited, chat sync paginates that room's history before advancing the cursor to close the omitted-event gap.
|
||||
|
||||
Full media sync walks every joined room and paginates until Matrix reports the end of history. Per-room history or media failures leave the full-sync marker unset and make an operator-triggered full media sync fail visibly; a later full sync can retry the incomplete rooms. Startup may retain the successfully loaded partial cache for availability, but it does not claim that a full backfill completed.
|
||||
|
||||
The media listing reads persisted indexes for every stored account, including accounts whose runtimes are stopped or require login, and exposes received items only: records attributed to the owning Reddit/Matrix account are excluded, including historical self-sender forms. Private media is served behind the same authorization boundary as the dashboard and APIs.
|
||||
|
||||
The process-global exchange library is separate. Dashboard uploads are decoded and re-encoded with `sharp`, which validates the actual JPEG/PNG/WebP contents, applies orientation, and removes EXIF/location/camera metadata. OpenRouter then assigns overlapping anatomy tags plus SFW/NSFW classification. Failed or low-confidence classifications remain disabled until the operator corrects them.
|
||||
|
||||
|
||||
+3
-1
@@ -13,6 +13,7 @@ The application uses one `better-sqlite3` database, normally `data/reddit-bot.db
|
||||
- `reddit_accounts`: verified Reddit/Matrix identity, encrypted-at-rest only to the degree provided by the host/volume, cookie JSON, Matrix credentials, active/status timestamps.
|
||||
- `reddit_conversations`: account-scoped cached Matrix conversation payloads.
|
||||
- `reddit_account_sync_state`: account-scoped sync cursor, full/delta timestamps, and invitation cooldown.
|
||||
- `reddit_recipient_account_status`: process-global, case-insensitive recipient username status (`active`, `suspended`, or `deleted`) and last confirmed check time.
|
||||
- `reddit_media`: account-scoped private-media metadata and relative file path.
|
||||
- `media_library_assets`: process-global operator-owned image metadata, overlapping anatomy tags, SFW/NSFW classification, consent time, enablement, dimensions, hash, and private relative path.
|
||||
- `media_library_sends`: account/conversation/inbound-event reservation and confirmed/failed Matrix delivery audit for a shared library asset.
|
||||
@@ -52,8 +53,9 @@ The special legacy-unassigned account exists only to preserve rows created befor
|
||||
5. Conversation policy is snapshotted in state JSON when automation starts for that conversation.
|
||||
6. Secrets and the database path are not stored in `app_config`.
|
||||
7. Private media paths in SQLite are relative to the persistent data root; HTTP access remains authenticated.
|
||||
8. Received `reddit_media` and operator `media_library_assets` are separate ownership domains; no automatic copy or promotion exists.
|
||||
8. Received `reddit_media` and operator `media_library_assets` are separate ownership domains; no automatic copy or promotion exists. Received-media identity represents the account-scoped Matrix message occurrence, so repeated use of one remote asset can produce multiple gallery rows that safely share the same private relative file path.
|
||||
9. A library image content hash has at most one reservation per account conversation, even if the same file is later re-uploaded under a new asset ID; an inbound request also has at most one selected asset. Failed or uncertain attempts remain reserved to prevent duplicates.
|
||||
10. Recipient account status changes only after a definitive Reddit profile response. Transient lookup failures retain the previous value, and the shared check timestamp prevents duplicate lookups across account runtimes during the four-hour interval.
|
||||
|
||||
## Repository map
|
||||
|
||||
|
||||
@@ -63,6 +63,8 @@ Exchange-library uploads require an explicit consent checkbox before the raw ima
|
||||
|
||||
Conversation reply composers include a collapsed saved-image picker. It lists only server-approved assets for the latest inbound request, keeps thumbnails blurred until revealed, and requires selection plus a dedicated confirmation dialog before sending. The server remains authoritative and may reject a choice if the conversation changes before the rate-limited send executes.
|
||||
|
||||
The conversation inbox has a system-derived **Account deleted** bucket alongside the operator-maintained open, snoozed, and done queues. A deleted recipient is excluded from the default, unread, needs-reply, waiting, manual, and image buckets and is labelled in both the list and detail views. Suspended profiles are not treated as deleted.
|
||||
|
||||
## Feature structure
|
||||
|
||||
Each route is a slice under `features/<name>/` with page/route components, local styles, and feature-specific helpers. Cross-feature visual/domain components live under `components/`; application lifecycle belongs under `app/`; transport belongs under `api/`.
|
||||
|
||||
+3
-2
@@ -36,14 +36,15 @@ This is an ownership map, not an API dump. Read the owning implementation and te
|
||||
| `services/chat/cache.ts` | Account-scoped cached conversations and sync state | SQLite is durable; do not share caches across accounts. |
|
||||
| `services/chat/matrix/username-resolver.ts` | Matrix/Reddit identity mapping | Preserve unresolved internal IDs without leaking them in operator text. |
|
||||
| `services/chat/matrix/token-refresher.ts` | Explicit refresh-unavailable behavior | New credentials come from verified extension dumps, not token sniffing. |
|
||||
| `services/recipient-account-status.ts` | Periodic cached-conversation recipient profile checks | Only definitive Reddit results replace durable state; checks are authentication-bound and must not block the reply loop. |
|
||||
| `services/conversation-automation.ts` | Reply-first policy, claims, retries, send-time refresh, handoffs, turn budgets | Snapshot policy; claim before send; supersede stale queued work; reconcile pending claims; fail closed. |
|
||||
| `services/conversation-state.ts` | Pure conversation parsing/state/safety helpers | Conversation text is untrusted data; keep prompt boundaries explicit. |
|
||||
| `services/conversation-boundaries.ts` | Sequence-aware media requests, refusals, and full-stop detection | Short ambiguous replies become boundaries only in the matching request context. |
|
||||
| `services/outbound-message-safety.ts` | Final no-proximity/no-meeting rule | This is defense in depth beyond prompts. |
|
||||
| `services/conversation-outcome*.ts` | Response outcome classification/recording | Keep analytics attribution account/message scoped. |
|
||||
| `services/response-checker.ts` | Inbox/performance response checks and successful examples | Non-overlapping; do not confuse chat sync with outcome maturity. |
|
||||
| `services/media.ts` | Download, deduplicate, index, and list private media | Preserve account ownership and authenticated serving. |
|
||||
| `services/media-sync.ts` | Periodic authenticated media sync | Stop while auth is unavailable and on runtime shutdown. |
|
||||
| `services/media.ts` | Download, verify, index, and list private media occurrences while reusing asset files | Preserve account ownership, retryable download failures, and authenticated serving. |
|
||||
| `services/media-sync.ts` | Periodic authenticated media sync and operator-visible completion status | Stop while auth is unavailable and on runtime shutdown; do not report partial work as success. |
|
||||
| `services/media-library.ts` | Shared operator uploads, image validation/re-encoding, classification state, and private files | Require consent confirmation; received media never enters this store. |
|
||||
| `services/media-library-exchange.ts` | Inbound request detection, progressive selection, reservation, and image-first exchange | Never offer proactively; exact tags do not substitute; no asset reuse per conversation. |
|
||||
| `database/media-library-repository.ts` | Shared asset metadata and durable account/conversation send audit | Reserve before remote send; uncertain sends stay consumed. |
|
||||
|
||||
Reference in new issue
Block a user