2026 03 13 S211.4 Vitto Conversational Intelligence
Documentation for Plans
S211.4 — Vitto Conversational Intelligence: WhatsApp UX Fixes + Anticipatory Improvements
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Fix 6 confirmed WhatsApp UX issues (duplicate messages, "What next?" spam, can't query own alerts, progress noise, persona problems, broken quick reply actions) and ship 4 anticipatory improvements that make Vitto feel genuinely intelligent rather than robotic.
Architecture: Changes span three layers: (1) ChannelProcessingService message flow — consolidate ACK + progress + quick reply into a single coordinated pipeline instead of 3 independent fire-and-forget paths; (2) Agent tool system — add query_system_status tool so Vitto can access its own health/alert/digest data when users ask; (3) Response enrichment — make quick replies contextual and embed them in the response message instead of sending as a separate bubble.
Tech Stack: NestJS (backend-vitto-api), Jest, TypeScript
Commit tag convention: All commits use [S211.4] — implementor substitutes with actual sub-session number per SAGP v1.2.
Spec source: Real WhatsApp conversation from 2026-03-13 showing all 6 issues. Root cause analysis completed with file:line references.
Problem Summary (from real conversation)
| # | Problem | Root Cause | File:Line |
|---|---|---|---|
| 1 | Duplicate greeting — "Mira mijo, what's good?" sent TWICE | ACK message (line 242-251) + LLM response (line 308) = 2 separate messages for fast tier |
channel-processing.service.ts:242 |
| 2 | "What next?" after EVERY response — separate bubble with generic buttons | generateQuickReplies() always returns non-empty for >20 chars; sent as separate Twilio Content API message |
channel-processing.service.ts:323 + channel-response-enricher.service.ts:65 |
| 3 | Vitto can't see its own alerts — "I don't have anything in my memory" when asked about 72 critical alerts it just reported in the digest | No tool in channelSafeTools queries ProactiveMonitorService.getAlerts(); memory tools search conversation history, not alert DB |
channel-processing.service.ts:59-68 |
| 4 | 3 progress messages — "Hmm let me think..." + "Recalling..." + "Digging through memory..." | ACK (1 message) + WhatsAppProgressEmitter (up to 2 messages) = 3 total; no shared counter | channel-processing.service.ts:242 + whatsapp-progress-emitter.ts:28 |
| 5 | "Mira mijo" cringe persona — culturally inappropriate casual Spanish | Friendly persona + whatsapp: { tone: 'casual' } + humorLevel: 2 + no language guard |
persona.service.ts:207-209 |
| 6 | "Go deeper" button does nothing useful — re-runs same loop, finds nothing new | Quick reply "Go deeper" triggers same agent with same tools; no deeper analysis capability | channel-response-enricher.service.ts:47 |
Anticipated Issues (not yet reported but will surface)
| # | Problem | Why It Will Surface | Confidence |
|---|---|---|---|
| A1 | Greeting wastes an agent loop iteration — "Hi" runs through full agent loop with 18 tools | Fast tier still invokes LLM + tool availability; wastes ~500 tokens per greeting | Certain |
| A2 | Daily digest is a plain text wall — no structure, no action buttons | Users can't tap to investigate specific metrics | Certain |
| A3 | No way to acknowledge/resolve alerts from WhatsApp | "Acknowledge" button from S211.1 rich cards has no handler | Certain |
| A4 | Vitto forgets what it just sent — user refers to digest content and Vitto has no context | Digest is generated by cron and dispatched; not stored in conversation memory | Likely |
File Structure
New Files
| File | Responsibility |
|---|---|
APIs/backend-vitto-api/src/app/tools/definitions/query-system-status.tool.ts |
Tool definition: query_system_status — queries alerts, health checks, digest summary, cost stats |
APIs/backend-vitto-api/src/app/tools/__tests__/query-system-status.spec.ts |
Tests for the new tool |
APIs/backend-vitto-api/src/app/engine/__tests__/whatsapp-message-flow.spec.ts |
Integration tests for the consolidated WhatsApp message flow |
Modified Files
| File | Change |
|---|---|
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts |
Major: Consolidate ACK + progress + quick reply into coordinated flow; skip ACK for fast tier; embed quick replies in response body instead of separate message |
APIs/backend-vitto-api/src/app/engine/whatsapp-progress-emitter.ts |
Count ACK as first progress message; reduce maxProgressMessages to 1 when ACK was sent |
APIs/backend-vitto-api/src/app/engine/channel-response-enricher.service.ts |
Suppress quick replies for greetings and short responses; make "Go deeper" trigger a contextual follow-up |
APIs/backend-vitto-api/src/app/engine/vitto-expression.service.ts |
Remove ACK for fast tier; add shouldSendAck() gate |
APIs/backend-vitto-api/src/app/engine/persona.service.ts |
Add language boundary to Friendly persona; reduce WhatsApp humorLevel from 2→1 |
APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts |
Register query_system_status tool |
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts:59-68 |
Add query_system_status to channelSafeTools |
Chunk 1: Fix Duplicate Messages + Progress Noise (Issues 1, 4)
Task 1: Eliminate ACK Message for Fast-Tier (Greetings)
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts:242-252
The ACK at line 242 fires for ALL modelTier !== 'fast' messages. But the classifier routes greetings AS fast tier (line 148 of request-classifier), so greetings should NOT get an ACK. The real issue: the condition is correct for greetings, but the ACK text and the LLM greeting response look identical for short inputs, creating perceived duplicates.
The fix has two parts:
- For
fasttier: no ACK (already correct —modelTier !== 'fast'guards it). Verify this is working. - For
balancedtier: coordinate ACK with progress emitter so total pre-response messages ≤ 1.
- Step 1: Read the current ACK flow and verify the fast-tier guard
Read channel-processing.service.ts lines 240-252. Confirm the classification.modelTier !== 'fast' guard exists. The duplicate greeting issue may be caused by the classifier NOT routing "Hi Vitto" as fast tier (verify: does "Hi Vitto" match the greeting pattern in the classifier when the message is longer than 30 chars?).
Check: request-classifier.service.ts:139 — the greeting pattern check is gated by trimmed.length <= 30. "Hi Vitto" is 8 chars — should pass. But verify by reading the exact code path.
- Step 2: Make ACK and progress emitter share a message budget
In channel-processing.service.ts, the ACK sends at line 246 and the WhatsAppProgressEmitter is created at line 238 with its own counter. Fix: pass the ACK state to the emitter.
Change the emitter creation to account for the ACK:
// After the ACK block (line 252), pass ackSent state to emitter:
if (channel === 'whatsapp') {
const { WhatsAppProgressEmitter } = await import('./whatsapp-progress-emitter');
const ackWasSent = (classification.modelTier !== 'fast') && !!ack;
progressEmitter = new WhatsAppProgressEmitter(
this.channelGateway, ctx.channelId as string, this.expression,
{ maxProgressMessages: ackWasSent ? 1 : 2 },
);
}
This ensures: ACK sent? → only 1 more progress message allowed. No ACK? → up to 2 progress messages. Total pre-response messages ≤ 2.
- Step 3: Move emitter creation AFTER the ACK block
Currently the emitter is created at line 237 (before ACK) and the ACK is at line 242. Reorder so the emitter is created AFTER the ACK, allowing us to pass the ackWasSent flag.
- Step 4: Commit
git add APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts
git commit -m "[S211.4] fix: coordinate ACK and progress emitter to cap pre-response messages at 2"
Task 2: Investigate and Fix Actual Duplicate Greeting
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts
The user reported "Mira mijo, what's good?" sent TWICE at 7:49:45 and 7:49:46. If the classifier correctly routes "Hi Vitto" as fast tier (no ACK), the duplicate may have a different root cause:
Hypothesis A: Twilio webhook fires twice for the same message (known Twilio race condition with WebSocket status callbacks).
Hypothesis B: The controller submitTask() is called twice.
Hypothesis C: The ACK IS firing because the classifier doesn't classify it as fast.
- Step 1: Add idempotency guard in the controller webhook
In vitto.controller.ts around the WhatsApp webhook handler, add a dedup check using MessageSid:
// At the top of handleWhatsAppInbound or whatsappWebhook:
private readonly recentMessageSids = new Map<string, number>();
// Inside the handler:
const messageSid = body.MessageSid || body.SmsMessageSid;
if (messageSid && this.recentMessageSids.has(messageSid)) {
this.logger.debug(`Duplicate webhook ignored: ${messageSid}`);
return; // Already processing this message
}
if (messageSid) {
this.recentMessageSids.set(messageSid, Date.now());
// Prune old entries
if (this.recentMessageSids.size > 50) {
const cutoff = Date.now() - 60000;
for (const [sid, ts] of this.recentMessageSids) {
if (ts < cutoff) this.recentMessageSids.delete(sid);
}
}
}
- Step 2: Test by examining logs
After deploying, send "Hi Vitto" and check backend logs for:
[ChannelProcessing] Channel intent: command (confidence: X, tier: fast)— confirms fast tierExactly ONE
routeTaskResponselog line (not two)Step 3: Commit
git add APIs/backend-vitto-api/src/app/engine/vitto.controller.ts
git commit -m "[S211.4] fix: add MessageSid dedup guard to prevent duplicate WhatsApp webhook processing"
Chunk 2: Fix "What Next?" Spam + Make Quick Replies Contextual (Issues 2, 6)
Task 3: Embed Quick Replies in Response Instead of Separate Message
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts:322-333 - Modify:
APIs/backend-vitto-api/src/app/engine/channel-response-enricher.service.ts
Currently quick replies are sent as a SEPARATE Twilio Content API message after the main response. This creates 2 bubbles per response. Fix: when the RichMessageComposerService already composed a rich message with quick reply buttons, skip the separate enricher message entirely. When it didn't, embed the quick reply in the LAST message chunk instead of sending it separately.
- Step 1: Suppress separate quick reply when rich message already has buttons
The guard !richOutput?.rendered?.alreadySent at line 323 already handles WhatsApp adapter cases. But for responses that DON'T trigger the composer (plain text responses), the enricher still fires.
Fix: suppress the separate quick reply for ALL greetings and short responses:
// channel-processing.service.ts, replace lines 322-333:
// Quick reply buttons — only for substantive responses, skip greetings/short
const isSubstantiveResponse = result.output && result.output.length > 100
&& classification.modelTier !== 'fast';
if (channel === 'whatsapp' && ctx.channelId && isSubstantiveResponse
&& !richOutput?.rendered?.alreadySent) {
const suggestions = this.responseEnricher.generateQuickReplies(
result.output, classification,
result.toolCalls.map((t: { toolName: string }) => t.toolName),
);
if (suggestions.length > 0) {
this.channelGateway.sendWhatsAppQuickReply(
ctx.channelId as string,
// Use the LAST sentence of the response as the quick reply body
// instead of generic "What next?"
this.extractLastSentence(result.output),
suggestions.slice(0, 3),
).catch(() => {});
}
}
Add the helper:
private extractLastSentence(text: string): string {
const sentences = text.trim().split(/(?<=[.!?])\s+/);
const last = sentences[sentences.length - 1] || text;
return last.substring(0, 200); // WhatsApp body limit for quick reply
}
- Step 2: Make enricher suppress for trivial responses
In channel-response-enricher.service.ts, raise the minimum threshold and suppress for greetings:
// Line 29: raise from 20 to 80
if (trimmed.length < 80) return [];
// Add after line 29: suppress for greeting-like responses
if (/^(?:hey|hi|hello|what'?s\s+up|good\s+morning)/i.test(trimmed)) return [];
- Step 3: Make "Go deeper" actually useful
Change the tool-used-but-not-complete suggestions (line 47) from generic to contextual:
// Line 42-48: replace generic "Go deeper" with action-oriented suggestions
if (toolCallsUsed.length > 0) {
if (toolCallsUsed.includes('task_complete') || /done|completed|finished|ready/i.test(lower)) {
return ['Thanks!', 'Show details', 'Next task'];
}
if (toolCallsUsed.includes('recall_memory') || toolCallsUsed.includes('recall_vitto_memory')) {
return ['Search more', 'Check alerts', 'New topic'];
}
return ['Continue', 'More context', 'New topic'];
}
- Step 4: Commit
git add APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts \
APIs/backend-vitto-api/src/app/engine/channel-response-enricher.service.ts
git commit -m "[S211.4] fix: suppress quick reply spam for greetings, embed in response body, contextual buttons"
Chunk 3: Add query_system_status Tool (Issue 3 — CRITICAL)
This is the most impactful fix. Vitto generates a daily digest with alert counts but can't answer questions about them because no tool queries the proactive alert system.
Task 4: Create query_system_status Tool Definition
Files:
- Create:
APIs/backend-vitto-api/src/app/tools/definitions/query-system-status.tool.ts - Create:
APIs/backend-vitto-api/src/app/tools/__tests__/query-system-status.spec.ts
The tool queries: unresolved alerts (with severity/checkName breakdown), health check status, daily digest summary, and cost stats. It wraps ProactiveMonitorService.getAlerts(), .getAlertStats(), .getStatus(), and CostTrackerService.getSpendingStats().
- Step 1: Write the tool definition
// APIs/backend-vitto-api/src/app/tools/definitions/query-system-status.tool.ts
import { Injectable, Logger, Optional } from '@nestjs/common';
import { ProactiveMonitorService } from '../../proactive/proactive-monitor.service';
/**
* S211.4: Tool that gives Vitto access to its own system health data.
* Without this, Vitto generates daily digests with alert counts but can't
* answer when users ask "what are those 72 alerts?"
*/
@Injectable()
export class QuerySystemStatusTool {
private readonly logger = new Logger('QuerySystemStatus');
constructor(
@Optional() private readonly proactiveMonitor?: ProactiveMonitorService,
) {}
getDefinition() {
return {
name: 'query_system_status',
description: 'Query Vitto\'s own system health: unresolved alerts, health check status, alert statistics. Use when users ask about alerts, system health, monitoring, or reference numbers from the daily digest.',
input_schema: {
type: 'object' as const,
properties: {
query: {
type: 'string',
enum: ['alerts', 'health_checks', 'alert_stats', 'all'],
description: 'What to query: "alerts" for unresolved alerts list, "health_checks" for current check status, "alert_stats" for severity/check breakdown, "all" for complete picture',
},
severity: {
type: 'string',
enum: ['critical', 'warning', 'info'],
description: 'Optional: filter alerts by severity',
},
limit: {
type: 'number',
description: 'Max alerts to return (default 20)',
},
},
required: ['query'],
},
};
}
async execute(input: {
query: string;
severity?: string;
limit?: number;
}): Promise<string> {
if (!this.proactiveMonitor) {
return 'System monitoring is not available in this environment.';
}
try {
const sections: string[] = [];
if (input.query === 'alerts' || input.query === 'all') {
const alerts = await this.proactiveMonitor.getAlerts({
resolved: false,
severity: input.severity,
limit: input.limit || 20,
});
if (alerts.length === 0) {
sections.push('No unresolved alerts.');
} else {
sections.push(`Unresolved alerts (${alerts.length}):`);
for (const a of alerts) {
sections.push(`- [${a.severity}] ${a.checkName}: ${a.message} (since ${a.createdAt?.toISOString?.() || 'unknown'})${a.suggestedAction ? ` → ${a.suggestedAction}` : ''}`);
}
}
}
if (input.query === 'health_checks' || input.query === 'all') {
const checks = this.proactiveMonitor.getStatus();
sections.push(`\nHealth checks (${checks.length} registered):`);
for (const c of checks) {
const status = c.lastRun?.healthy ? 'healthy' : c.lastRun ? 'UNHEALTHY' : 'not yet run';
sections.push(`- ${c.name} (${c.severity}): ${status}${c.lastRun ? ` — last checked ${c.lastRun.at.toISOString()}` : ''}`);
}
}
if (input.query === 'alert_stats' || input.query === 'all') {
const stats = await this.proactiveMonitor.getAlertStats();
sections.push(`\nAlert statistics:`);
sections.push(`Total: ${stats.total} | Unresolved: ${stats.unresolved} | Resolved: ${stats.resolved}`);
sections.push(`By severity: ${Object.entries(stats.bySeverity).map(([k, v]) => `${k}=${v}`).join(', ')}`);
if (Object.keys(stats.byCheck).length > 0) {
sections.push(`Top checks with unresolved: ${Object.entries(stats.byCheck).map(([k, v]) => `${k}(${v})`).join(', ')}`);
}
}
return sections.join('\n') || 'No data available.';
} catch (err) {
this.logger.warn(`query_system_status failed: ${(err as Error).message}`);
return `Error querying system status: ${(err as Error).message}`;
}
}
}
- Step 2: Write test
// APIs/backend-vitto-api/src/app/tools/__tests__/query-system-status.spec.ts
import { QuerySystemStatusTool } from '../definitions/query-system-status.tool';
describe('QuerySystemStatusTool', () => {
let tool: QuerySystemStatusTool;
let mockProactive: any;
beforeEach(() => {
mockProactive = {
getAlerts: jest.fn().mockResolvedValue([
{ severity: 'critical', checkName: 'api-health', message: 'backend-docs-api unreachable', createdAt: new Date(), suggestedAction: 'Restart service' },
{ severity: 'critical', checkName: 'api-health', message: 'backend-docs-api unreachable', createdAt: new Date() },
]),
getStatus: jest.fn().mockReturnValue([
{ name: 'api-health', severity: 'critical', lastRun: { at: new Date(), healthy: false } },
]),
getAlertStats: jest.fn().mockResolvedValue({
total: 100, unresolved: 72, resolved: 28,
bySeverity: { critical: 72, warning: 20, info: 8 },
byCheck: { 'api-health': 72 },
}),
};
tool = new QuerySystemStatusTool(mockProactive);
});
it('should return unresolved alerts', async () => {
const result = await tool.execute({ query: 'alerts' });
expect(result).toContain('Unresolved alerts (2)');
expect(result).toContain('api-health');
expect(result).toContain('Restart service');
});
it('should return alert statistics', async () => {
const result = await tool.execute({ query: 'alert_stats' });
expect(result).toContain('Unresolved: 72');
expect(result).toContain('critical=72');
});
it('should return health check status', async () => {
const result = await tool.execute({ query: 'health_checks' });
expect(result).toContain('api-health');
expect(result).toContain('UNHEALTHY');
});
it('should return all sections when query is "all"', async () => {
const result = await tool.execute({ query: 'all' });
expect(result).toContain('Unresolved alerts');
expect(result).toContain('Health checks');
expect(result).toContain('Alert statistics');
});
it('should handle missing proactive monitor gracefully', async () => {
const toolNoMonitor = new QuerySystemStatusTool();
const result = await toolNoMonitor.execute({ query: 'alerts' });
expect(result).toContain('not available');
});
});
- Step 3: Run tests
Run: npx jest --config APIs/backend-vitto-api/jest.config.ts --testPathPattern="query-system-status"
Expected: PASS — 5 tests
- Step 4: Commit
git add APIs/backend-vitto-api/src/app/tools/definitions/query-system-status.tool.ts \
APIs/backend-vitto-api/src/app/tools/__tests__/query-system-status.spec.ts
git commit -m "[S211.4] feat: add query_system_status tool so Vitto can access its own alerts and health data"
Task 5: Register Tool and Add to Channel-Safe List
Files:
Modify:
APIs/backend-vitto-api/src/app/tools/tool-registry.service.tsModify:
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts:59-68Step 1: Register in tool-registry.service.ts
Add to the tool definitions (follow the pattern of existing tools like recall_memory):
// Import at top:
import { QuerySystemStatusTool } from './definitions/query-system-status.tool';
// In constructor or init, inject QuerySystemStatusTool
// In registerTools(), add:
this.register(this.querySystemStatus.getDefinition(), (input) => this.querySystemStatus.execute(input));
Note: The exact registration pattern depends on how tool-registry.service.ts registers tools. Read the file to find the pattern — likely a Map with name → { definition, handler }. Follow the existing pattern exactly.
- Step 2: Add to channelSafeTools
In channel-processing.service.ts:59-68, add 'query_system_status' to the array:
private readonly channelSafeTools = [
'http_request', 'query_platform', 'query_docs', 'search_docs', 'read_doc',
'recall_memory', 'search_decisions', 'recall_vitto_memory',
'query_system_status', // S211.4: Vitto can now query its own alerts/health
'browser_navigate', 'browser_interact', 'browser_screenshot',
'computer_use',
'execute_code',
'create_handoff', 'check_handoff_status',
'mcp_preflight', 'mcp_get_platform_info',
'task_complete',
];
- Step 3: Commit
git add APIs/backend-vitto-api/src/app/tools/tool-registry.service.ts \
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts
git commit -m "[S211.4] feat: register query_system_status in tool registry and channel-safe tools"
Chunk 4: Fix Persona Issues (Issue 5)
Task 6: Add Language Boundary to Friendly Persona
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/persona.service.ts:199-224
The Friendly persona's WhatsApp override says tone: 'casual' with no language guard. Combined with humorLevel: 2, the LLM generates culturally aggressive casual Spanish ("mira mijo"). Fix: add explicit language boundary and reduce WhatsApp humor.
- Step 1: Update Friendly persona definition
In persona.service.ts, find the Friendly built-in persona (around line 199) and modify:
// Change channelOverrides.whatsapp:
channelOverrides: {
whatsapp: { tone: 'casual', greeting: 'Hey!' }, // Remove emoji from greeting
telegram: { tone: 'casual', greeting: 'Hi there!' },
},
// Change humorLevel from 2 to 1:
humorLevel: 1, // Was 2 — moderate humor causes culturally inappropriate expressions
// Change formality from 1 to 2 (balanced):
formality: 2, // Was 1 (casual) — too casual leads to "mira mijo" type expressions
- Step 2: Add language boundary in buildBehavioralDirectives()
In the buildBehavioralDirectives() method (around line 425), add a global directive at the end:
// After the responseFormat section, before return:
directives.push('Always respond in the same language the user used. Never mix languages or use culturally-specific slang/idioms unless the user does first.');
- Step 3: Commit
git add APIs/backend-vitto-api/src/app/engine/persona.service.ts
git commit -m "[S211.4] fix: add language boundary to persona, reduce WhatsApp humor to prevent culturally inappropriate expressions"
Chunk 5: Anticipatory Improvements (A1-A4)
Task 7: Skip Agent Loop for Pure Greetings (A1)
Files:
- Modify:
APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts
Currently "Hi Vitto" runs through the full agent loop (LLM call + 18 available tools). This wastes ~500 tokens. For pure greetings, we should respond directly without the agent loop.
- Step 1: Add greeting fast-path before agent loop
In executeUnifiedAgentLoop(), after the classifier result is checked but BEFORE the agent loop runs, add:
// After classification check (around line 200), before context assembly:
// S211.4: Fast-path for pure greetings — skip agent loop entirely
if (classification.reason.includes('Social/greeting') && classification.modelTier === 'fast') {
const persona = await this.contextAssembly.getActivePersonaName?.(userId, spaceId, channel);
const greetingResponse = this.expression.generateGreeting?.(
(metadata.resolvedName as string) || (metadata.profileName as string) || 'there',
persona,
) || `Hey ${(metadata.resolvedName as string) || 'there'}! What can I help you with?`;
// Set result directly, skip agent loop
task.phases.push({
phase: 'EXECUTE', status: 'completed', startedAt,
completedAt: new Date(), output: greetingResponse,
tokens: { input: 0, output: 0 },
});
task.status = 'completed';
task.completedAt = new Date();
task.cost.durationMs = Date.now() - startedAt.getTime();
task.cost.iterations = 0;
await this.routeTaskResponse(task, sseSubject);
this.mind.removeActiveTask(task.id);
return;
}
Add to VittoExpressionService:
generateGreeting(userName: string, persona?: string): string {
const greetings = [
`Hey ${userName}! What's on your mind?`,
`Hi ${userName}! How can I help?`,
`Hey ${userName}! What are we working on?`,
];
return greetings[Math.floor(Math.random() * greetings.length)];
}
- Step 2: Commit
git add APIs/backend-vitto-api/src/app/engine/channel-processing.service.ts \
APIs/backend-vitto-api/src/app/engine/vitto-expression.service.ts
git commit -m "[S211.4] perf: fast-path greetings without agent loop — saves ~500 tokens per greeting"
Task 8: Store Digest in Conversation Memory (A4)
Files:
- Modify:
APIs/backend-vitto-api/src/app/proactive/daily-digest.service.ts
When the daily digest is generated and sent, also store a summary in the user's conversation memory so Vitto can reference it when the user asks follow-up questions about digest content.
- Step 1: Add memory persistence after digest delivery
Find the deliverToSubscribedUsers() method (or equivalent dispatch point). After successful delivery to each user, persist a memory:
// After sending digest to a user:
if (this.mind) {
await this.mind.memory.remember({
type: 'system' as any,
userId: pref.userId,
summary: `Daily digest sent: ${digest.tasks.total} tasks (${digest.tasks.successRate}% success), ${digest.alerts.unresolved} unresolved alerts (${digest.alerts.critical} critical), cost $${digest.cost.estimatedCostUSD.toFixed(2)}`,
tags: ['daily-digest', 'system-status', new Date().toISOString().slice(0, 10)],
importance: 0.7,
metadata: {
date: digest.date,
taskCount: digest.tasks.total,
unresolvedAlerts: digest.alerts.unresolved,
criticalAlerts: digest.alerts.critical,
cost: digest.cost.estimatedCostUSD,
recommendations: digest.recommendations,
},
}).catch(() => {}); // Non-critical
}
This means when the user asks "Do you know more about the 72 unresolved issues?", recall_vitto_memory will find the digest memory AND the new query_system_status tool can provide live data.
- Step 2: Commit
git add APIs/backend-vitto-api/src/app/proactive/daily-digest.service.ts
git commit -m "[S211.4] feat: persist daily digest summary to conversation memory for follow-up context"
Task 9: End-to-End Verification
- Step 1: Run all new and existing tests
npx jest --config APIs/backend-vitto-api/jest.config.ts --testPathPattern="query-system-status|whatsapp-message-flow|escalation-grouping"
- Step 2: Verify build compiles
npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -20
- Step 3: Log decisions via MCP
log_decision({
type: 'architecture',
question: 'How to fix 6 WhatsApp UX issues while making Vitto feel intelligent?',
chosen: 'Three-layer fix: (1) Consolidate message flow — ACK/progress/quick-reply share a budget of max 2 pre-response messages. (2) Add query_system_status tool — Vitto can access its own alerts/health. (3) Context-aware responses — skip agent loop for greetings, suppress generic buttons, language-boundary persona.',
reasoning: '...',
confidence: 0.91,
})
Summary — What This Plan Produces
| Deliverable | Files | Tests | Issue Fixed |
|---|---|---|---|
| ACK + progress coordination | 1 modified | — | #1, #4 |
| MessageSid webhook dedup | 1 modified | — | #1 |
| Quick reply suppression + contextual buttons | 2 modified | — | #2, #6 |
query_system_status tool |
1 new + 1 test | 5 tests | #3 (CRITICAL) |
| Tool registration + channel-safe list | 2 modified | — | #3 |
| Persona language boundary + humor reduction | 1 modified | — | #5 |
| Greeting fast-path (skip agent loop) | 2 modified | — | A1 |
| Digest memory persistence | 1 modified | — | A4 |
| Total | 2 new + 9 modified | 5 new tests | 6 fixes + 2 improvements |
What This Plan Does NOT Cover (Deferred)
- A2: Rich digest formatting — Use CardMessage for daily digest instead of text. Requires the rich messaging adapters from S211.1 to be deployed first. Should be a follow-up task.
- A3: Alert acknowledge handler — When user taps "Acknowledge" button on a WhatsApp rich card, route it back to
ProactiveMonitorService.resolveAlert(). Requires inbound button payload routing which is a separate feature. - WhatsApp Flows — Phase 3 of the rich messaging plan. Blocked on WhatsApp Cloud API credentials.
- Cost tracking fix ($0.00) — Needs investigation of
CostTrackerService.getSpendingStats()data source. May be a separate S212 issue where token attribution was broken.