agent-ecosystem/agent-teams-controller/src/internal/messageStore.js
iliya 0d0364cfd3 feat: introduce MEMBER_DELEGATE_DESCRIPTION for action mode protocols
- Added MEMBER_DELEGATE_DESCRIPTION constant to centralize the delegate instructions for action mode protocols, improving code maintainability.
- Updated buildActionModeProtocolText to utilize the new constant, enhancing clarity and reducing duplication.
- Enhanced messageStore and TeamProvisioningService to support filePath handling in attachments, improving metadata management.
- Updated type definitions to reflect the addition of MEMBER_DELEGATE_DESCRIPTION in the protocols interface.
2026-03-18 14:01:21 +02:00

238 lines
7.2 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
function nowIso() {
return new Date().toISOString();
}
function ensureDir(dirPath) {
fs.mkdirSync(dirPath, { recursive: true });
}
function readJson(filePath, fallbackValue) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch {
return fallbackValue;
}
}
function writeJson(filePath, value) {
ensureDir(path.dirname(filePath));
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
fs.writeFileSync(tempPath, JSON.stringify(value, null, 2));
fs.renameSync(tempPath, filePath);
}
function getInboxPath(paths, memberName) {
return path.join(paths.teamDir, 'inboxes', `${String(memberName).trim()}.json`);
}
function getSentMessagesPath(paths) {
return path.join(paths.teamDir, 'sentMessages.json');
}
function normalizeAttachments(attachments) {
if (!Array.isArray(attachments) || attachments.length === 0) {
return undefined;
}
const normalized = attachments
.filter((item) => item && typeof item === 'object')
.map((item) => ({
id: String(item.id || '').trim(),
filename: String(item.filename || '').trim(),
mimeType: String(item.mimeType || '').trim(),
size: Number(item.size || 0),
...(typeof item.filePath === 'string' && item.filePath.trim()
? { filePath: item.filePath.trim() }
: {}),
}))
.filter((item) => item.id && item.filename && item.mimeType && Number.isFinite(item.size));
return normalized.length > 0 ? normalized : undefined;
}
function normalizeTaskRefs(taskRefs) {
if (!Array.isArray(taskRefs) || taskRefs.length === 0) {
return undefined;
}
const normalized = taskRefs
.filter((item) => item && typeof item === 'object')
.map((item) => ({
taskId: String(item.taskId || '').trim(),
displayId: String(item.displayId || '').trim(),
teamName: String(item.teamName || '').trim(),
}))
.filter((item) => item.taskId && item.displayId && item.teamName);
return normalized.length > 0 ? normalized : undefined;
}
function buildMessage(flags, defaults) {
const timestamp =
typeof flags.timestamp === 'string' && flags.timestamp.trim() ? flags.timestamp.trim() : nowIso();
const messageId =
typeof flags.messageId === 'string' && flags.messageId.trim()
? flags.messageId.trim()
: crypto.randomUUID();
const attachments = normalizeAttachments(flags.attachments);
const taskRefs = normalizeTaskRefs(flags.taskRefs);
return {
from:
typeof flags.from === 'string' && flags.from.trim()
? flags.from.trim()
: defaults.from || 'user',
...(defaults.to ? { to: defaults.to } : {}),
text: String(flags.text || ''),
timestamp,
read: defaults.read,
...(taskRefs ? { taskRefs } : {}),
...(typeof flags.summary === 'string' && flags.summary.trim()
? { summary: flags.summary.trim() }
: {}),
...(typeof flags.relayOfMessageId === 'string' && flags.relayOfMessageId.trim()
? { relayOfMessageId: flags.relayOfMessageId.trim() }
: {}),
...(typeof flags.source === 'string' && flags.source.trim() ? { source: flags.source.trim() } : {}),
...(typeof flags.leadSessionId === 'string' && flags.leadSessionId.trim()
? { leadSessionId: flags.leadSessionId.trim() }
: {}),
...(typeof flags.conversationId === 'string' && flags.conversationId.trim()
? { conversationId: flags.conversationId.trim() }
: {}),
...(typeof flags.replyToConversationId === 'string' && flags.replyToConversationId.trim()
? { replyToConversationId: flags.replyToConversationId.trim() }
: {}),
...(typeof flags.color === 'string' && flags.color.trim() ? { color: flags.color.trim() } : {}),
...(typeof flags.toolSummary === 'string' && flags.toolSummary.trim()
? { toolSummary: flags.toolSummary.trim() }
: {}),
...(Array.isArray(flags.toolCalls) && flags.toolCalls.length > 0
? {
toolCalls: flags.toolCalls
.filter((item) => item && typeof item === 'object' && typeof item.name === 'string')
.map((item) => ({
name: item.name,
...(typeof item.preview === 'string' ? { preview: item.preview } : {}),
})),
}
: {}),
...(attachments ? { attachments } : {}),
messageId,
};
}
function appendRow(filePath, row) {
const current = readJson(filePath, []);
const list = Array.isArray(current) ? current : [];
list.push(row);
writeJson(filePath, list);
return row;
}
function sendInboxMessage(paths, flags) {
const memberName =
typeof flags.member === 'string' && flags.member.trim()
? flags.member.trim()
: typeof flags.to === 'string' && flags.to.trim()
? flags.to.trim()
: '';
if (!memberName) {
throw new Error('Missing recipient');
}
const payload = buildMessage(flags, {
from: 'user',
to: memberName,
read: false,
});
appendRow(getInboxPath(paths, memberName), payload);
return {
deliveredToInbox: true,
messageId: payload.messageId,
message: payload,
};
}
function appendSentMessage(paths, flags) {
const payload = buildMessage(flags, {
from: 'team-lead',
to: typeof flags.to === 'string' && flags.to.trim() ? flags.to.trim() : undefined,
read: true,
});
appendRow(getSentMessagesPath(paths), payload);
return payload;
}
/**
* Exact readonly lookup by messageId across sent messages and all inbox files.
*
* Used by task_create_from_message to resolve provenance. Lookup is exact-messageId
* only and must never resolve by relayOfMessageId, text matching, or active context.
* Must reject ambiguous matches (same messageId in multiple stores) instead of guessing.
*
* Returns { message, store } or throws.
*/
function lookupMessage(paths, messageId) {
const id = typeof messageId === 'string' ? messageId.trim() : '';
if (!id) {
throw new Error('Missing messageId');
}
let match = null;
let matchCount = 0;
// 1. Search sentMessages.json
const sentRows = readJson(getSentMessagesPath(paths), []);
if (Array.isArray(sentRows)) {
for (const row of sentRows) {
if (row && row.messageId === id) {
match = { message: row, store: 'sent' };
matchCount++;
if (matchCount > 1) {
throw new Error(`Ambiguous messageId: ${id} found in multiple stores`);
}
}
}
}
// 2. Search all inbox files (early-exit on ambiguity)
const inboxDir = path.join(paths.teamDir, 'inboxes');
let inboxFiles = [];
try {
inboxFiles = fs.readdirSync(inboxDir).filter((f) => f.endsWith('.json'));
} catch {
// No inboxes directory — that's fine.
}
for (const file of inboxFiles) {
const rows = readJson(path.join(inboxDir, file), []);
if (!Array.isArray(rows)) continue;
for (const row of rows) {
if (row && row.messageId === id) {
matchCount++;
if (matchCount > 1) {
throw new Error(`Ambiguous messageId: ${id} found in multiple stores`);
}
match = { message: row, store: `inbox:${file.replace('.json', '')}` };
}
}
}
if (matchCount === 0) {
throw new Error(`Message not found: ${id}`);
}
return match;
}
module.exports = {
appendSentMessage,
lookupMessage,
sendInboxMessage,
};