managarten/docs/architecture/COMPANION_BRAIN_ARCHITECTURE.md
Till JS b7a76ca24f docs(ai): mark Planner + Runner (Steps 4+5) as done in Workbench roadmap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 21:24:51 +02:00

64 KiB

Mana Companion Brain — Architecture & Implementation Plan

Vollstaendiger Umbau-Plan fuer ein zentrales Intelligenz-System ueber alle Module. Start mit 5 Pilot-Modulen: Todo, Calendar, Drink, Food, Places. Stand: April 2026


1. Vision

Mana hat 40+ Module, die isoliert arbeiten. Der Companion Brain verbindet sie zu einem System, das den Nutzer proaktiv begleitet — erinnert, motiviert, Muster erkennt und Zusammenhaenge zwischen Modulen herstellt. Alles lokal, privacy-first.

Drei Saeulen:

  1. Pulse — Regelbasierte Nudges & Tageszusammenfassungen (kein LLM)
  2. Rituale — Gefuehrte Routinen die Daten in Module schreiben (AI-generiert)
  3. Companion Chat — LLM mit Tool-Zugriff auf alle Module

Fundament:

  • Domain Event Bus (semantische Events statt CRUD-Logs)
  • Projection Engine (live-reaktive Aggregation ueber alle Module)
  • Goal System (moduluebergreifende Ziele mit Fortschritt)
  • Semantic Memory (extrahiertes Nutzerwissen, persistent)
  • Tool Layer (standardisierter LLM-Zugriff auf Module)
  • Feedback Loop (Nudge-Outcomes fuer Lernfaehigkeit)

2. Architektur-Uebersicht

+---------------------------------------------------+
|                  MODULE LAYER                      |
|  Todo - Calendar - Drink - Food - Places       |
|  Jedes Modul emittiert Domain Events via Stores    |
+------------------------+--------------------------+
                         | emit()
                         v
+---------------------------------------------------+
|                  EVENT BUS                         |
|  Typed, synchron, in-process                       |
|  TaskCompleted - DrinkLogged - EventCreated ...    |
+--+--------+--------+--------+--------+-----------+
   |        |        |        |        |
   v        v        v        v        v
+------+ +------+ +------+ +------+ +------+
|Event | |State | |Proj. | |Rule  | |Trig- |
|Store | |Write | |Engine| |Engine| |gers  |
|      | |Dexie | |      | |      | |      |
+------+ +------+ +--+---+ +--+---+ +------+
                      |        |
           +----------+--------+----------+
           v          v        v          v
+---------------------------------------------------+
|              INTELLIGENCE LAYER                    |
|                                                    |
| +------------+ +----------+ +-------+ +--------+  |
| |Projections | | Memory   | | Goals | |Feedback|  |
| |DaySnapshot | | Patterns | | Meter | | Loop   |  |
| |Streaks     | | Prefs    | | Track | | Nudge  |  |
| |Correlations| | Context  | | Link  | | Outcome|  |
| +-----+------+ +----+-----+ +---+---+ +---+----+  |
|       |              |           |         |       |
|       +--------------+-----------+---------+       |
|                      v                             |
|           Context Document Generator               |
|           (~500 Token Nutzer-Snapshot)              |
+------------------------+--------------------------+
                         |
                         v
+---------------------------------------------------+
|              INTERACTION LAYER                     |
|                                                    |
| +----------+ +----------+ +---------+ +---------+  |
| |  Pulse   | | Rituale  | |Companion| |Insights |  |
| |  Engine  | | (AI-gen) | |  Chat   | | Cards   |  |
| | regelb.  | |          | |LLM+Tool | |         |  |
| +----------+ +----------+ +---------+ +---------+  |
|                                                    |
|  Feedback: Nudge -> Outcome -> Memory Update       |
+---------------------------------------------------+

3. Domain Event System

3.1 Warum Domain Events statt CRUD-Logs

Aktuell loggt _activity nur { op: 'update', collection: 'tasks', recordId }. Daraus laesst sich nicht ableiten, was passiert ist. Wurde der Task erledigt? Umbenannt? Verschoben? Das erzwingt Archaeologie — Felder vergleichen, Semantik raten.

Domain Events tragen Bedeutung: TaskCompleted { taskId, title, project } ist sofort verstaendlich fuer Projections, Rules, LLM und Mensch.

3.2 Event Bus Interface

Neues File: apps/mana/apps/web/src/lib/data/events/event-bus.ts

// ── Core Types ──────────────────────────────────────

export interface DomainEvent<T extends string = string, P = unknown> {
  type: T;
  payload: P;
  meta: EventMeta;
}

export interface EventMeta {
  id: string;            // crypto.randomUUID()
  timestamp: string;     // ISO
  appId: string;         // source module
  collection: string;    // source table
  recordId: string;      // affected record
  userId: string;        // from getEffectiveUserId()
  causedBy?: string;     // parent event id (for trigger chains)
}

// ── Bus Interface ───────────────────────────────────

export type EventHandler<E extends DomainEvent = DomainEvent> = (event: E) => void;

export interface EventBus {
  emit(event: DomainEvent): void;
  on<T extends string>(type: T, handler: EventHandler): () => void;
  onAny(handler: EventHandler): () => void;
  off(type: string, handler: EventHandler): void;
}

Implementierung: Einfacher synchroner Dispatcher mit async Subscribers.

  • emit() ist synchron (blockiert Dexie-Hook nicht)
  • Handlers laufen in queueMicrotask() — nach dem Hook, aber vor dem naechsten Frame
  • Guard gegen Endlos-Loops: _emitting Set verhindert re-entrant emits vom selben Event-Typ
export function createEventBus(): EventBus {
  const handlers = new Map<string, Set<EventHandler>>();
  const anyHandlers = new Set<EventHandler>();

  return {
    emit(event: DomainEvent) {
      queueMicrotask(() => {
        const typeHandlers = handlers.get(event.type);
        if (typeHandlers) {
          for (const h of typeHandlers) h(event);
        }
        for (const h of anyHandlers) h(event);
      });
    },

    on(type, handler) {
      if (!handlers.has(type)) handlers.set(type, new Set());
      handlers.get(type)!.add(handler);
      return () => handlers.get(type)?.delete(handler);
    },

    onAny(handler) {
      anyHandlers.add(handler);
      return () => anyHandlers.delete(handler);
    },

    off(type, handler) {
      handlers.get(type)?.delete(handler);
    },
  };
}

// Singleton
export const eventBus = createEventBus();

3.3 Event Store

Ersetzt die _activity-Tabelle als primaere Quelle fuer "was ist passiert".

Neue Dexie-Tabelle _events:

_events: '++seq, meta.id, meta.type, meta.appId, meta.timestamp,
          [meta.appId+meta.timestamp], [meta.type+meta.timestamp]'

Felder:

  • seq — Auto-increment (Reihenfolge-Garantie)
  • type — Domain Event Type (z.B. 'TaskCompleted')
  • payload — Serialisiertes Event-Payload (verschluesselt fuer sensitive Felder)
  • meta — EventMeta Objekt

Retention: 90 Tage (wie _activity), max 50.000 Events. Pruning via bestehender Quota-Recovery.

Subscriber: eventBus.onAny() schreibt jedes Event in _events.

3.4 Domain Events pro Modul (5 Pilot-Module)

Todo Events

Event Payload Abgeleitet aus
TaskCreated { taskId, title, dueDate?, priority?, projectId?, labelIds? } tasksStore.createTask()
TaskCompleted { taskId, title, projectId?, wasOverdue: boolean } tasksStore.completeTask()
TaskUncompleted { taskId, title } tasksStore.uncompleteTask()
TaskUpdated { taskId, fields: string[] } tasksStore.updateTask()
TaskDeleted { taskId, title } tasksStore.deleteTask()
TaskRescheduled { taskId, title, oldDate?, newDate } updateTask wenn dueDate aendert
SubtasksUpdated { taskId, total, completed } tasksStore.updateSubtasks()
ReminderSet { taskId, minutesBefore, type } remindersStore.createReminder()

Calendar Events

Event Payload Abgeleitet aus
CalendarEventCreated { eventId, title, startTime, endTime, isAllDay, isRecurring, calendarId } eventsStore.createEvent()
CalendarEventUpdated { eventId, fields: string[] } eventsStore.updateEvent()
CalendarEventDeleted { eventId, title, wasRecurring } eventsStore.deleteEvent()
CalendarEventMoved { eventId, title, oldStart, newStart } updateEvent wenn startTime aendert

Drink Events

Event Payload Abgeleitet aus
DrinkLogged { entryId, drinkType, quantityMl, name, date, time, fromPreset: boolean } drinkStore.logDrink(), logFromPreset()
DrinkEntryDeleted { entryId, drinkType, quantityMl } drinkStore.deleteEntry()
DrinkEntryUndone { entryId } drinkStore.undoLastEntry()
DrinkGoalReached { date, goalMl, actualMl, drinkType: 'water' } Projection erkennt Zielerreichung

Food Events

Event Payload Abgeleitet aus
MealLogged { mealId, mealType, inputType, description, calories?, protein?, date } mealMutations.create()
MealFromPhotoLogged { mealId, mealType, photoMediaId, confidence, foods? } mealMutations.createFromPhoto()
MealDeleted { mealId, mealType } mealMutations.delete()
NutritionGoalSet { dailyCalories, dailyProtein?, dailyCarbs?, dailyFat? } goalMutations.create/update()
DailyCalorieGoalReached { date, goal, actual } Projection erkennt Zielerreichung

Places Events

Event Payload Abgeleitet aus
PlaceCreated { placeId, name, category?, lat, lng } placesStore.createPlace()
PlaceVisited { placeId, name, visitCount } placesStore.recordVisit()
LocationLogged { logId, lat, lng, placeId?, accuracy } trackingStore.logNow()
TrackingStarted {} trackingStore.startTracking()
TrackingStopped { durationMs, logCount } trackingStore.stopTracking()

3.5 Event-Emission aus Module Stores

Jeder Store bekommt emit()-Calls in seinen Mutations. Kein Umbau der Dexie-Hooks noetig — Events werden im Store emittiert, nicht im Hook.

Warum im Store statt im Hook? Der Hook sieht nur CRUD. Der Store kennt die Semantik. completeTask() weiss, dass es ein Completion ist — der Hook sieht nur update({ completedAt }).

Beispiel: Todo Store nach Umbau:

// stores/tasks.svelte.ts
import { eventBus } from '$lib/data/events/event-bus';

export const tasksStore = {
  async completeTask(id: string) {
    const task = await taskTable.get(id);
    if (!task) return;
    const now = new Date().toISOString();
    const wasOverdue = task.dueDate && task.dueDate < now.slice(0, 10);
    
    await taskTable.update(id, { completedAt: now, updatedAt: now });
    
    eventBus.emit({
      type: 'TaskCompleted',
      payload: {
        taskId: id,
        title: task.title,       // plaintext snapshot (pre-encryption)
        projectId: task.projectId,
        wasOverdue,
      },
      meta: {
        id: crypto.randomUUID(),
        timestamp: now,
        appId: 'todo',
        collection: 'tasks',
        recordId: id,
        userId: getEffectiveUserId(),
      },
    });
  },
  // ... andere Mutations analog
};

Konvention: Jede Store-Mutation die einen Seiteneffekt hat, emittiert ein Event. Reine UI-State-Aenderungen (z.B. calendarViewStore.setDate()) emittieren nicht.

3.6 Event Helper fuer Module

Um Boilerplate zu reduzieren, ein createEventEmitter Helper:

Neues File: apps/mana/apps/web/src/lib/data/events/emit.ts

import { eventBus } from './event-bus';
import { getEffectiveUserId } from '../current-user';

export function emitDomainEvent<P>(
  type: string,
  appId: string,
  collection: string,
  recordId: string,
  payload: P,
  causedBy?: string
): void {
  eventBus.emit({
    type,
    payload,
    meta: {
      id: crypto.randomUUID(),
      timestamp: new Date().toISOString(),
      appId,
      collection,
      recordId,
      userId: getEffectiveUserId(),
      causedBy,
    },
  });
}

Aufruf im Store wird dann einzeilig:

emitDomainEvent('TaskCompleted', 'todo', 'tasks', id, {
  taskId: id, title: task.title, projectId: task.projectId, wasOverdue,
});

4. Projection Engine

4.1 Prinzip

Projections sind live-reaktive Aggregationen ueber Modul-Daten. Sie hoeren Domain Events und aktualisieren sich inkrementell. Consumers (Pulse, Companion, Dashboard) lesen Projections — nie Rohdaten.

Neuer Ordner: apps/mana/apps/web/src/lib/data/projections/

4.2 DaySnapshot

Beantwortet: "Was ist heute los?"

// projections/day-snapshot.ts

export interface DaySnapshot {
  date: string;                       // YYYY-MM-DD
  
  // Todo
  tasks: {
    total: number;
    completed: number;
    overdue: number;
    dueToday: TaskSummary[];
  };
  
  // Calendar
  events: {
    upcoming: EventSummary[];         // naechste 5 Events
    total: number;
    nextEvent?: EventSummary;
  };
  
  // Drink
  drinks: {
    water: { ml: number; goal: number; percent: number };
    coffee: { ml: number; count: number };
    other: { ml: number; count: number };
    total: { ml: number; count: number };
  };
  
  // Food
  nutrition: {
    meals: number;
    calories: { actual: number; goal: number; percent: number };
    protein?: { actual: number; goal: number };
  };
  
  // Places
  places: {
    visited: number;
    currentLocation?: { lat: number; lng: number; placeName?: string };
    tracking: boolean;
  };
}

Implementierung: Dexie liveQueries die auf $derived gemapped werden. Event-Listener fuer inkrementelle Updates (z.B. DrinkLogged addiert direkt statt neu zu querien).

4.3 Streaks

Beantwortet: "Was laeuft gut, was droht zu brechen?"

// projections/streaks.ts

export interface StreakInfo {
  moduleId: string;
  label: string;                      // "Wasser-Ziel", "Journal", "Sport"
  currentStreak: number;              // Tage in Folge
  longestStreak: number;
  lastActiveDate: string;             // YYYY-MM-DD
  status: 'active' | 'at_risk' | 'broken';
  // active: heute oder gestern aktiv
  // at_risk: gestern nicht aktiv, vorgestern schon
  // broken: >1 Tag Pause
}

Berechnet aus: TimeBlocks + Modul-spezifische Logik (Drink: Tagesziel erreicht, Todo: mindestens 1 Task erledigt, etc.)

4.4 Correlations

Beantwortet: "Was haengt zusammen?"

// projections/correlations.ts

export interface Correlation {
  id: string;
  factorA: { module: string; metric: string; label: string };
  factorB: { module: string; metric: string; label: string };
  coefficient: number;               // Pearson r, -1 bis +1
  pValue: number;                    // Statistische Signifikanz
  sampleSize: number;                // Anzahl Tage
  direction: 'positive' | 'negative';
  sentence: string;                  // "An Tagen mit Sport trinkst du 30% mehr Wasser"
  computedAt: string;
}

Berechnung: 1x taeglich, ueber TimeBlocks + Tagesaggregate der letzten 30-90 Tage. Pearson-Korrelation zwischen Paaren. Nur Korrelationen mit |r| > 0.3 und p < 0.05 werden gespeichert.

Metriken pro Modul:

  • Todo: tasks_completed_count, overdue_count
  • Calendar: events_count, meeting_hours
  • Drink: water_ml, coffee_count, goal_reached (boolean)
  • Food: calories, protein, meals_count
  • Places: places_visited, distance_km

4.5 ContactHealth (spaeter, nicht in Pilot)

Wird mit dem Contacts-Modul relevant. Trackt Kontakthaeufigkeit vs. erwartete Frequenz.


5. Goal System

5.1 Datenmodell

Neue Dexie-Tabelle: goals

goals: 'id, moduleId, status, [moduleId+status]'
export interface LocalGoal {
  id: string;
  title: string;                      // "4x Sport pro Woche"
  description?: string;
  
  // Metrik-Definition
  metric: GoalMetric;
  target: GoalTarget;
  
  // Verknuepfung
  moduleId: string;                   // primaeres Modul
  linkedModules: string[];            // weitere beteiligte Module
  
  // Status
  status: 'active' | 'paused' | 'completed' | 'abandoned';
  
  // Tracking
  currentValue: number;               // live berechnet
  currentPeriodStart: string;         // Beginn der aktuellen Periode
  history: GoalPeriodResult[];        // vergangene Perioden
  
  createdAt: string;
  updatedAt: string;
  deletedAt?: string;
}

export interface GoalMetric {
  source: 'event_count' | 'event_sum' | 'streak_days' | 'custom';
  eventType?: string;                 // Domain Event Type (z.B. 'DrinkLogged')
  filterField?: string;               // z.B. 'drinkType'
  filterValue?: string;               // z.B. 'water'
  sumField?: string;                  // z.B. 'quantityMl' (fuer event_sum)
}

export interface GoalTarget {
  value: number;                      // Zielwert
  period: 'day' | 'week' | 'month';
  comparison: 'gte' | 'lte' | 'eq';  // >= (Sport), <= (Kaffee), = (exakt)
}

export interface GoalPeriodResult {
  periodStart: string;
  periodEnd: string;
  value: number;
  reached: boolean;
}

5.2 Goal-Tracking via Events

Der Goal-Tracker subscribed auf den Event Bus und aktualisiert currentValue inkrementell:

// Beispiel: Ziel "8 Glaeser Wasser/Tag"
// metric: { source: 'event_count', eventType: 'DrinkLogged', filterField: 'drinkType', filterValue: 'water' }
// target: { value: 8, period: 'day', comparison: 'gte' }

eventBus.on('DrinkLogged', (event) => {
  if (event.payload.drinkType === 'water') {
    goal.currentValue += 1;
    if (goal.currentValue >= goal.target.value) {
      emitDomainEvent('GoalReached', 'companion', 'goals', goal.id, {
        goalId: goal.id, title: goal.title, value: goal.currentValue,
      });
    }
  }
});

5.3 Vordefinierte Ziel-Templates

Fuer den Start 10-15 Templates die der Nutzer mit einem Tap aktiviert:

  • 8 Glaeser Wasser/Tag (Drink, event_count, DrinkLogged, water)
  • 2000 kcal/Tag (Food, event_sum, MealLogged, calories)
  • 5 Tasks/Tag erledigen (Todo, event_count, TaskCompleted)
  • Alle Mahlzeiten tracken (Food, event_count, MealLogged, 3/day)
  • Jeden Tag einen neuen Ort besuchen (Places, event_count, PlaceVisited, 1/day)

6. Semantic Memory

6.1 Datenmodell

Neue Dexie-Tabelle: _memory

_memory: 'id, category, confidence, lastConfirmed, [category+confidence]'
export interface MemoryFact {
  id: string;
  category: 'pattern' | 'preference' | 'relationship' | 'context';
  
  content: string;                    // Menschenlesbarer Fakt
  // "Trainiert typischerweise Mo/Mi/Fr abends"
  // "Trinkt morgens immer zuerst Kaffee, dann Wasser"
  // "Meetings haeufig Di/Do vormittags"
  
  confidence: number;                 // 0.0 - 1.0
  confirmations: number;             // wie oft bestaetigt
  contradictions: number;             // wie oft widersprochen
  
  sourceEvents: string[];             // Event-IDs die diesen Fakt stuetzen
  sourceModules: string[];            // beteiligte Module
  
  firstSeen: string;                  // wann erstmals erkannt
  lastConfirmed: string;              // letzte Bestaetigung
  expiresAt?: string;                 // fuer temporaere Kontexte
  
  createdAt: string;
  updatedAt: string;
  deletedAt?: string;
}

6.2 Extraktion

Zwei Wege:

  1. Regelbasiert (kein LLM): Pattern-Detektoren ueber Event-Stream:

    • Wiederholungs-Detektor: "3x in 2 Wochen am Montag trainiert → Pattern: trainiert montags"
    • Zeitfenster-Detektor: "Tasks werden zu 80% zwischen 09-12 Uhr erledigt → Preference: Morgen-Produktivitaet"
    • Sequenz-Detektor: "Kaffee wird immer vor dem ersten Event geloggt → Pattern: Kaffee vor Meetings"
  2. LLM-basiert (Tier 1 browser): Woechentlich zusammengefasste Events an lokales Gemma-Modell:

    • "Hier sind die Events der letzten Woche. Welche Muster und Praeferenzen erkennst du?"
    • Ergebnis als JSON parsen → MemoryFact[] schreiben

6.3 Confidence-Lifecycle

Neuer Fakt erkannt          → confidence: 0.3
Nochmal bestaetigt          → confidence: 0.5
3+ Bestaetigungen           → confidence: 0.7
10+ Bestaetigungen          → confidence: 0.9
Widerspruch erkannt         → confidence -= 0.15
Laenger als 30 Tage nicht   → confidence -= 0.05/Woche
  bestaetigt
confidence < 0.1            → Fakt wird geloescht

7. Context Document Generator

7.1 Zweck

Komprimiert den gesamten Nutzerzustand in ein ~500 Token Dokument, das als System-Prompt an das LLM geht. Aktualisiert sich bei jedem Companion-Aufruf.

7.2 Template

// projections/context-document.ts

export function generateContextDocument(
  day: DaySnapshot,
  streaks: StreakInfo[],
  goals: LocalGoal[],
  memory: MemoryFact[],
  correlations: Correlation[]
): string {
  return `## Nutzer-Kontext (${day.date})

### Heute
- ${day.tasks.dueToday.length} Tasks faellig (${day.tasks.completed} erledigt, ${day.tasks.overdue} ueberfaellig)
- ${day.events.total} Termine${day.events.nextEvent ? ` — naechster: ${day.events.nextEvent.title} um ${day.events.nextEvent.startTime}` : ''}
- Wasser: ${day.drinks.water.ml}ml von ${day.drinks.water.goal}ml (${day.drinks.water.percent}%)
- Ernaehrung: ${day.nutrition.calories.actual} von ${day.nutrition.calories.goal} kcal, ${day.nutrition.meals} Mahlzeiten
${day.places.tracking ? `- Standort-Tracking aktiv` : ''}

### Ziele
${goals.filter(g => g.status === 'active').map(g =>
  `- "${g.title}" — ${g.currentValue}/${g.target.value} (${g.target.period})`
).join('\n')}

### Streaks
${streaks.filter(s => s.status !== 'broken').map(s =>
  `- ${s.label}: ${s.currentStreak} Tage${s.status === 'at_risk' ? ' (GEFAEHRDET)' : ''}`
).join('\n')}
${streaks.filter(s => s.status === 'broken').map(s =>
  `- ${s.label}: UNTERBROCHEN seit ${daysSince(s.lastActiveDate)} Tagen`
).join('\n')}

### Bekannte Muster
${memory.filter(m => m.confidence > 0.5).map(m => `- ${m.content}`).join('\n')}

### Korrelationen
${correlations.slice(0, 3).map(c => `- ${c.sentence}`).join('\n')}
`;
}

8. Tool Layer (LLM Write-Access)

8.1 ModuleTool Interface

Neues File: apps/mana/apps/web/src/lib/data/tools/types.ts

export interface ModuleTool {
  name: string;                       // 'create_task', 'log_drink'
  module: string;                     // 'todo', 'drink'
  description: string;                // Fuer LLM Function-Schema
  parameters: ToolParameter[];
  execute: (params: Record<string, unknown>) => Promise<ToolResult>;
}

export interface ToolParameter {
  name: string;
  type: 'string' | 'number' | 'boolean';
  description: string;
  required: boolean;
  enum?: string[];                    // z.B. ['water', 'coffee', 'tea']
}

export interface ToolResult {
  success: boolean;
  data?: unknown;
  message?: string;                   // Menschenlesbare Bestaetigung
}

8.2 Tool-Definitionen (5 Pilot-Module)

Jedes Modul bekommt eine tools.ts:

// modules/todo/tools.ts
export const todoTools: ModuleTool[] = [
  {
    name: 'create_task',
    module: 'todo',
    description: 'Erstellt einen neuen Task',
    parameters: [
      { name: 'title', type: 'string', description: 'Titel des Tasks', required: true },
      { name: 'dueDate', type: 'string', description: 'Faelligkeitsdatum (YYYY-MM-DD)', required: false },
      { name: 'priority', type: 'number', description: 'Prioritaet 0-3', required: false },
    ],
    execute: async (params) => {
      const task = await tasksStore.createTask({
        title: params.title as string,
        dueDate: params.dueDate as string | undefined,
        priority: params.priority as number | undefined,
      });
      return { success: true, data: task, message: `Task "${task.title}" erstellt` };
    },
  },
  {
    name: 'complete_task',
    module: 'todo',
    description: 'Markiert einen Task als erledigt',
    parameters: [
      { name: 'taskId', type: 'string', description: 'ID des Tasks', required: true },
    ],
    execute: async (params) => {
      await tasksStore.completeTask(params.taskId as string);
      return { success: true, message: 'Task erledigt' };
    },
  },
];

// modules/drink/tools.ts
export const drinkTools: ModuleTool[] = [
  {
    name: 'log_drink',
    module: 'drink',
    description: 'Loggt ein Getraenk',
    parameters: [
      { name: 'drinkType', type: 'string', description: 'Art', required: true, enum: ['water', 'coffee', 'tea', 'juice', 'alcohol', 'other'] },
      { name: 'quantityMl', type: 'number', description: 'Menge in ml', required: true },
      { name: 'name', type: 'string', description: 'Name des Getraenks', required: false },
    ],
    execute: async (params) => {
      const entry = await drinkStore.logDrink({
        name: (params.name as string) ?? (params.drinkType as string),
        drinkType: params.drinkType as DrinkType,
        quantityMl: params.quantityMl as number,
      });
      return { success: true, data: entry, message: `${params.quantityMl}ml ${params.drinkType} geloggt` };
    },
  },
];

// modules/calendar/tools.ts — create_event
// modules/food/tools.ts — log_meal
// modules/places/tools.ts — record_visit, create_place

8.3 Tool Registry

Neues File: apps/mana/apps/web/src/lib/data/tools/registry.ts

import { todoTools } from '$lib/modules/todo/tools';
import { calendarTools } from '$lib/modules/calendar/tools';
import { drinkTools } from '$lib/modules/drink/tools';
import { foodTools } from '$lib/modules/food/tools';
import { placesTools } from '$lib/modules/places/tools';

const ALL_TOOLS: ModuleTool[] = [
  ...todoTools,
  ...calendarTools,
  ...drinkTools,
  ...foodTools,
  ...placesTools,
];

export function getTools(): ModuleTool[] {
  return ALL_TOOLS;
}

export function getTool(name: string): ModuleTool | undefined {
  return ALL_TOOLS.find((t) => t.name === name);
}

export function getToolsForLlm(): LlmFunctionSchema[] {
  return ALL_TOOLS.map((t) => ({
    name: t.name,
    description: t.description,
    parameters: {
      type: 'object',
      properties: Object.fromEntries(
        t.parameters.map((p) => [p.name, {
          type: p.type,
          description: p.description,
          ...(p.enum ? { enum: p.enum } : {}),
        }])
      ),
      required: t.parameters.filter((p) => p.required).map((p) => p.name),
    },
  }));
}

8.4 Integration mit LLM Orchestrator

Der bestehende LlmOrchestrator in @mana/shared-llm bekommt eine neue Methode:

// In shared-llm/src/orchestrator.ts

async runWithTools<TOut>(
  task: LlmTask,
  input: { messages: ChatMessage[]; tools: LlmFunctionSchema[] },
): Promise<LlmTaskResult<TOut>>

Das LLM gibt tool_use Responses zurueck, die der Orchestrator ueber getTool(name).execute(params) ausfuehrt. Das Ergebnis wird als tool_result Message zurueckgefuettert.


9. Rule Engine (Pulse)

9.1 Prinzip

Die Rule Engine liest Projections und erzeugt Nudges. Kein LLM — rein deterministisch. Laeuft auf zwei Wegen:

  1. Event-getriggert: Reagiert auf Domain Events (z.B. TaskCompleted → Streak-Check)
  2. Zeitgesteuert: Periodische Checks (Morgen-Summary, Abend-Reflexion, stuendlich)

9.2 Rule Interface

Neues File: apps/mana/apps/web/src/lib/companion/rules/types.ts

export interface PulseRule {
  id: string;
  name: string;
  description: string;
  
  // Trigger
  trigger:
    | { kind: 'event'; eventType: string }
    | { kind: 'schedule'; cron: string }     // z.B. '0 8 * * *' (08:00 taeglich)
    | { kind: 'interval'; minutes: number }; // z.B. 60 (stuendlich)
  
  // Check — gibt null zurueck wenn kein Nudge noetig
  check: (ctx: RuleContext) => Promise<Nudge | null>;
}

export interface RuleContext {
  day: DaySnapshot;
  streaks: StreakInfo[];
  goals: LocalGoal[];
  memory: MemoryFact[];
  now: Date;
}

export interface Nudge {
  id: string;
  type: NudgeType;
  title: string;
  body: string;
  priority: 'low' | 'medium' | 'high';
  actionLabel?: string;               // "Jetzt loggen"
  actionRoute?: string;               // '/drink'
  actionTool?: string;                // 'log_drink' — Companion kann direkt ausfuehren
  expiresAt?: string;                 // wann der Nudge irrelevant wird
}

type NudgeType =
  | 'streak_warning'
  | 'goal_progress'
  | 'goal_reached'
  | 'morning_summary'
  | 'evening_reflection'
  | 'overdue_tasks'
  | 'water_reminder'
  | 'meal_reminder'
  | 'correlation_insight';

9.3 Vordefinierte Rules (Pilot)

// rules/water-reminder.ts
export const waterReminderRule: PulseRule = {
  id: 'water-reminder',
  name: 'Wasser-Erinnerung',
  trigger: { kind: 'interval', minutes: 90 },
  async check(ctx) {
    const { water } = ctx.day.drinks;
    if (water.percent >= 100) return null;  // Ziel erreicht
    const hourOfDay = ctx.now.getHours();
    if (hourOfDay < 8 || hourOfDay > 21) return null;  // Nachtruhe
    
    const remaining = water.goal - water.ml;
    const hoursLeft = 21 - hourOfDay;
    const mlPerHour = Math.ceil(remaining / hoursLeft);
    
    return {
      id: `water-${ctx.day.date}-${hourOfDay}`,
      type: 'water_reminder',
      title: 'Wasser trinken',
      body: `Noch ${remaining}ml bis zum Ziel. ~${mlPerHour}ml pro Stunde.`,
      priority: water.percent < 50 ? 'medium' : 'low',
      actionLabel: 'Glas loggen',
      actionTool: 'log_drink',
    };
  },
};

// rules/streak-warning.ts
export const streakWarningRule: PulseRule = {
  id: 'streak-warning',
  name: 'Streak-Warnung',
  trigger: { kind: 'schedule', cron: '0 18 * * *' },  // 18:00 taeglich
  async check(ctx) {
    const atRisk = ctx.streaks.filter(s => s.status === 'at_risk');
    if (atRisk.length === 0) return null;
    
    const best = atRisk.reduce((a, b) => a.currentStreak > b.currentStreak ? a : b);
    return {
      id: `streak-${ctx.day.date}`,
      type: 'streak_warning',
      title: `${best.label}-Streak in Gefahr!`,
      body: `${best.currentStreak} Tage — nicht heute brechen.`,
      priority: best.currentStreak > 7 ? 'high' : 'medium',
    };
  },
};

// rules/morning-summary.ts
// rules/evening-reflection.ts
// rules/overdue-tasks.ts
// rules/meal-reminder.ts
// rules/goal-reached.ts

9.4 Rule Engine Runner

Integriert sich in den bestehenden Reminder-Scheduler als zusaetzliche Source:

// companion/rules/engine.ts

export function createRuleEngine(rules: PulseRule[]): ReminderSource {
  return {
    id: 'companion-pulse',
    async checkDue(): Promise<DueReminder[]> {
      const ctx = await buildRuleContext();
      const nudges: Nudge[] = [];
      
      for (const rule of rules) {
        if (shouldTrigger(rule)) {
          const nudge = await rule.check(ctx);
          if (nudge && !isDismissed(nudge.id)) {
            nudges.push(nudge);
          }
        }
      }
      
      return nudges.map(toReminder);
    },
    async markSent(id) { /* track in localStorage */ },
  };
}

10. Feedback Loop

10.1 Datenmodell

Neue Dexie-Tabelle: _nudgeOutcomes

_nudgeOutcomes: '++id, nudgeId, nudgeType, outcome, timestamp, [nudgeType+outcome]'
export interface NudgeOutcome {
  id?: number;
  nudgeId: string;
  nudgeType: NudgeType;
  outcome: 'acted' | 'dismissed' | 'snoozed' | 'expired' | 'auto_resolved';
  latencyMs?: number;                 // Zeit bis Reaktion
  timestamp: string;
}

10.2 Lerneffekt

Aggregation ueber _nudgeOutcomes beeinflusst die Rule Engine:

// Beispiel: Wasser-Reminder wird um 10:00 immer dismissed
// → confidence fuer "Nutzer mag morgens keine Wasser-Reminder" steigt
// → Rule Engine verschiebt auf 11:00

// Beispiel: Streak-Warning um 18:00 fuehrt zu 80% zu Aktion
// → confidence fuer "18:00 ist guter Zeitpunkt" steigt
// → bleibt bei 18:00

Memory-Facts werden aus Outcome-Patterns extrahiert und fliessen in den Context Document Generator.


11. Companion Chat (Interaction Layer)

11.1 Modul-Struktur

Neues Modul: apps/mana/apps/web/src/lib/modules/companion/

companion/
  module.config.ts          — Registriert companion-Tabellen
  collections.ts            — conversations, messages, rituals, goals
  stores/
    chat.svelte.ts          — Chat-Mutations (send, receive, tool-call)
    rituals.svelte.ts       — Ritual-CRUD + Step-Execution
    goals.svelte.ts         — Goal-CRUD + Progress-Tracking
  queries.ts                — Live-Queries fuer Chat, Rituals, Goals
  tools.ts                  — Companion-eigene Tools (read_context, get_insights)
  components/
    CompanionChat.svelte    — Chat-Interface mit Tool-Execution
    CompanionFeed.svelte    — Timeline von Nudges + Insights + Chat
    RitualRunner.svelte     — Step-by-Step Ritual-UI
    GoalCard.svelte         — Ziel-Fortschritts-Anzeige

11.2 Chat-Flow

User: "Wie laeuft mein Tag?"
    |
    v
CompanionChat → LLM Orchestrator
    |
    |  System Prompt = Context Document (~500 Tokens)
    |  + Tool Schemas (getToolsForLlm())
    |  + User Message
    |
    v
LLM (Gemma lokal ODER Gemini Cloud)
    |
    |  Response: "Du hast heute 3/7 Tasks erledigt und erst 400ml
    |  Wasser getrunken. Dein Kalender ist ab 15:00 frei — guter
    |  Zeitpunkt fuer die ueberfaelligen Tasks. Soll ich dich
    |  in 2 Stunden ans Wasser erinnern?"
    |
    |  tool_use: create_reminder(...)
    |
    v
Tool Execution → drinkStore / remindersStore
    |
    v
CompanionChat zeigt Antwort + Aktions-Bestaetigung

11.3 Ritual-Generierung

User: "Erstell mir eine Morgenroutine"
    |
    v
LLM + Context Document + Tool Schemas
    |
    |  LLM sieht: Nutzer hat Drink, Todo, Food, Calendar aktiv
    |  Memory: "Trinkt morgens zuerst Kaffee"
    |  Goals: "8 Glaeser Wasser/Tag"
    |
    v
Generiertes Ritual:
  1. Glas Wasser loggen (tool: log_drink, water, 250ml)
  2. Stimmung checken (free_text → journal)
  3. Tages-Tasks priorisieren (zeigt DaySnapshot.tasks.dueToday)
  4. Kalender-Ueberblick (zeigt DaySnapshot.events.upcoming)
  5. Fruehstueck loggen (tool: log_meal)

12. Dateien & Ordnerstruktur

= implementiert, = ausstehend

apps/mana/apps/web/src/lib/
  data/
    events/                         ✅ Phase 1
      event-bus.ts                  ✅ EventBus Singleton (sync dispatch, microtask handlers)
      event-store.ts                ✅ Persistenz in _events Tabelle (90d TTL, 50k max)
      emit.ts                       ✅ emitDomainEvent() Helper
      types.ts                      ✅ DomainEvent, EventMeta, EventBus Interfaces
      catalog.ts                    ✅ 22 Event-Typen (ManaEvent union type)
      index.ts                      ✅ Barrel Export
    projections/                    ✅ Phase 2
      day-snapshot.ts               ✅ useDaySnapshot() — live Tagesaggregation
      streaks.ts                    ✅ useStreaks() — 3 Streak-Typen, 90d Lookback
      context-document.ts           ✅ generateContextDocument() — ~500 Token LLM-Prompt
      correlations.ts               ✅ Phase 7 — Pearson ueber 7 Metriken
      types.ts                      ✅ DaySnapshot, StreakInfo, TaskSummary, EventSummary
      index.ts                      ✅ Barrel Export
    tools/                          ✅ Phase 4
      types.ts                      ✅ ModuleTool, ToolParameter, ToolResult, LlmFunctionSchema
      registry.ts                   ✅ registerTools(), getToolsForLlm()
      executor.ts                   ✅ executeTool() mit Validierung + Typ-Coercion
      init.ts                       ✅ initTools() — registriert alle 5 Module
      index.ts                      ✅ Barrel Export
  companion/
    goals/                          ✅ Phase 3
      types.ts                      ✅ LocalGoal, GoalMetric, GoalTarget, 6 Templates
      store.ts                      ✅ CRUD + Event-Bus-Subscription fuer Progress
      queries.ts                    ✅ useActiveGoals(), useAllGoals()
      index.ts                      ✅ Barrel Export
    rules/                          ✅ Phase 3
      types.ts                      ✅ PulseRule, Nudge, NudgeType, RuleContext
      rules.ts                      ✅ 5 Rules (water, streak, morning, overdue, meal)
      engine.ts                     ✅ evaluateRules(), createPulseReminderSource()
      index.ts                      ✅ Barrel Export
    feedback/                       ✅ Phase 3
      types.ts                      ✅ NudgeOutcome
      tracker.ts                    ✅ recordOutcome(), getOutcomeStats(), getActionRate()
      index.ts                      ✅ Barrel Export
    memory/                         ✅ Phase 7
      types.ts                      ✅ MemoryFact, Correlation
      store.ts                      ✅ recordFact, contradictFact, applyDecay, getFacts
      extractors.ts                 ✅ 3 Extractors (day-of-week, time-of-day, frequency)
      index.ts                      ✅ Barrel Export
    rituals/                        ✅ Phase 6
      types.ts                      ✅ 6 Step-Typen, 3 Templates
      store.ts                      ✅ createFromTemplate, CRUD, logs
      queries.ts                    ✅ useActiveRituals, useAllRituals
      index.ts                      ✅ Barrel Export
  modules/
    companion/                      ✅ Phase 5
      types.ts                      ✅ LocalConversation, LocalMessage
      collections.ts                ✅ companionConversations, companionMessages
      stores/chat.svelte.ts         ✅ Conversation + Message CRUD
      queries.ts                    ✅ useConversations, useMessages
      engine.ts                     ✅ runCompanionChat (LLM + Tools + Context)
      index.ts                      ✅ Barrel Export
      components/
        CompanionChat.svelte        ✅ Chat-UI mit Streaming + Tool-Results
        RitualRunner.svelte         ✅ Step-by-Step Runner
        CompanionFeed.svelte        ⬜ Timeline (spaetere Iteration)
        GoalCard.svelte             ⬜ Goal-Fortschritts-Widget (spaetere Iteration)
    todo/
      tools.ts                      ✅ 3 Tools (create, complete, stats)
      stores/tasks.svelte.ts        ✅ 5 Events (Created, Completed, Uncompleted, Deleted, Subtasks)
    calendar/
      tools.ts                      ✅ 2 Tools (create_event, get_todays_events)
      stores/events.svelte.ts       ✅ 3 Events (Created, Updated, Deleted)
    drink/
      tools.ts                      ✅ 3 Tools (log, progress, undo)
      stores/drink.svelte.ts        ✅ 3 Events (Logged, Deleted, Undone)
    food/
      tools.ts                      ✅ 2 Tools (log_meal, nutrition_summary)
      mutations.ts                  ✅ 3 Events (Logged, PhotoLogged, Deleted)
    places/
      tools.ts                      ✅ 4 Tools (create, visit, get_places, location)
      stores/places.svelte.ts       ✅ 3 Events (Created, Deleted, Visited)
      stores/tracking.svelte.ts     ✅ 3 Events (Started, Stopped, LocationLogged)

13. Dexie-Tabellen

Implementiert (v10 Schema)

// Event Store — append-only domain event log
_events: '++seq, type, meta.appId, meta.timestamp, meta.recordId, [meta.appId+meta.timestamp], [type+meta.timestamp]',

// Goals — companion brain goal tracking
companionGoals: 'id, moduleId, status, [moduleId+status]',

// Semantic Memory — extracted user patterns (prepared, not yet populated)
_memory: 'id, category, confidence, lastConfirmed, [category+confidence]',

// Feedback Loop — nudge outcome tracking
_nudgeOutcomes: '++id, nudgeId, nudgeType, outcome, timestamp, [nudgeType+outcome]',

Noch ausstehend (Phase 5+)

// Companion Chat (Phase 5)
companionConversations: 'id, createdAt',
companionMessages: 'id, conversationId, role, createdAt, [conversationId+createdAt]',

// Rituals (Phase 6)
rituals: 'id, status, createdAt',
ritualSteps: 'id, ritualId, order, [ritualId+order]',
ritualLogs: '++id, ritualId, date, [ritualId+date]',

14. Implementierungs-Reihenfolge

Phase 1: Event-Fundament — ERLEDIGT (2026-04-13)

Commit: e927c1f10

  1. data/events/ — EventBus, EventStore, emit Helper, Type Catalog
  2. Domain Events fuer 5 Pilot-Module definiert (catalog.ts, 22 Event-Typen)
  3. Stores der 5 Module umgebaut: emit() Calls eingefuegt
  4. Event Store Subscriber: eventBus.onAny()_events Tabelle (v10 Schema)
  5. Tests: noch ausstehend

Ergebnis: Semantischer Event-Stream fliesst. 20 Domain Events aus 5 Modulen.

Implementierungsnotizen:

  • Events werden im Store emittiert (nicht im Dexie-Hook) — der Store kennt die Semantik
  • emitDomainEvent() Helper reduziert Boilerplate auf eine Zeile pro Event
  • Re-Entrancy-Guard im EventBus verhindert Endlos-Loops
  • _activity Tabelle bleibt parallel bestehen (Sync-Debugging)

Phase 2: Projections — ERLEDIGT (2026-04-13)

Commit: 40e1145e9

  1. DaySnapshot Projection (live Dexie-Queries ueber alle 5 Module)
  2. Streaks Projection (3 Streak-Definitionen: Wasser, Tasks, Mahlzeiten, 90-Tage Lookback)
  3. Context Document Generator (Template-basiert, ~300-500 Token)
  4. Dashboard-Widget: "Mein Tag" Karte — spaeter in UI-Phase

Ergebnis: Zentraler Ueberblick ueber alle 5 Module, live-reaktiv.

Implementierungsnotizen:

  • Projections nutzen useLiveQueryWithDefault aus @mana/local-store/svelte
  • DaySnapshot queried 5 Dexie-Tabellen + decrypted in einem buildSnapshot()-Call
  • Streaks berechnen per checkDate() ob ein Tag "zaehlt" (z.B. Wasser-Ziel erreicht)
  • Context Document ist reines String-Template, kein LLM noetig
  • startEventStore() in (app)/+layout.svelte bei Auth-Ready gewired

Phase 3: Goals + Pulse — ERLEDIGT (2026-04-13)

Commit: 9066b6c9a

  1. Goal Datenmodell + Store + Queries (companion/goals/)
  2. Goal-Tracking via Event-Bus-Subscription (auto-increment currentValue)
  3. 6 Goal-Templates (Wasser, Tasks, Mahlzeiten, Kalorien, Orte, Kaffee-Limit)
  4. Rule Engine mit 5 Rules (companion/rules/)
  5. ReminderSource-Adapter fuer bestehenden Scheduler
  6. Nudge-UI: Toast / Bottom-Sheet — in Phase 5 (Companion Chat)

Ergebnis: Goals tracken automatisch, Rules erzeugen Nudges.

Implementierungsnotizen:

  • Goals leben in companionGoals Tabelle (v10 Schema), nicht im Core-Modul
  • Goal-Tracker subscribed auf eventBus.onAny() und matched per eventType + Filter
  • Perioden-Reset (day/week/month) passiert automatisch beim naechsten Event
  • GoalReached Event wird emittiert wenn Ziel erstmals in einer Periode erreicht
  • Rules nutzen localStorage fuer Dismissal-Tracking und Last-Run-Timestamps
  • _memory und _nudgeOutcomes Tabellen vorbereitet (v10 Schema)

Phase 4: Tool Layer — ERLEDIGT (2026-04-13)

Commit: 66dd684bb

  1. ModuleTool Interface + Registry (dynamische Registrierung)
  2. tools.ts fuer 5 Pilot-Module (13 Tools total)
  3. Tool Executor mit Parameter-Validierung + Typ-Coercion
  4. LLM Function Schema Generator (getToolsForLlm())
  5. Integration in LLM Orchestrator (runWithTools) — in Phase 5

Ergebnis: 13 Tools bereit fuer LLM Function-Calling.

Implementierungsnotizen:

  • Registry nutzt registerTools() Pattern statt statische Imports (tree-shaking-freundlich)
  • initTools() in (app)/+layout.svelte gewired neben startEventStore()
  • Executor coerced String→Number und String→Boolean automatisch
  • Tools pro Modul: Todo (3), Calendar (2), Drink (3), Food (2), Places (4)
  • Jeder Tool hat eine message Feld fuer menschenlesbare Bestaetigung

Phase 5: Companion Chat — ERLEDIGT (2026-04-13)

Commit: 46db527f8

  1. Companion Modul (types, collections, stores/chat, queries)
  2. CompanionChat Svelte-Komponente (Streaming, Tool-Results inline)
  3. Chat-Flow: Context Document als System-Prompt + Tool Schemas + LLM
  4. /companion Route mit Sidebar (Gespraechsliste) + Chat-Area
  5. CompanionFeed: Timeline von Nudges + Chat — spaetere UI-Iteration

Ergebnis: Nutzer kann mit dem System sprechen und Aktionen ausfuehren.

Implementierungsnotizen:

  • Chat nutzt @mana/local-llm (Gemma, browser-lokal) direkt — kein Server-Call
  • Tool Calling via JSON-Block-Extraction (\``tool {...}````) statt nativem Function-Calling (Gemma unterstuetzt das nicht nativ)
  • Max 3 Tool-Call-Runden pro Nachricht (verhindert Endlos-Loops)
  • Conversations + Messages persistent in IndexedDB (companionConversations, companionMessages)
  • Entscheidung: Companion lebt als eigenes Modul unter /companion, nicht als Overlay
  • Streaming via onToken Callback — erster Round streamt, Folgerunden (nach Tool-Call) nicht

Phase 6: Rituale — ERLEDIGT (2026-04-13)

Commit: 41357b254

  1. Ritual Datenmodell (6 Step-Typen: tool_call, number_input, text_input, mood_picker, info_display, checklist)
  2. RitualRunner Komponente (Step-Card-UI, Progress-Bar, Tool-Execution)
  3. AI-Ritual-Generierung via Companion Chat — spaetere Iteration
  4. 3 Ritual-Templates (Morgenroutine, Abendroutine, Trink-Check)
  5. /companion/rituals Route mit Template-Picker + Ritual-Liste

Ergebnis: Gefuehrte Routinen die in Module schreiben.

Implementierungsnotizen:

  • Rituale leben in companion/rituals/ (nicht als eigenes Modul)
  • Steps referenzieren Tools per Name — dieselben Tools die der Chat nutzt
  • info_display Steps zeigen Projektionsdaten (Tasks, Events, Drinks, Nutrition, Streaks)
  • Completion-Logs tracken wieviele Steps pro Tag abgeschlossen wurden
  • Templates sind statisch definiert — AI-Generierung wird in Phase 5 integriert

Phase 7: Memory + Correlations — ERLEDIGT (2026-04-13)

Commit: 87a1dd682

  1. Semantic Memory Store (_memory Tabelle, Confidence-Lifecycle)
  2. 3 regelbasierte Pattern-Extractors (11 Extraction-Rules ueber 5 Module)
  3. Correlation Engine (Pearson ueber 7 Metriken, cross-modul)
  4. Memory + Correlations in Context Document integriert
  5. Feedback Loop: _nudgeOutcomes Tabelle + Tracker (Phase 3)
  6. LLM-basierte Memory-Extraktion — spaetere Iteration

Ergebnis: System lernt Muster, findet Korrelationen, alles fliesst ins LLM.

Implementierungsnotizen:

  • Pattern Extractors: day-of-week (Wochentags-Muster), time-of-day (4h-Peak-Fenster), frequency (Tages-Durchschnitt)
  • Confidence: 0.3 initial, +0.15 pro Bestaetigung, -0.15 bei Widerspruch, Decay nach 30 Tagen
  • Correlations nur cross-modul (gleich-Modul wird uebersprungen, trivial korreliert)
  • Nur Korrelationen mit |r| >= 0.3 und >= 14 Tage Daten werden behalten
  • extractAllPatterns() soll taeglich laufen (manuell oder via Scheduled Rule)
  • computeCorrelations() berechnet on-demand, nicht persistent gecached

Phase 8: Rollout auf weitere Module (Woche 8+)

Pro Modul:

  1. Domain Events definieren (catalog.ts erweitern)
  2. Store Mutations mit emit() versehen
  3. tools.ts schreiben
  4. Projections erweitern (DaySnapshot Felder)
  5. Goal-Templates hinzufuegen
  6. Pulse Rules hinzufuegen

Geschaetzter Aufwand pro Modul: 1-2 Tage.


15. Abhaengigkeiten & Reihenfolge-Graph

Phase 1 (Events) ──────┬──> Phase 2 (Projections)
                        |         |
                        |         v
                        ├──> Phase 3 (Goals + Pulse)
                        |         |
                        v         v
                   Phase 4 (Tools) ──> Phase 5 (Companion Chat)
                                              |
                                              v
                                       Phase 6 (Rituale)
                                              |
                                              v
                                       Phase 7 (Memory)
                                              |
                                              v
                                       Phase 8 (Rollout)

Status: Phase 1-8 ERLEDIGT (2026-04-13). 29 von ~40 Modulen angebunden.


15b. Phase 8 Status: Modul-Rollout

Angebundene Module (29)

# Modul Events Tools Batch
1 Todo 5 3 Pilot
2 Calendar 3 2 Pilot
3 Drink 3 3 Pilot
4 Food 3 2 Pilot
5 Places 6 4 Pilot
6 Habits 3 3 Batch 2
7 Journal 3 2 Batch 2
8 Notes 2 1 Batch 2
9 Contacts 2 2 Batch 2
10 Body 5 3 Batch 2
11 Finance 2 1 Batch 3
12 Dreams 2 1 Batch 3
13 Cards 2 1 Batch 3
14 Times 2 2 Batch 3
15 Social Events 2 1 Batch 3
16 Music 1 1 Batch 4
17 Storage 1 1 Batch 4
18 Chat 2 1 Batch 4
19 Memoro 1 1 Batch 4
20 Skilltree 2 2 Batch 4
21 Period 1 1 Batch 5
22 Firsts 1 1 Batch 5
23 Guides 1 1 Batch 5
24 Inventory 1 1 Batch 5
25 Photos 1 0 Batch 5
26 Plants 2 1 Batch 6
27 News 1 0 Batch 6
28 Recipes 2 1 Batch 6
29 Questions 1 0 Batch 6
Total 67 47

Noch fehlende Module (~11)

Modul Grund Prioritaet
Citycorners Nischen-Modul (Konstanz-Guide) Niedrig
Uload URL-Shortener, wenig Brain-relevant Niedrig
Calc Kein persistenter State Nicht noetig
Moodlit Ambient-Lighting, kein Tracking Nicht noetig
Playground Dev-Tool fuer LLM-Tests Nicht noetig
Who Rate-Spiel, kein Tracking Nicht noetig
Quotes Zitate (read-only) Nicht noetig
Context Kein eigener Store / Mutations Nicht noetig
Presi Praesentation-Builder Niedrig
Meditate Meditation-Sessions Mittel
Sleep Schlaf-Tracking Mittel

Empfehlung: Meditate und Sleep lohnen sich fuer Correlations (Schlaf vs. Produktivitaet). Die anderen sind entweder read-only, Dev-Tools oder haben keinen persistenten State der fuer das Brain relevant waere.


15c. Bekannte Altlasten & Optimierungs-Potenzial

Altlast: _activity Tabelle

Die alte _activity-Tabelle wird weiterhin parallel zum neuen _events Event Store befuellt (via Dexie-Hooks in database.ts). Sie enthaelt nur CRUD-Operationen ohne Semantik. Kann entfernt werden sobald alle Debug-Tools und die Activity-Seite auf _events umgestellt sind.

TODO: trackActivity() Calls in database.ts:546-638 entfernen und Activity-Query in activity.ts auf queryEvents() umstellen.

Altlast: Trigger-System duplikation

Das bestehende Trigger-System (lib/triggers/) feuert ebenfalls bei Dexie-Writes und hat eigene Actions (logHabit, createTask, createNote). Das Companion Brain hat ein eigenes, maechtigeres System (Domain Events + Goals + Rules). Langfristig sollte das alte Trigger-System in die Rule Engine migriert werden.

TODO: Bestehende Automations (automations Tabelle) als Pulse Rules abbilden, altes Trigger-System entfernen.

Optimierung: Streaks-Berechnung

useStreaks() in streaks.ts berechnet fuer jeden Streak bis zu 90 Tage zurueck — pro Streak eine separate Dexie-Query pro Tag (worst case: 3 Streaks x 90 Tage = 270 Queries). Fuer die Pilotphase akzeptabel, langfristig sollte das via Event-basierte inkrementelle Berechnung ersetzt werden (Event "DrinkGoalReached" → Streak +1 statt taeglich zurueckschauen).

Optimierung: DaySnapshot Query-Last

buildSnapshot() in day-snapshot.ts queried 5+ Dexie-Tabellen sequentiell + decrypted jeweils. Bei grossen Datenmengen koennte das >100ms dauern. Moegliche Optimierungen:

  • Parallele Queries via Promise.all()
  • Caching des Snapshots fuer 30s (statt bei jedem liveQuery-Trigger neu berechnen)
  • Event-basiertes inkrementelles Update statt Full-Scan

Optimierung: Context Document fuer LLM

Der Context Document Generator ist aktuell ein reines String-Template. Wenn das LLM-Modell besser wird (groesseres Kontextfenster), koennte das Dokument um historische Daten erweitert werden (letzte Woche, Trends). Aktuell auf ~500 Tokens optimiert fuer Gemma 4 E2B (2B Modell).

Optimierung: Companion Chat ohne WebGPU

Der Chat funktioniert aktuell NUR mit WebGPU (Gemma lokal). Fuer Browser ohne WebGPU (Firefox, Safari) gibt es keinen Fallback. TODO: Server-Fallback via mana-llm Ollama-Endpoint integrieren, gesteuert ueber den bestehenden LLM Orchestrator Tier-System.

Feature-Luecke: Goal-UI

Goals haben kein eigenes UI ausser der Workbench "Ziele" Page. Es gibt keine Moeglichkeit fuer den Nutzer, eigene Goals frei zu definieren (nur Templates). TODO: Goal-Editor-Modal mit Metric/Target-Builder.

Feature-Luecke: Pulse Nudge-UI

Pulse Rules erzeugen Nudges, aber diese werden nur als OS-Notifications angezeigt (via Reminder-Scheduler). Es gibt keine In-App-Anzeige. TODO: NudgeToast Komponente oder Integration in den CompanionFeed.


16. Privacy-Garantien

Daten Verarbeitung Speicherung
Domain Events Lokal (Browser) IndexedDB, encrypted
Projections Lokal (Browser) In-Memory, nicht persistiert
Goals Lokal + Sync IndexedDB → mana-sync (encrypted)
Memory Facts Lokal (Browser) IndexedDB, encrypted
Context Document Lokal (Browser) In-Memory, nie persistiert
LLM Inference Tier 1: Browser (Gemma) Kein Server-Kontakt
Tier 2: mana-llm (Ollama) Context geht an eigenen Server
Tier 3: Cloud (Gemini) Nur mit explizitem Consent
Nudge Outcomes Lokal (Browser) IndexedDB, nicht synced
Tool Execution Lokal (Browser) Writes gehen in Module-Tabellen

Invariante: Sensitive Daten (Journal, Dreams, Finance, Food) werden nie an Tier 2/3 gesendet — erzwungen durch contentClass: 'sensitive' im LLM Orchestrator.


17. Migration: _activity → _events

Die _activity-Tabelle bleibt vorerst bestehen (Sync-Debugging). Langfristig:

  1. Phase 1-2: _events wird parallel zu _activity befuellt
  2. Phase 7: Alle Consumers (Activity-Page, Debug-Tools) auf _events umstellen
  3. Danach: _activity-Befuellung aus Hooks entfernen, Tabelle als deprecated markieren
  4. Naechste Major-Version: Tabelle loeschen

18. Testing-Strategie

Unit Tests

  • Event Bus: emit/subscribe/unsubscribe, ordering, no re-entrant loops
  • Projections: DaySnapshot korrekt aus Mock-Daten, Streak-Berechnung
  • Rules: Nudge-Generierung unter verschiedenen Bedingungen
  • Tools: Parameter-Validierung, Execute-Flow
  • Correlations: Pearson-Berechnung, Signifikanz-Filter

Integration Tests

  • Store emit() → EventBus → EventStore → Projection Update
  • Rule Engine → Nudge → UI → Outcome → Memory Update
  • Companion Chat → LLM Mock → Tool Call → Store Mutation → Event

E2E Tests

  • Morgenroutine-Ritual durchspielen: 5 Steps → Daten in 3 Modulen
  • Wasser-Ziel erreichen: 8x log_drink → GoalReached Event → Nudge
  • Companion-Frage: "Wie war meine Woche?" → Context Document → Antwort

19. Manuelles Testen (Anleitung)

Voraussetzungen

pnpm run mana:dev   # Startet das Dev-Server auf :5173

Oeffne http://localhost:5173 im Browser (Chrome/Edge mit WebGPU-Support fuer LLM).

1. Event Bus verifizieren

Oeffne die Browser DevTools Console und teste:

// Zugriff auf den Event Bus (global via Module-Import im App-Kontext)
// Am einfachsten: Daten erzeugen und in IndexedDB pruefen

// 1. Erstelle einen Task in der Todo-App
// 2. Oeffne DevTools → Application → IndexedDB → mana → _events
// 3. Dort sollte ein Event mit type="TaskCreated" erscheinen

// Alternativ via Console:
const db = await indexedDB.open('mana');
// Events sind in der _events Tabelle mit type, payload, meta

Was zu pruefen ist:

  • Neuen Task erstellen → TaskCreated Event in _events
  • Task erledigen → TaskCompleted Event
  • Drink loggen → DrinkLogged Event
  • Mahlzeit loggen → MealLogged Event
  • Ort besuchen → PlaceVisited Event

2. Projections testen

Die Projections sind live-reaktiv. Am einfachsten via Browser Console:

// DaySnapshot ist eine Svelte-reaktive Query.
// In einer Svelte-Komponente:
//   import { useDaySnapshot } from '$lib/data/projections';
//   const day = useDaySnapshot();
//   console.log(day.value);

// Zum manuellen Testen: Daten erzeugen und schauen ob DaySnapshot reagiert
// 1. Gehe zu /drink und logge ein Glas Wasser
// 2. Die DaySnapshot.drinks.water.ml sollte sich erhoehen
// 3. Gehe zu /todo und erstelle+erledige einen Task
// 4. DaySnapshot.tasks.completed sollte steigen

3. Companion Chat testen

  1. Navigiere zu /companion
  2. Klicke "Gespraech starten"
  3. WICHTIG: Der Chat nutzt @mana/local-llm (Gemma, ~500MB Download). Beim ersten Mal muss das Modell heruntergeladen werden. Das dauert je nach Verbindung 1-5 Minuten. WebGPU muss verfuegbar sein (Chrome 113+, Edge 113+, kein Firefox/Safari).
  4. Teste Nachrichten:
    • "Wie sieht mein Tag aus?" → Sollte DaySnapshot-Daten zusammenfassen
    • "Log mir ein Glas Wasser" → Sollte log_drink Tool aufrufen
    • "Erstelle einen Task: Einkaufen gehen" → Sollte create_task Tool aufrufen
    • "Wie viel Wasser habe ich heute getrunken?" → Nutzt Context Document

Ohne WebGPU (Fallback): Der Chat wird fehlschlagen wenn kein WebGPU verfuegbar ist. In dem Fall die Engine temporaer auf einen Server-Endpoint umbauen oder den Chat-Flow mit Mock-Responses testen.

4. Rituale testen

  1. Navigiere zu /companion/rituals
  2. Klicke "+ Neu" → waehle "Morgenroutine"
  3. Klicke den Play-Button neben der erstellten Routine
  4. Der RitualRunner zeigt Step fuer Step:
    • Step 1: "Glas Wasser trinken" → Klick "Ausfuehren" → loggt 250ml Wasser
    • Step 2: "Dein Tag auf einen Blick" → zeigt heutige Events
    • Step 3: "Heutige Tasks" → zeigt faellige Tasks
    • Step 4: "Deine Streaks" → zeigt Streak-Status
  5. "Weiter" / "Fertig" navigiert durch die Steps
  6. Pruefe in /drink ob das Wasser tatsaechlich geloggt wurde

5. Goals testen

Goals sind aktuell nur programmatisch testbar (kein UI). Via Console:

// In einer Svelte-Komponente oder via Hot-Module:
import { goalStore, GOAL_TEMPLATES } from '$lib/companion/goals';

// Goal aus Template erstellen
const goal = await goalStore.createFromTemplate(GOAL_TEMPLATES[0]); // "8 Glaeser Wasser"
console.log(goal);

// Jetzt ein Wasser loggen → Goal currentValue sollte steigen
// (der Goal-Tracker subscribed auf den Event Bus)

6. Memory + Correlations testen

Braucht mindestens 7-14 Tage an Daten. Zum Testen mit Seed-Daten:

// Pattern Extraction manuell ausfuehren:
import { extractAllPatterns } from '$lib/companion/memory';
await extractAllPatterns();

// Extrahierte Facts pruefen:
import { memoryStore } from '$lib/companion/memory';
const facts = await memoryStore.getFacts();
console.log(facts);

// Correlations berechnen:
import { computeCorrelations } from '$lib/data/projections';
const corrs = await computeCorrelations();
console.log(corrs);

Hinweis: Mit weniger als 7 Events pro Typ oder 14 Tagen Daten liefern die Extractors und Correlations keine Ergebnisse — das ist beabsichtigt, um Rauschen zu vermeiden.

7. Pulse Rules testen

Rules laufen ueber den Reminder-Scheduler (30s Intervall). Zum manuellen Test:

import { evaluateRules } from '$lib/companion/rules';
import { useDaySnapshot } from '$lib/data/projections/day-snapshot';
import { useStreaks } from '$lib/data/projections/streaks';

// In einer Komponente:
const day = useDaySnapshot();
const streaks = useStreaks();
const nudges = evaluateRules(day.value, streaks.value, []);
console.log(nudges);

8. IndexedDB direkt inspizieren

Alle Companion-Daten liegen in IndexedDB (mana Database):

Tabelle Inhalt
_events Domain Event Stream (type, payload, meta)
companionGoals Aktive Ziele mit currentValue
companionConversations Chat-Gespraeche
companionMessages Chat-Nachrichten + Tool-Calls
rituals Erstellte Rituale
ritualSteps Ritual-Steps (pro Ritual)
ritualLogs Completion-Logs
_memory Extrahierte Muster (nach extractAllPatterns)
_nudgeOutcomes Nudge-Reaktionen
pendingProposals Staged AI-Intents (siehe §20)

Oeffne: DevTools → Application → IndexedDB → mana → [Tabelle]


20. AI Workbench (ab 2026-04-14)

Der Companion wird schrittweise vom Chatbot-mit-Tools zum zweiten Akteur im System: er arbeitet parallel zum Menschen in den bestehenden Modulen, User sieht jede Aenderung inline und approved / reverted wo noetig. Fundament laeuft; Missions + Runner folgen.

20.1 Actor-Modell

Jeder Write im System traegt ab jetzt einen expliziten Actor. Source of Truth ist die Data-Schicht (Events + Records + Sync-Payload), nicht ambient Kontext.

type Actor =
  | { kind: 'user' }
  | { kind: 'ai'; missionId; iterationId; rationale }
  | { kind: 'system'; source: 'projection' | 'rule' | 'migration' };
  • Events: EventMeta.actor: Actor (required — kein Legacy-Fallback)
  • Records: Dexie-Hooks stempeln __lastActor + feldweise __fieldActors (parallel zu __fieldTimestamps)
  • Sync-Payload: _pendingChanges.actor geht an mana-sync (Go/Postgres-Migration offen)
  • Ambient-Hilfe: runAs(actor, fn) an definierten Boundaries — Primitive frieren den Actor synchron ein, bevor er ueber setTimeout / queueMicrotask verloren geht

Code: apps/mana/apps/web/src/lib/data/events/actor.ts

20.2 Policy-Layer

AI-Writes werden nicht automatisch ausgefuehrt. Per-Tool-Policy entscheidet:

Decision Bedeutung
auto Direkt ausfuehren, Actor in Events + Records stempeln
propose Als Proposal in pendingProposals stagen, User approved inline
deny Refuse — Tool niemals fuer AI zugaenglich

Default (DEFAULT_AI_POLICY): lesendes / append-only self-state → auto, alles Mutierende → propose. User / System Actors umgehen die Policy.

Code: apps/mana/apps/web/src/lib/data/ai/policy.ts

20.3 Proposals

interface Proposal {
  id, createdAt, expiresAt?, status: 'pending' | 'approved' | 'rejected' | 'expired';
  actor: { kind: 'ai', missionId, iterationId, rationale };
  missionId?, iterationId?;           // fuer Workbench-Queries indiziert
  intent: { kind: 'toolCall', toolName, params };
  decidedAt?, decidedBy?, userFeedback?;
}

Proposals sind lokal only — sie syncen nicht. Der approved Write syncet normal durch den Modulpfad, mit dem AI-Actor attribuiert.

Approval-Flow: approveProposal(id) laeuft das gespeicherte Intent unter runAsAsync(aiActor, () => executeToolRaw(...)). executeToolRaw umgeht die Policy — sonst wuerde sie das Intent sofort wieder in ein Proposal zurueckwerfen.

Code: apps/mana/apps/web/src/lib/data/ai/proposals/

20.4 Ghost-UI in Pilot-Modul (todo)

<AiProposalInbox module="todo" /> ist die opt-in Komponente: pro Modulseite ein Einzeiler. Rendert pending Proposals als dashed Ghost-Karten ueber dem echten Content — zero UI wenn keine anstehen. Approve / Reject inline. Filter ueber Tool-Registry: Proposal fuer create_task landet auf /todo, create_event auf /calendar, etc.

Code:

  • apps/mana/apps/web/src/lib/components/ai/AiProposalInbox.svelte
  • apps/mana/apps/web/src/lib/data/ai/proposals/queries.ts

20.5 Roadmap

  • Schritt 1 — Actor-Attribution (Events + Records + Sync-Payload)
  • Schritt 2 — Policy-Config + pendingProposals + Propose-Path im Executor
  • Schritt 3 — Ghost-UI im Todo-Pilot (<AiProposalInbox />)
  • Schritt 4 — Missions-Datenmodell + Planner-LLM-Task
    • Dexie aiMissions (v18), cross-device synct
    • aiPlanTask als LlmTask (minTier browser, contentClass personal)
    • Strikter JSON-Parser mit Tool-Allowlist + Rationale-Zwang
  • Schritt 5 — In-App MissionRunner (Foreground-Tick in (app)/+layout.svelte)
    • runMission(id, deps) + runDueMissions(now, deps) — injiziert, testbar
    • Default-Input-Resolver für Notes / Kontext / Goals
    • 60-Sekunden-Tick, Overlap-Guard, idempotent
  • Schritt 6 — Missions-UI (Create/Edit-Form + Iteration-History)
  • Schritt 7 — Workbench-Timeline-Lens (cross-module AI-Aktivitaet)
  • Schritt 8 — Server-side mana-ai Bun-Service (offline-of-tab Runs)

20.6 Offene Follow-ups

  • mana-sync (Go) + Postgres-Migration fuer actor-Feld im pendingChange-Payload
  • System-Actor in Projections + Rule-Engine wrappen (heute im User-Kontext)
  • Inbox-Rollout auf weitere Module (Kalender, Notes, …) sobald Tools dort in DEFAULT_AI_POLICY eingetragen sind

20.7 Manueller Test

Browser-Console auf /todo:

const { executeTool } = await import('/src/lib/data/tools/executor');
await executeTool(
  'create_task',
  { title: 'Test von der KI' },
  { kind: 'ai', missionId: 'demo', iterationId: '1', rationale: 'Beispiel-Proposal' }
);

Ghost-Karte erscheint sofort ueber der Task-Liste.