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
@@ -21,6 +21,20 @@ export const ChatMessageSchema = z.strictObject({
|
||||
/** Operator-maintained triage state. Independent of Matrix's own unread count. */
|
||||
export const ConversationTriageStatusSchema = z.enum(['open', 'snoozed', 'done']);
|
||||
|
||||
/**
|
||||
* Where the reply loop currently sits. Anything other than `active` means the
|
||||
* bot has handed the conversation over and replies are being written by hand.
|
||||
*/
|
||||
export const ConversationAutomationStatusSchema = z.enum([
|
||||
'active',
|
||||
'image-received',
|
||||
'human-required',
|
||||
'boundary',
|
||||
'user-overridden',
|
||||
]);
|
||||
/** The policy snapshotted when the conversation started. `manual` never auto-replies. */
|
||||
export const ConversationPolicyModeSchema = z.enum(['legacy', 'r4r', 'r4r-media', 'manual']);
|
||||
|
||||
export const ActiveChatSchema = z.strictObject({
|
||||
username: z.string(),
|
||||
roomName: z.string(),
|
||||
@@ -39,6 +53,9 @@ export const ActiveChatSchema = z.strictObject({
|
||||
originMessage: z.string().optional(),
|
||||
hasReceivedImages: z.boolean().optional(),
|
||||
sellerDetected: z.boolean().optional(),
|
||||
/** Last confirmed Reddit profile state for the other participant. */
|
||||
recipientAccountStatus: z.enum(['active', 'suspended', 'deleted']).optional(),
|
||||
recipientAccountStatusCheckedAt: z.number().optional(),
|
||||
owner: RedditAccountOwnerSchema.optional(),
|
||||
/** True when the newest message is inbound and newer than the read watermark. */
|
||||
hasUnreadInbound: z.boolean().optional(),
|
||||
@@ -48,6 +65,11 @@ export const ActiveChatSchema = z.strictObject({
|
||||
snoozedUntil: z.number().nullable().optional(),
|
||||
/** Timestamp of the newest message the operator has read. */
|
||||
lastReadMessageTs: z.number().optional(),
|
||||
/** Reply-loop state. Absent when no conversation state has been recorded yet. */
|
||||
automationStatus: ConversationAutomationStatusSchema.optional(),
|
||||
automationPolicyMode: ConversationPolicyModeSchema.optional(),
|
||||
/** Why automation handed over, when it has. */
|
||||
automationHandoffReason: z.string().nullable().optional(),
|
||||
});
|
||||
export const ChatsResponseSchema = z.array(ActiveChatSchema);
|
||||
export const ConversationsResponseSchema = ChatsResponseSchema;
|
||||
@@ -93,7 +115,7 @@ export const ConversationDetailResponseSchema = ActiveChatSchema.extend({
|
||||
automation: z.strictObject({
|
||||
turnsUsed: z.number().int().min(0).max(MAX_CONFIGURED_AUTOMATED_CONVERSATION_TURNS),
|
||||
remainingTurns: z.number().int().min(0).max(MAX_CONFIGURED_AUTOMATED_CONVERSATION_TURNS),
|
||||
status: z.enum(['active', 'image-received', 'human-required', 'boundary', 'user-overridden']),
|
||||
status: ConversationAutomationStatusSchema,
|
||||
handoffReason: z.string().nullable(),
|
||||
}),
|
||||
syncInProgress: z.strictObject({
|
||||
@@ -162,6 +184,8 @@ export const FetchAllChatsResponseSchema = z.strictObject({
|
||||
export type ActiveChat = z.infer<typeof ActiveChatSchema>;
|
||||
export type ChatMessage = z.infer<typeof ChatMessageSchema>;
|
||||
export type ConversationTriageStatus = z.infer<typeof ConversationTriageStatusSchema>;
|
||||
export type ConversationAutomationStatus = z.infer<typeof ConversationAutomationStatusSchema>;
|
||||
export type ConversationPolicyMode = z.infer<typeof ConversationPolicyModeSchema>;
|
||||
export type BulkMarkConversationsReadResponse = z.infer<typeof BulkMarkConversationsReadResponseSchema>;
|
||||
export type ChatSyncStatus = z.infer<typeof ChatSyncStatusSchema>;
|
||||
export type ConversationDetailResponse = z.infer<typeof ConversationDetailResponseSchema>;
|
||||
|
||||
Reference in new issue
Block a user