Compare commits
4 Commits
codex/fix-
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
| 1178146d59 | |||
| 8de9feda62 | |||
| 9a67b160ef | |||
| 831cae1729 |
@@ -1,12 +1,26 @@
|
||||
# Agent Handoff
|
||||
|
||||
Last updated: 2026-07-07
|
||||
Last updated: 2026-07-11
|
||||
|
||||
## Current DAVE Telegram Style Fix
|
||||
|
||||
- Operator feedback from 2026-07-11 screenshots: DAVE still sent large Telegram report blocks, legacy `CRUCIX ROUTINE` fallback alerts, visible report labels such as `Confidence`, and tool/audit traces such as `Werkzeuge: get_security_profile`.
|
||||
- Active branch: `codex/fix-dave-telegram-human-style`.
|
||||
- Intent: DAVE should write like a human Telegram contact: short messages, one thought per message, no visible Markdown/report sections, and no automatic tool traces in normal chat.
|
||||
- Code changes in progress:
|
||||
- `TelegramAlerter.sendConversation(...)` now defaults to 420-character chat chunks and can split long paragraphs by sentence.
|
||||
- Tiered Telegram fallback alerts now use `_formatTieredAlertConversation(...)` and `sendConversation(...)` instead of the legacy Markdown `CRUCIX ROUTINE` block.
|
||||
- Normal Telegram AI answers no longer append tool trace suffixes; `/trace` remains the explicit audit command.
|
||||
- Proactive DAVE and dynamic presence prompts request short human-style Telegram messages; evidence is capped and tool names are hidden from visible chat.
|
||||
- DAVE persona and terminal-agent protocol now require short natural chat answers for proactive/presence responses.
|
||||
- Tests added in `test/telegram-chat.test.mjs` to verify short default chunks and conversational fallback alerts without `CRUCIX ROUTINE`, `Confidence:`, `Direction:`, or visible tool traces.
|
||||
|
||||
## Source Degradation Handling
|
||||
|
||||
- Issue #84 tracks the DAVE diagnosis showing live health degraded because GDELT, OFAC, and EPA timed out; ACLED returned `403`; ADS-B and Reddit lacked credentials; BLS had an invalid configured key.
|
||||
- Code-side fixes in progress on `codex/fix-source-degradation-handling`: GDELT requests are bounded and optional geo enrichment cannot consume the source budget; OFAC uses bounded XML snippet streaming; EPA RadNet uses shorter bounded requests and returns degraded state instead of blocking; BLS retries unauthenticated v1 when `BLS_API_KEY` is rejected and exposes a config warning through source health.
|
||||
- Remaining non-code actions after deploy: ACLED account must accept terms/complete profile/API access for `403`; ADS-B needs `ADSB_API_KEY` or `RAPIDAPI_KEY`; Reddit needs `REDDIT_CLIENT_ID` and `REDDIT_CLIENT_SECRET`; BLS key should be removed or replaced if the v1 fallback is acceptable.
|
||||
- PR #85 merged as `831cae17295730ee4be0848098cbe9f52cabe716`; PR runs 981-982 and production runs 983-985 passed. The registry image was redeployed to Dockge. Live verification after the new sweep showed GDELT and OFAC no longer degraded; `BLS_API_KEY` was cleared in the Dockge `.env`, and BLS no longer appears in degraded health. Remaining degraded sources are ACLED `403`, ADS-B missing key, EPA RadNet table `404`, and Reddit missing OAuth.
|
||||
|
||||
## DAVE Conversational Telegram Delivery And Web Search
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ export class DavePresence {
|
||||
this._save();
|
||||
try {
|
||||
const runtime = this.getRuntime();
|
||||
const prompt = `Decide whether DAVE should initiate a natural, useful conversation with the operator now. This is a dynamic presence evaluation, not a fixed scheduled briefing. Inspect the security profile and current terminal intelligence using only read-only tools, including web_search only when current external corroboration is necessary. Consider freshness, source integrity, recent sweep changes, evidence, scenarios, personal relevance, time since interaction, and whether there is something genuinely useful to say. You may provide a concise relevant warning, situational update, evidence-grounded all-clear, practical suggestion, or one natural question that improves protection or context. Set notify=false when speaking would add noise. Never call mutating tools. Never imply consciousness, feelings, continuous observation, or activity outside this evaluation. If notifying, write one short chat-style message without visible Markdown, headings, tables, or report sections. Local time: ${clock.time}; timezone: ${timezone}; sent today: ${this.state.sentCount}/${this.maxPerDay}; last operator interaction: ${this.state.lastUserInteractionAt || 'unknown'}; last DAVE message: ${this.state.lastSentAt || 'none'}.`;
|
||||
const prompt = `Decide whether DAVE should initiate a natural, useful conversation with the operator now. This is a dynamic presence evaluation, not a fixed scheduled briefing. Inspect the security profile and current terminal intelligence using only read-only tools, including web_search only when current external corroboration is necessary. Consider freshness, source integrity, recent sweep changes, evidence, scenarios, personal relevance, time since interaction, and whether there is something genuinely useful to say. You may provide a concise relevant warning, situational update, evidence-grounded all-clear, practical suggestion, or one natural question that improves protection or context. Set notify=false when speaking would add noise. Never call mutating tools. Never imply consciousness, feelings, continuous observation, or activity outside this evaluation. If notifying, write like a human Telegram contact: one short thought, one optional follow-up question, no visible Markdown, no headings, no lists, no report sections, and keep the final answer below 420 characters when possible. Local time: ${clock.time}; timezone: ${timezone}; sent today: ${this.state.sentCount}/${this.maxPerDay}; last operator interaction: ${this.state.lastUserInteractionAt || 'unknown'}; last DAVE message: ${this.state.lastSentAt || 'none'}.`;
|
||||
const result = await this.agent.run(prompt, {
|
||||
chatId: 'dave-presence',
|
||||
context: String(await this.getContext()).slice(0, 12000),
|
||||
@@ -123,11 +123,11 @@ export class DavePresence {
|
||||
|
||||
const german = profile.language === 'de';
|
||||
const evidence = result.evidence?.length
|
||||
? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 4).map(item => `- ${item}`).join('\n')}`
|
||||
? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 2).map(item => `- ${String(item).slice(0, 120)}`).join('\n')}`
|
||||
: '';
|
||||
const messages = [`[DAVE // ${german ? 'AKTIV' : 'ACTIVE'}]\n${result.answer}`, evidence].filter(Boolean);
|
||||
const sent = typeof this.alerter.sendConversation === 'function'
|
||||
? await this.alerter.sendConversation(messages, { maxChunkChars: 800 })
|
||||
? await this.alerter.sendConversation(messages, { maxChunkChars: 380 })
|
||||
: await this.alerter.sendMessage(messages.join('\n\n'), { parseMode: null });
|
||||
if (!sent?.ok && sent !== true) {
|
||||
this._schedule(now, this.minIntervalMs);
|
||||
|
||||
@@ -257,7 +257,7 @@ ${JSON.stringify(this.registry.describe())}
|
||||
|
||||
PROTOCOL: Output exactly one JSON object, without markdown.
|
||||
Tool call: {"type":"tool_call","tool":"tool_name","arguments":{},"rationale":"short operational reason"}
|
||||
Final: {"type":"final","answer":"concise, natural chat answer in the user's language without visible Markdown syntax","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true' : 'false'},"priority":"routine|priority|flash"}
|
||||
Final: {"type":"final","answer":"short natural chat answer in the user's language, normally under 420 characters for proactive/presence, without visible Markdown syntax or tool traces","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true' : 'false'},"priority":"routine|priority|flash"}
|
||||
${presence
|
||||
? 'In scheduled presence mode, never call mutating tools. Set notify=true for an evidence-grounded briefing, meaningful change, useful all-clear, or one practical check-in question. Set notify=false when available data is too stale or unreliable.'
|
||||
: proactive
|
||||
@@ -277,7 +277,7 @@ Synthesize a direct answer using only the user request, conversation, snapshot,
|
||||
|
||||
You must not request or call another tool. Never output an object with type "tool_call".
|
||||
Output exactly one JSON object without markdown:
|
||||
{"type":"final","answer":"concise, natural chat answer in the user's language without visible Markdown syntax","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true or false' : 'false'},"priority":"routine|priority|flash"}`;
|
||||
{"type":"final","answer":"short natural chat answer in the user's language, normally under 420 characters for proactive/presence, without visible Markdown syntax or tool traces","confidence":"low|medium|high","evidence":["URL or event id"],"notify":${proactive ? 'true or false' : 'false'},"priority":"routine|priority|flash"}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,10 +150,10 @@ export class TelegramAlerter {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
_conversationChunks(text, maxLen = 900) {
|
||||
_conversationChunks(text, maxLen = 420) {
|
||||
const cleaned = stripVisibleMarkdown(String(text || '').trim());
|
||||
if (!cleaned) return [];
|
||||
const limit = Math.max(300, Math.min(1800, Number(maxLen) || 900));
|
||||
const limit = Math.max(160, Math.min(900, Number(maxLen) || 420));
|
||||
const paragraphs = cleaned.split(/\n{2,}/).map(part => part.trim()).filter(Boolean);
|
||||
const chunks = [];
|
||||
let current = '';
|
||||
@@ -163,21 +163,19 @@ export class TelegramAlerter {
|
||||
};
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.length > limit) {
|
||||
flush();
|
||||
for (const sentence of splitSentences(paragraph)) {
|
||||
if ((current.length + sentence.length + 1) > limit) flush();
|
||||
if (sentence.length > limit) {
|
||||
chunks.push(...this._chunkText(sentence, limit).map(part => part.trim()).filter(Boolean));
|
||||
} else {
|
||||
current = current ? `${current} ${sentence}` : sentence;
|
||||
}
|
||||
const sentences = splitSentences(paragraph);
|
||||
const shouldSentenceSplit = paragraph.length > Math.floor(limit * 0.65) || sentences.length > 2;
|
||||
const units = shouldSentenceSplit ? sentences : [paragraph];
|
||||
|
||||
for (const unit of units) {
|
||||
if (unit.length > limit) {
|
||||
flush();
|
||||
chunks.push(...this._chunkText(unit, limit).map(part => part.trim()).filter(Boolean));
|
||||
continue;
|
||||
}
|
||||
flush();
|
||||
continue;
|
||||
if ((current.length + unit.length + 2) > limit) flush();
|
||||
current = current ? `${current}\n\n${unit}` : unit;
|
||||
}
|
||||
if ((current.length + paragraph.length + 2) > limit) flush();
|
||||
current = current ? `${current}\n\n${paragraph}` : paragraph;
|
||||
}
|
||||
flush();
|
||||
return chunks;
|
||||
@@ -195,7 +193,7 @@ export class TelegramAlerter {
|
||||
* Evaluate delta signals with LLM and send tiered alert if warranted.
|
||||
* Uses semantic dedup, rate limiting, and a much richer evaluation prompt.
|
||||
*/
|
||||
async evaluateAndAlert(llmProvider, delta, memory) {
|
||||
async evaluateAndAlert(llmProvider, delta, memory, opts = {}) {
|
||||
if (!this.isConfigured) return false;
|
||||
if (!delta?.summary?.totalChanges) return false;
|
||||
if (this._isMuted()) {
|
||||
@@ -262,9 +260,13 @@ export class TelegramAlerter {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. Format and send tiered alert
|
||||
const message = this._formatTieredAlert(evaluation, delta, tier);
|
||||
const sent = await this.sendAlert(message);
|
||||
// 4. Format and send as short conversational messages.
|
||||
const messages = this._formatTieredAlertConversation(evaluation, delta, tier, opts);
|
||||
const sendResult = await this.sendConversation(messages, {
|
||||
maxChunkChars: opts.maxChunkChars ?? 360,
|
||||
delayMs: opts.delayMs ?? 450,
|
||||
});
|
||||
const sent = Boolean(sendResult?.ok || sendResult === true);
|
||||
|
||||
if (sent) {
|
||||
// Mark signals as alerted with content hashing
|
||||
@@ -963,6 +965,50 @@ Respond with ONLY valid JSON:
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
_formatTieredAlertConversation(evaluation, delta, tier, opts = {}) {
|
||||
const german = normalizeLanguage(opts.language) === 'de';
|
||||
const tierLabel = german
|
||||
? ({ FLASH: 'SOFORT', PRIORITY: 'PRIO', ROUTINE: 'HINWEIS' }[tier] || 'HINWEIS')
|
||||
: ({ FLASH: 'NOW', PRIORITY: 'PRIORITY', ROUTINE: 'NOTE' }[tier] || 'NOTE');
|
||||
const confidence = String(evaluation.confidence || 'MEDIUM').toLowerCase();
|
||||
const direction = String(delta?.summary?.direction || 'mixed').toLowerCase();
|
||||
const headline = compactLine(evaluation.headline || (german ? 'Neue relevante Aenderung erkannt.' : 'New relevant change detected.'));
|
||||
const reason = compactLine(evaluation.reason || '');
|
||||
const signals = Array.isArray(evaluation.signals) ? evaluation.signals.map(compactLine).filter(Boolean).slice(0, 2) : [];
|
||||
|
||||
const messages = [
|
||||
`[DAVE // ${tierLabel}]\n${headline}`,
|
||||
];
|
||||
|
||||
if (reason && reason !== headline) {
|
||||
messages.push(reason);
|
||||
}
|
||||
|
||||
if (tier !== 'ROUTINE' || confidence !== 'low') {
|
||||
messages.push(german
|
||||
? `Einordnung: ${confidence}, Richtung ${direction}.`
|
||||
: `Read: ${confidence} confidence, ${direction} direction.`);
|
||||
}
|
||||
|
||||
if (signals.length) {
|
||||
messages.push(german
|
||||
? `Ich sehe vor allem: ${signals.join('; ')}.`
|
||||
: `Main signals: ${signals.join('; ')}.`);
|
||||
}
|
||||
|
||||
if (evaluation.actionable && evaluation.actionable !== 'Monitor') {
|
||||
messages.push(german
|
||||
? `Naechster sinnvoller Schritt: ${compactLine(evaluation.actionable)}`
|
||||
: `Useful next step: ${compactLine(evaluation.actionable)}`);
|
||||
} else {
|
||||
messages.push(german
|
||||
? 'Soll ich das genauer auseinandernehmen?'
|
||||
: 'Want me to dig into it?');
|
||||
}
|
||||
|
||||
return messages.filter(Boolean);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────
|
||||
@@ -1008,6 +1054,20 @@ function splitSentences(text) {
|
||||
return normalized.match(/[^.!?\n]+[.!?]+(?:\s+|$)|[^.!?\n]+$/g)?.map(item => item.trim()).filter(Boolean) || [normalized];
|
||||
}
|
||||
|
||||
function normalizeLanguage(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'de' || normalized === 'en' ? normalized : null;
|
||||
}
|
||||
|
||||
function compactLine(value, maxLen = 320) {
|
||||
const cleaned = stripVisibleMarkdown(String(value || ''))
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/^[-*]\s*/, '')
|
||||
.trim();
|
||||
if (cleaned.length <= maxLen) return cleaned;
|
||||
return `${cleaned.slice(0, maxLen - 1).trim()}...`;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ ADAPTIVE WRITING STYLE:
|
||||
|
||||
TELEGRAM CHAT DELIVERY:
|
||||
- Write like a live security chat, not like a report. Prefer short, natural paragraphs and direct sentences.
|
||||
- One thought per message. Do not dump every finding at once.
|
||||
- Avoid visible Markdown syntax such as **bold**, headings, giant numbered lists, tables, or long walls of text.
|
||||
- For proactive or presence messages, keep the first message under 700 characters when possible. Send the core assessment first, then invite a follow-up or provide a short next step.
|
||||
- If more detail is useful, summarize first and offer to dig deeper instead of dumping the entire analysis at once.`;
|
||||
- For proactive or presence messages, keep the first answer below 420 characters when possible. Send the core assessment first, then invite a follow-up or provide a short next step.
|
||||
- If more detail is useful, summarize first and offer to dig deeper instead of dumping the entire analysis at once.
|
||||
- Do not append internal tool names, protocol traces, or audit notes to normal chat answers. The operator can ask for traces separately.`;
|
||||
|
||||
export function normalizePreferredLanguage(value) {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
|
||||
21
server.mjs
21
server.mjs
@@ -212,9 +212,6 @@ if (telegramAlerter.isConfigured) {
|
||||
}
|
||||
const chatId = msg?.chat?.id || config.telegram.chatId;
|
||||
const result = await telegramChatAssistant.replyDetailed(question, { chatId });
|
||||
const german = securityProfileStore.getAgentProfile()?.language === 'de';
|
||||
const tools = [...new Set((result.trace || []).filter(item => item.status === 'ok').map(item => item.tool))];
|
||||
const traceSuffix = tools.length ? `\n\n${german ? 'Verwendete Werkzeuge' : 'Tools used'}: ${tools.join(', ')}` : '';
|
||||
if (result.pendingAction) {
|
||||
const action = result.pendingAction;
|
||||
return {
|
||||
@@ -228,7 +225,7 @@ if (telegramAlerter.isConfigured) {
|
||||
},
|
||||
};
|
||||
}
|
||||
return { text: `${result.answer}${traceSuffix}`, parseMode: null, conversation: true, maxChunkChars: 900 };
|
||||
return { text: result.answer, parseMode: null, conversation: true, maxChunkChars: 380 };
|
||||
};
|
||||
|
||||
telegramAlerter.onMessage((text, msg) => {
|
||||
@@ -872,15 +869,16 @@ async function runSweepCycle() {
|
||||
// 6. Alert evaluation — Telegram + Discord (LLM with rule-based fallback, multi-tier, semantic dedup)
|
||||
if (delta?.summary?.totalChanges > 0) {
|
||||
if (telegramAlerter.isConfigured) {
|
||||
const telegramAlertOpts = { language: securityProfileStore.getAgentProfile()?.language || null };
|
||||
if (config.telegram.agentEnabled && config.telegram.agentProactiveEnabled && shouldRunProactiveAgent(delta)) {
|
||||
runProactiveAgent(synthesized, delta).catch(err => {
|
||||
console.error('[Agent] Proactive analysis failed, using rule fallback:', err.message);
|
||||
telegramAlerter.evaluateAndAlert(null, delta, memory).catch(fallbackError => {
|
||||
telegramAlerter.evaluateAndAlert(null, delta, memory, telegramAlertOpts).catch(fallbackError => {
|
||||
console.error('[Crucix] Telegram alert fallback error:', fallbackError.message);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
telegramAlerter.evaluateAndAlert(llmProvider, delta, memory).catch(err => {
|
||||
telegramAlerter.evaluateAndAlert(llmProvider, delta, memory, telegramAlertOpts).catch(err => {
|
||||
console.error('[Crucix] Telegram alert error:', err.message);
|
||||
});
|
||||
}
|
||||
@@ -927,7 +925,7 @@ function shouldRunProactiveAgent(delta) {
|
||||
|
||||
async function runProactiveAgent(data, delta) {
|
||||
if (telegramAlerter.getMuteStatus().muted) return false;
|
||||
const prompt = `Evaluate the latest sweep as the operator's Security Manager. Use the security profile when available to assess geographic and personal relevance, dependencies, urgency, and preferred language. Cross-check material changes with source health, evidence, scenarios, memory, predictions, and web_search when current external corroboration is needed. Distinguish verified facts from inference. Do not call mutating tools. If you notify, write like a live Telegram chat: short first message, no visible Markdown, no report wall. Delta summary: ${JSON.stringify(delta?.summary || {})}`;
|
||||
const prompt = `Evaluate the latest sweep as the operator's Security Manager. Use the security profile when available to assess geographic and personal relevance, dependencies, urgency, and preferred language. Cross-check material changes with source health, evidence, scenarios, memory, predictions, and web_search when current external corroboration is needed. Distinguish verified facts from inference. Do not call mutating tools. If you notify, write like a human Telegram contact: one short thought, one optional follow-up question, no visible Markdown, no headings, no lists, no report wall, no tool names, and keep the final answer below 420 characters when possible. Delta summary: ${JSON.stringify(delta?.summary || {})}`;
|
||||
const profile = securityProfileStore.getAgentProfile();
|
||||
const result = await terminalAgent.analyzeProactively(prompt, {
|
||||
context: buildTelegramChatContext(data, buildHealth()),
|
||||
@@ -937,25 +935,22 @@ async function runProactiveAgent(data, delta) {
|
||||
if (result.pendingAction) return false;
|
||||
const policy = evaluateSecurityAlertPolicy(result, profile);
|
||||
if (!profile && !result.notify) {
|
||||
return telegramAlerter.evaluateAndAlert(null, delta, memory);
|
||||
return telegramAlerter.evaluateAndAlert(null, delta, memory, { language: profile?.language || null });
|
||||
}
|
||||
if (!policy.send) {
|
||||
console.log(`[Security Manager] Proactive notification suppressed: ${policy.reason}`);
|
||||
return false;
|
||||
}
|
||||
const german = profile?.language === 'de';
|
||||
const evidence = result.evidence?.length ? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.map(item => `- ${item}`).join('\n')}` : '';
|
||||
const tools = [...new Set((result.trace || []).filter(item => item.status === 'ok').map(item => item.tool))];
|
||||
const trace = tools.length ? `${german ? 'Werkzeuge' : 'Tools'}: ${tools.join(', ')}` : '';
|
||||
const evidence = result.evidence?.length ? `${german ? 'Belege' : 'Evidence'}:\n${result.evidence.slice(0, 2).map(item => `- ${String(item).slice(0, 120)}`).join('\n')}` : '';
|
||||
const priority = german
|
||||
? ({ routine: 'HINWEIS', priority: 'PRIORITÄT', flash: 'SOFORT' }[result.priority] || 'HINWEIS')
|
||||
: String(result.priority || 'routine').toUpperCase();
|
||||
const messages = [
|
||||
`[DAVE // ${priority}]\n${result.answer}`,
|
||||
evidence,
|
||||
trace,
|
||||
].filter(Boolean);
|
||||
const sent = await telegramAlerter.sendConversation(messages, { maxChunkChars: 800 });
|
||||
const sent = await telegramAlerter.sendConversation(messages, { maxChunkChars: 380 });
|
||||
return sent.ok;
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,74 @@ test('Telegram conversation delivery strips visible markdown and splits large AI
|
||||
assert.equal('reply_to_message_id' in sent[1].body, false);
|
||||
});
|
||||
|
||||
test('Telegram conversation defaults to short human-sized chat chunks', async () => {
|
||||
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
|
||||
const requests = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
|
||||
return { ok: true, json: async () => ({ ok: true, result: { message_id: requests.length } }) };
|
||||
};
|
||||
|
||||
try {
|
||||
const text = [
|
||||
'Das ist die erste kurze Einschaetzung. Sie soll separat lesbar bleiben und nicht wie ein Bericht wirken.',
|
||||
'Hier kommt mehr Kontext, aber auch dieser Kontext bleibt in natuerlichen Chat-Haeppchen. '.repeat(8),
|
||||
'Soll ich die Details danach sauber aufdrillen?',
|
||||
].join('\n\n');
|
||||
const result = await alerter.sendConversation(text, { delayMs: 0 });
|
||||
assert.equal(result.ok, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
const sent = requests.filter(request => request.url.includes('/sendMessage'));
|
||||
assert.ok(sent.length >= 3);
|
||||
for (const message of sent) {
|
||||
assert.ok(message.body.text.length <= 420, `message too long: ${message.body.text.length}`);
|
||||
assert.equal('parse_mode' in message.body, false);
|
||||
}
|
||||
});
|
||||
|
||||
test('Telegram tiered fallback alert is sent as conversational DAVE messages', async () => {
|
||||
const alerter = new TelegramAlerter({ botToken: 'test-token', chatId: '42' });
|
||||
const requests = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, options = {}) => {
|
||||
requests.push({ url, body: options.body ? JSON.parse(options.body) : null });
|
||||
return { ok: true, json: async () => ({ ok: true, result: { message_id: requests.length } }) };
|
||||
};
|
||||
const memory = {
|
||||
getAlertedSignals: () => ({}),
|
||||
markAsAlerted: () => {},
|
||||
};
|
||||
const delta = {
|
||||
summary: { totalChanges: 1, criticalChanges: 1, direction: 'mixed' },
|
||||
signals: {
|
||||
new: [{
|
||||
key: 'source_degradation',
|
||||
severity: 'critical',
|
||||
label: '7 additional sources failing',
|
||||
direction: 'up',
|
||||
}],
|
||||
escalated: [],
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const sent = await alerter.evaluateAndAlert(null, delta, memory, { language: 'de', delayMs: 0 });
|
||||
assert.equal(sent, true);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
|
||||
const texts = requests.filter(request => request.url.includes('/sendMessage')).map(request => request.body.text);
|
||||
assert.ok(texts.length >= 2);
|
||||
assert.match(texts[0], /^\[DAVE \/\/ HINWEIS\]/);
|
||||
assert.doesNotMatch(texts.join('\n'), /CRUCIX ROUTINE|Confidence:|Direction:|Cross-correlation:|Werkzeuge|Tools used/);
|
||||
for (const text of texts) assert.ok(text.length <= 380, `alert chunk too long: ${text.length}`);
|
||||
});
|
||||
|
||||
test('Telegram OSINT extraction excludes private AI chat messages', () => {
|
||||
const messages = extractBotChannelMessages([
|
||||
{ update_id: 1, message: { text: 'private question', chat: { id: 42, type: 'private' } } },
|
||||
|
||||
Reference in New Issue
Block a user