agent-ecosystem/src/main/services/team/TeamSentMessagesStore.ts
iliya 6bcb81d337 feat: implement structured task references and enhance task handling
- Introduced a new structured task reference format `{ taskId, displayId, teamName }` for consistent task mention persistence across UI and storage.
- Enhanced message handling in various components to support the new task reference structure, including normalization and validation.
- Updated task-related functions to accommodate optional task reference fields, improving task management and messaging capabilities.
- Improved rendering and navigation of task references in the UI, ensuring stable links across messages and comments.
- Refactored task reference utilities for better integration and usability within the application.
2026-03-11 15:14:19 +02:00

119 lines
4.3 KiB
TypeScript

import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as path from 'path';
import { atomicWriteAsync } from './atomicWrite';
import type { InboxMessage } from '@shared/types';
const MAX_MESSAGES = 200;
const MAX_SENT_MESSAGES_FILE_BYTES = 2 * 1024 * 1024;
const logger = createLogger('TeamSentMessagesStore');
export class TeamSentMessagesStore {
private getFilePath(teamName: string): string {
return path.join(getTeamsBasePath(), teamName, 'sentMessages.json');
}
async readMessages(teamName: string): Promise<InboxMessage[]> {
const filePath = this.getFilePath(teamName);
let raw: string;
try {
const stat = await fs.promises.stat(filePath);
// Avoid hangs on non-regular files (FIFO, sockets) and huge/binary files.
if (!stat.isFile() || stat.size > MAX_SENT_MESSAGES_FILE_BYTES) {
return [];
}
raw = await readFileUtf8WithTimeout(filePath, 5_000);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return [];
}
if (error instanceof FileReadTimeoutError) {
logger.error(`Timed out reading sent messages for ${teamName}`);
return [];
}
// Bug #4: graceful degradation instead of crashing
logger.error(`Failed to read sent messages for ${teamName}: ${String(error)}`);
return [];
}
let parsed: unknown;
try {
parsed = JSON.parse(raw) as unknown;
} catch {
return [];
}
if (!Array.isArray(parsed)) {
return [];
}
const messages: InboxMessage[] = [];
for (const item of parsed) {
if (!item || typeof item !== 'object') continue;
const row = item as Partial<InboxMessage>;
if (
typeof row.from !== 'string' ||
typeof row.text !== 'string' ||
typeof row.timestamp !== 'string' ||
typeof row.messageId !== 'string' ||
row.messageId.trim().length === 0
) {
continue;
}
// Bug #5: preserve optional fields (attachments, color)
messages.push({
from: row.from,
to: typeof row.to === 'string' ? row.to : undefined,
text: row.text,
timestamp: row.timestamp,
read: typeof row.read === 'boolean' ? row.read : true,
taskRefs: Array.isArray(row.taskRefs) ? row.taskRefs : undefined,
summary: typeof row.summary === 'string' ? row.summary : undefined,
messageId: row.messageId,
color: typeof row.color === 'string' ? row.color : undefined,
attachments: Array.isArray(row.attachments) ? row.attachments : undefined,
source: typeof row.source === 'string' ? (row.source as InboxMessage['source']) : undefined,
leadSessionId: typeof row.leadSessionId === 'string' ? row.leadSessionId : undefined,
conversationId: typeof row.conversationId === 'string' ? row.conversationId : undefined,
replyToConversationId:
typeof row.replyToConversationId === 'string' ? row.replyToConversationId : undefined,
toolSummary: typeof row.toolSummary === 'string' ? row.toolSummary : undefined,
toolCalls: Array.isArray(row.toolCalls)
? (row.toolCalls as unknown[])
.filter(
(tc): tc is { name: string; preview?: string } =>
tc != null &&
typeof tc === 'object' &&
typeof (tc as Record<string, unknown>).name === 'string'
)
.map((tc) => ({
name: tc.name,
preview: typeof tc.preview === 'string' ? tc.preview : undefined,
}))
: undefined,
});
}
return messages;
}
async appendMessage(teamName: string, message: InboxMessage): Promise<void> {
// Bug #6: wrap in try/catch to prevent crash on IO errors
try {
const existing = await this.readMessages(teamName);
existing.push(message);
// Trim to MAX_MESSAGES (keep newest)
const trimmed = existing.length > MAX_MESSAGES ? existing.slice(-MAX_MESSAGES) : existing;
await atomicWriteAsync(this.getFilePath(teamName), JSON.stringify(trimmed, null, 2));
} catch (error) {
logger.error(`Failed to append sent message for ${teamName}: ${String(error)}`);
}
}
}