agent-ecosystem/agent-teams-controller/src/internal/maintenance.js
iliya a29d8403d6 refactor: update comment formatting and notification handling in maintenance and tasks
- Modified comment notification formatting in tasks.js to enhance clarity by using bold text for task references.
- Updated the automated comment notification detection in maintenance.js to align with the new formatting.
- Adjusted test cases to reflect the updated comment structure for consistency in notifications.
2026-03-14 22:10:16 +02:00

170 lines
4.8 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const kanbanStore = require('./kanbanStore.js');
const taskStore = require('./taskStore.js');
function listInboxNames(paths) {
const inboxDir = path.join(paths.teamDir, 'inboxes');
let entries = [];
try {
entries = fs.readdirSync(inboxDir);
} catch (error) {
if (error && error.code === 'ENOENT') {
return [];
}
throw error;
}
return entries
.filter((name) => name.endsWith('.json') && !name.startsWith('.'))
.map((name) => name.replace(/\.json$/, ''));
}
function readInboxMessages(paths) {
const messages = [];
for (const member of listInboxNames(paths)) {
const inboxPath = path.join(paths.teamDir, 'inboxes', `${member}.json`);
let parsed;
try {
parsed = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
} catch {
continue;
}
if (!Array.isArray(parsed)) {
continue;
}
for (const item of parsed) {
if (!item || typeof item !== 'object') {
continue;
}
if (
typeof item.from !== 'string' ||
typeof item.text !== 'string' ||
typeof item.timestamp !== 'string'
) {
continue;
}
messages.push({
from: item.from,
to: typeof item.to === 'string' ? item.to : member,
text: item.text,
timestamp: item.timestamp,
summary: typeof item.summary === 'string' ? item.summary : undefined,
messageId: typeof item.messageId === 'string' ? item.messageId : undefined,
source: typeof item.source === 'string' ? item.source : undefined,
});
}
}
messages.sort((a, b) => {
const bt = Date.parse(b.timestamp);
const at = Date.parse(a.timestamp);
if (Number.isNaN(bt) || Number.isNaN(at)) {
return 0;
}
return bt - at;
});
return messages;
}
function isAutomatedCommentNotification(message) {
const summary = typeof message.summary === 'string' ? message.summary : '';
if (!/^Comment on #[A-Za-z0-9-]+/.test(summary)) return false;
const text = typeof message.text === 'string' ? message.text : '';
if (!text) return false;
if (text.includes('Reply to this comment using:')) return true;
if (text.startsWith('**Comment on task')) return true;
if (text.startsWith('New comment from user on your task #')) return true;
return false;
}
function syncLinkedComments(paths, tasks, messages) {
const taskIdPattern = /#([A-Za-z0-9-]+)/g;
const tasksById = new Map();
const processedTexts = new Set();
let linkedCommentsCreated = 0;
for (const task of tasks) {
tasksById.set(task.id, task);
if (task.displayId) {
tasksById.set(task.displayId, task);
}
}
for (const message of messages) {
if (!message.messageId || !message.summary || message.from === 'user') continue;
if (message.source === 'lead_session' || message.source === 'lead_process') continue;
if (message.source === 'system_notification') continue;
if (isAutomatedCommentNotification(message)) continue;
const textKey = `${message.from}\0${message.text}`;
if (processedTexts.has(textKey)) continue;
processedTexts.add(textKey);
const taskRefs = new Set();
for (const match of message.summary.matchAll(taskIdPattern)) {
taskRefs.add(match[1]);
}
for (const taskRef of taskRefs) {
const task = tasksById.get(taskRef);
if (!task) continue;
const commentId = `msg-${message.messageId}`;
const existingComments = Array.isArray(task.comments) ? task.comments : [];
if (existingComments.some((comment) => comment.id === commentId)) {
continue;
}
try {
taskStore.addTaskComment(paths, task.id, message.text, {
id: commentId,
author: message.from,
createdAt: message.timestamp,
});
linkedCommentsCreated += 1;
} catch {
// Best-effort: reconcile should not fail on individual comment sync writes.
}
}
}
return linkedCommentsCreated;
}
function reconcileArtifacts(context, options = {}) {
const garbageCollectKanban = options.garbageCollectKanban !== false;
const shouldSyncLinkedComments = options.syncLinkedComments !== false;
const tasks = taskStore.listTasks(context.paths);
const gcResult = garbageCollectKanban
? kanbanStore.garbageCollect(
context.paths,
context.teamName,
new Set(tasks.map((task) => task.id))
)
: { staleKanbanEntriesRemoved: 0, staleColumnOrderRefsRemoved: 0 };
const linkedCommentsCreated = shouldSyncLinkedComments
? syncLinkedComments(context.paths, tasks, readInboxMessages(context.paths))
: 0;
return {
staleKanbanEntriesRemoved: gcResult.staleKanbanEntriesRemoved,
staleColumnOrderRefsRemoved: gcResult.staleColumnOrderRefsRemoved,
linkedCommentsCreated,
};
}
module.exports = {
reconcileArtifacts,
};