import { getTeamsBasePath } from '@main/utils/pathDecoder'; import { randomUUID } from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; import { atomicWriteAsync } from './atomicWrite'; import { withFileLock } from './fileLock'; import { withInboxLock } from './inboxLock'; import type { InboxMessage, SendMessageRequest, SendMessageResult, TaskRef } from '@shared/types'; export interface MergeRuntimeDeliveryTaskRefsRequest { inboxName: string; messageId: string; relayOfMessageId: string; from: string; taskRefs: TaskRef[]; } export interface MergeRuntimeDeliveryTaskRefsResult { found: boolean; updated: boolean; message?: InboxMessage & { messageId: string }; } export interface CorrelateRuntimeDeliveryReplyRequest { inboxName: string; messageId: string; relayOfMessageId: string; from: string; taskRefs?: TaskRef[]; } export interface CorrelateRuntimeDeliveryReplyResult { found: boolean; updated: boolean; message?: InboxMessage & { messageId: string }; } export class TeamInboxWriter { async sendMessage(teamName: string, request: SendMessageRequest): Promise { const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${request.member}.json`); const messageId = request.messageId?.trim() || randomUUID(); const attachmentMeta = request.attachments?.map((a) => ({ id: a.id, filename: a.filename, mimeType: a.mimeType, size: a.size, })); const payload: InboxMessage = { from: request.from ?? 'user', to: request.to ?? request.member, text: request.text, timestamp: request.timestamp ?? new Date().toISOString(), read: false, taskRefs: request.taskRefs?.length ? request.taskRefs : undefined, actionMode: request.actionMode, commentId: typeof request.commentId === 'string' ? request.commentId : undefined, summary: request.summary, messageId, ...(request.relayOfMessageId && { relayOfMessageId: request.relayOfMessageId }), attachments: attachmentMeta?.length ? attachmentMeta : undefined, ...(request.source && { source: request.source }), ...(request.leadSessionId && { leadSessionId: request.leadSessionId }), ...(request.color && { color: request.color }), ...(request.conversationId && { conversationId: request.conversationId }), ...(request.replyToConversationId && { replyToConversationId: request.replyToConversationId, }), ...(request.toolSummary && { toolSummary: request.toolSummary }), ...(request.toolCalls && { toolCalls: request.toolCalls }), ...(request.messageKind && { messageKind: request.messageKind }), ...(request.workSyncIntent && { workSyncIntent: request.workSyncIntent }), ...(request.workSyncIntentKey && { workSyncIntentKey: request.workSyncIntentKey }), ...(request.workSyncReviewRequestEventIds?.length ? { workSyncReviewRequestEventIds: request.workSyncReviewRequestEventIds } : {}), ...(request.workSyncPayloadHash ? { workSyncPayloadHash: request.workSyncPayloadHash } : {}), ...(request.slashCommand && { slashCommand: request.slashCommand }), ...(request.commandOutput && { commandOutput: request.commandOutput }), }; let resultMessageId = messageId; let resultDeduplicated = false; await withFileLock(inboxPath, async () => { await withInboxLock(inboxPath, async () => { for (let attempt = 0; attempt < 3; attempt++) { const list = await this.readInbox(inboxPath); const duplicateIndex = this.findRuntimeDeliveryDuplicateIndex(list, payload); if (duplicateIndex >= 0) { const duplicate = list[duplicateIndex]; const merged = this.mergeTaskRefs(duplicate.taskRefs, payload.taskRefs); resultMessageId = duplicate.messageId ?? messageId; resultDeduplicated = true; if (merged.changed) { list[duplicateIndex] = { ...duplicate, taskRefs: merged.taskRefs, }; await atomicWriteAsync(inboxPath, JSON.stringify(list, null, 2)); const written = await this.readInbox(inboxPath); const writtenDuplicateIndex = this.findRuntimeDeliveryDuplicateIndex( written, payload ); const writtenDuplicate = writtenDuplicateIndex >= 0 ? written[writtenDuplicateIndex] : null; if ( writtenDuplicate && this.taskRefsIncludeAll(writtenDuplicate.taskRefs, payload.taskRefs ?? []) ) { return; } await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt)); continue; } return; } list.push(payload); await atomicWriteAsync(inboxPath, JSON.stringify(list, null, 2)); const written = await this.readInbox(inboxPath); if (written.some((msg) => msg.messageId === messageId)) { return; } await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt)); } throw new Error('Failed to verify inbox write'); }); }); return { deliveredToInbox: true, messageId: resultMessageId, ...(resultDeduplicated ? { deduplicated: true } : {}), }; } async mergeRuntimeDeliveryTaskRefs( teamName: string, request: MergeRuntimeDeliveryTaskRefsRequest ): Promise { const inboxName = request.inboxName.trim(); const messageId = request.messageId.trim(); const relayOfMessageId = request.relayOfMessageId.trim(); const taskRefs = this.normalizeTaskRefs(request.taskRefs); if (!inboxName || !messageId || !relayOfMessageId || taskRefs.length === 0) { return { found: false, updated: false }; } const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${inboxName}.json`); const expectedFrom = this.normalizeComparableParticipant(request.from); if (!expectedFrom) { return { found: false, updated: false }; } let result: MergeRuntimeDeliveryTaskRefsResult = { found: false, updated: false }; await withFileLock(inboxPath, async () => { await withInboxLock(inboxPath, async () => { for (let attempt = 0; attempt < 3; attempt++) { const list = await this.readInbox(inboxPath); const index = list.findIndex((message) => { const rowMessageId = typeof message.messageId === 'string' ? message.messageId.trim() : ''; const rowRelayOf = typeof message.relayOfMessageId === 'string' ? message.relayOfMessageId.trim() : ''; const rowSource = message.source; return ( rowMessageId === messageId && rowRelayOf === relayOfMessageId && this.normalizeComparableParticipant(message.from) === expectedFrom && (rowSource === undefined || rowSource === 'runtime_delivery') ); }); if (index < 0) { result = { found: false, updated: false }; return; } const existing = list[index]; const merged = this.mergeTaskRefs(existing.taskRefs, taskRefs); if (!merged.changed) { result = { found: true, updated: false, message: { ...existing, messageId }, }; return; } list[index] = { ...existing, taskRefs: merged.taskRefs }; await atomicWriteAsync(inboxPath, JSON.stringify(list, null, 2)); const written = await this.readInbox(inboxPath); const verified = written.find((message) => { const rowMessageId = typeof message.messageId === 'string' ? message.messageId.trim() : ''; const rowRelayOf = typeof message.relayOfMessageId === 'string' ? message.relayOfMessageId.trim() : ''; const rowSource = message.source; return ( rowMessageId === messageId && rowRelayOf === relayOfMessageId && this.normalizeComparableParticipant(message.from) === expectedFrom && (rowSource === undefined || rowSource === 'runtime_delivery') && this.taskRefsIncludeAll(message.taskRefs, taskRefs) ); }); if (verified) { result = { found: true, updated: true, message: { ...verified, messageId }, }; return; } await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt)); } throw new Error('Failed to verify inbox taskRefs merge'); }); }); return result; } async correlateRuntimeDeliveryReply( teamName: string, request: CorrelateRuntimeDeliveryReplyRequest ): Promise { const inboxName = request.inboxName.trim(); const messageId = request.messageId.trim(); const relayOfMessageId = request.relayOfMessageId.trim(); const expectedFrom = this.normalizeComparableParticipant(request.from); if (!inboxName || !messageId || !relayOfMessageId || !expectedFrom) { return { found: false, updated: false }; } const inboxPath = path.join(getTeamsBasePath(), teamName, 'inboxes', `${inboxName}.json`); const taskRefs = this.normalizeTaskRefs(request.taskRefs); let result: CorrelateRuntimeDeliveryReplyResult = { found: false, updated: false }; await withFileLock(inboxPath, async () => { await withInboxLock(inboxPath, async () => { for (let attempt = 0; attempt < 3; attempt++) { const list = await this.readInbox(inboxPath); const index = list.findIndex((message) => { const rowMessageId = typeof message.messageId === 'string' ? message.messageId.trim() : ''; const rowSource = message.source; return ( rowMessageId === messageId && this.normalizeComparableParticipant(message.from) === expectedFrom && (rowSource === undefined || rowSource === 'runtime_delivery') ); }); if (index < 0) { result = { found: false, updated: false }; return; } const existing = list[index]; const merged = this.mergeTaskRefs(existing.taskRefs, taskRefs); const currentRelayOf = typeof existing.relayOfMessageId === 'string' ? existing.relayOfMessageId.trim() : ''; if (currentRelayOf === relayOfMessageId && !merged.changed) { result = { found: true, updated: false, message: { ...existing, messageId }, }; return; } const nextMessage: InboxMessage = { ...existing, relayOfMessageId, ...(merged.taskRefs ? { taskRefs: merged.taskRefs } : {}), }; list[index] = nextMessage; await atomicWriteAsync(inboxPath, JSON.stringify(list, null, 2)); const written = await this.readInbox(inboxPath); const verified = written.find((message) => { const rowMessageId = typeof message.messageId === 'string' ? message.messageId.trim() : ''; const rowRelayOf = typeof message.relayOfMessageId === 'string' ? message.relayOfMessageId.trim() : ''; const rowSource = message.source; return ( rowMessageId === messageId && rowRelayOf === relayOfMessageId && this.normalizeComparableParticipant(message.from) === expectedFrom && (rowSource === undefined || rowSource === 'runtime_delivery') && this.taskRefsIncludeAll(message.taskRefs, taskRefs) ); }); if (verified) { result = { found: true, updated: true, message: { ...verified, messageId }, }; return; } await new Promise((resolve) => setTimeout(resolve, 10 * 2 ** attempt)); } throw new Error('Failed to verify inbox runtime delivery correlation update'); }); }); return result; } private findRuntimeDeliveryDuplicateIndex( messages: readonly InboxMessage[], payload: InboxMessage ): number { if ( payload.source !== 'runtime_delivery' || typeof payload.relayOfMessageId !== 'string' || payload.relayOfMessageId.trim().length === 0 ) { return -1; } const relayOfMessageId = payload.relayOfMessageId.trim(); const from = this.normalizeComparableParticipant(payload.from); const to = this.normalizeComparableParticipant(payload.to); const text = this.normalizeComparableText(payload.text); if (!from || !to || !text) { return -1; } return messages.findIndex( (candidate) => candidate.source === 'runtime_delivery' && (candidate.relayOfMessageId ?? '').trim() === relayOfMessageId && this.normalizeComparableParticipant(candidate.from) === from && this.normalizeComparableParticipant(candidate.to) === to && this.normalizeComparableText(candidate.text) === text ); } private mergeTaskRefs( existing: readonly TaskRef[] | undefined, incoming: readonly TaskRef[] | undefined ): { changed: boolean; taskRefs?: TaskRef[] } { const normalizedExisting = this.normalizeTaskRefs(existing); const normalizedIncoming = this.normalizeTaskRefs(incoming); if (normalizedIncoming.length === 0) { return { changed: false, taskRefs: normalizedExisting.length ? normalizedExisting : undefined, }; } const seen = new Set(normalizedExisting.map((taskRef) => this.taskRefKey(taskRef))); const merged = [...normalizedExisting]; let changed = false; for (const taskRef of normalizedIncoming) { const key = this.taskRefKey(taskRef); if (seen.has(key)) { continue; } seen.add(key); merged.push(taskRef); changed = true; } return { changed, taskRefs: merged.length ? merged : undefined }; } private taskRefsIncludeAll( actual: readonly TaskRef[] | undefined, expected: readonly TaskRef[] ): boolean { const actualKeys = new Set( this.normalizeTaskRefs(actual).map((taskRef) => this.taskRefKey(taskRef)) ); return this.normalizeTaskRefs(expected).every((taskRef) => actualKeys.has(this.taskRefKey(taskRef)) ); } private normalizeTaskRefs(taskRefs: readonly TaskRef[] | undefined): TaskRef[] { if (!Array.isArray(taskRefs)) { return []; } const normalized: TaskRef[] = []; for (const rawTaskRef of taskRefs as readonly unknown[]) { if (!rawTaskRef || typeof rawTaskRef !== 'object') { continue; } const taskRef = rawTaskRef as Record; const teamName = typeof taskRef.teamName === 'string' ? taskRef.teamName.trim() : ''; const taskId = typeof taskRef.taskId === 'string' ? taskRef.taskId.trim() : ''; const displayId = typeof taskRef.displayId === 'string' ? taskRef.displayId.trim() : ''; if (teamName && taskId && displayId) { normalized.push({ teamName, taskId, displayId }); } } return normalized; } private taskRefKey(taskRef: TaskRef): string { return `${taskRef.teamName.trim()}\u0000${taskRef.taskId.trim()}\u0000${taskRef.displayId.trim()}`; } private normalizeComparableParticipant(value: unknown): string { return typeof value === 'string' ? value.trim().toLowerCase() : ''; } private normalizeComparableText(value: unknown): string { return typeof value === 'string' ? value .trim() .replace(/\r\n/g, '\n') .replace(/[ \t]+/g, ' ') : ''; } private async readInbox(inboxPath: string): Promise { let raw: string; try { raw = await fs.promises.readFile(inboxPath, 'utf8'); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return []; } throw error; } const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) { return []; } return parsed.filter((item): item is InboxMessage => { if (!item || typeof item !== 'object') { return false; } const row = item as Partial; return ( typeof row.from === 'string' && typeof row.text === 'string' && typeof row.timestamp === 'string' && typeof row.read === 'boolean' ); }); } }