fix: keep notification summaries in plain text

- Force notification summaries to avoid markdown formatting
- Normalize generated notification text before system display
- Cover plain-text notification formatting with tests
This commit is contained in:
Bohdan Triapitsyn
2026-04-06 17:40:20 +03:00
parent 9254ec0783
commit f884919165
5 changed files with 40 additions and 4 deletions
@@ -1,7 +1,7 @@
# Notifications Module Documentation
## Purpose
This module provides notification message preparation utilities for the web server runtime, including text truncation and optional message summarization for system notifications.
This module provides notification message preparation utilities for the web server runtime, including text truncation, plain-text normalization, and optional message summarization for system notifications.
## Entrypoints and structure
- `packages/web/server/lib/notifications/index.js`: public entrypoint imported by `packages/web/server/index.js`.
@@ -107,6 +107,7 @@ The `settings` parameter for `prepareNotificationLastMessage` supports:
- Returns empty string for empty/null message.
- Returns truncated original message if summarization disabled, message under threshold, or summarization fails.
- Returns truncated summary if summarization succeeds and returns non-empty string.
- Normalizes markdown-like formatting to plain text before truncation.
- Always applies `maxLastMessageLength` truncation to final result.
## Notes for contributors
@@ -9,6 +9,26 @@ const resolvePositiveNumber = (value, fallback) => {
return value;
};
const normalizeNotificationPlainText = (text) => {
if (typeof text !== 'string') {
return '';
}
return text
.replace(/```[\s\S]*?```/g, ' ')
.replace(/`([^`]*)`/g, '$1')
.replace(/^[\t ]*[-*+]\s+/gm, '')
.replace(/^#{1,6}\s+/gm, '')
.replace(/\*\*(.*?)\*\*/g, '$1')
.replace(/__(.*?)__/g, '$1')
.replace(/\*(.*?)\*/g, '$1')
.replace(/_(.*?)_/g, '$1')
.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
.replace(/\s*\n\s*/g, ' ')
.replace(/\s+/g, ' ')
.trim();
};
export const truncateNotificationText = (text, maxLength = DEFAULT_NOTIFICATION_MESSAGE_MAX_LENGTH) => {
if (typeof text !== 'string') {
return '';
@@ -45,5 +65,6 @@ export const prepareNotificationLastMessage = async ({ message, settings, summar
}
}
return truncateNotificationText(messageForNotification, maxLastMessageLength);
const plainTextMessage = normalizeNotificationPlainText(messageForNotification) || normalizeNotificationPlainText(originalMessage);
return truncateNotificationText(plainTextMessage, maxLastMessageLength);
};
@@ -56,4 +56,19 @@ describe('notification message helpers', () => {
expect(result).toBe('short summary');
});
it('normalizes markdown summary to plain text', async () => {
const result = await prepareNotificationLastMessage({
message: '0123456789',
summarize: async () => "**Committed.**\n\n- Commit: `85924b9d`\n- Message: `fix desktop notifications`",
settings: {
summarizeLastMessage: true,
summaryThreshold: 5,
summaryLength: 80,
maxLastMessageLength: 200,
},
});
expect(result).toBe('Committed. Commit: 85924b9d Message: fix desktop notifications');
});
});
@@ -134,7 +134,6 @@ export const createNotificationTriggerRuntime = (deps) => {
}
const sessionId = extractSessionIdFromPayload(payload);
if (payload.type === 'message.updated') {
const info = payload.properties?.info;
if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId) {
@@ -138,7 +138,7 @@ export const createNotificationTemplateRuntime = (deps) => {
if (!text || typeof text !== 'string' || text.trim().length === 0) return text;
try {
const prompt = `Summarize the following text in approximately ${targetLength} characters. Be concise and capture the key point. Output ONLY the summary text, nothing else.\n\nText:\n${text}`;
const prompt = `Summarize the following text in approximately ${targetLength} characters. Be concise and capture the key point. Output plain text only. Do not use markdown, bullets, headings, code fences, backticks, or quotes. Output only the summary text.\n\nText:\n${text}`;
const completionTimeout = createTimeoutSignal(15000);
let response;