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:

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:

  1. For fast tier: no ACK (already correct — modelTier !== 'fast' guards it). Verify this is working.
  2. For balanced tier: coordinate ACK with progress emitter so total pre-response messages ≤ 1.

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.

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.

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.

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:

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.

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);
    }
  }
}

After deploying, send "Hi Vitto" and check backend logs for:

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:

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.

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
}

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 [];

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'];
}
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:

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().

// 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}`;
    }
  }
}
// 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');
  });
});

Run: npx jest --config APIs/backend-vitto-api/jest.config.ts --testPathPattern="query-system-status" Expected: PASS — 5 tests

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:

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.

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',
];
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:

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.

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

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.');
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:

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.

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)];
}
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:

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.

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.

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

npx jest --config APIs/backend-vitto-api/jest.config.ts --testPathPattern="query-system-status|whatsapp-message-flow|escalation-grouping"
npx tsc --noEmit --project APIs/backend-vitto-api/tsconfig.lib.json 2>&1 | head -20
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)