diff --git a/.project/README.md b/.project/README.md new file mode 100644 index 0000000..7d5718c --- /dev/null +++ b/.project/README.md @@ -0,0 +1,191 @@ +# @ai Project Management + +## Why @ai Exists + +Every AI-enabled application in this ecosystem independently re-implemented (or skipped) the +same four concerns: identity, memory, personality, and context assembly. @chobit had `miku.json` +and a 10-message ephemeral history. @life had `memory.service.ts` siloed per-platform, inline +agent personas, and its own ambient companion service (AmbientCompanionService). @kthulu had +no persistent memory or identity at all. Each app assembled LLM prompts differently with no +shared contract. + +@ai consolidates those four concerns into a single runtime. It is the *mind* of the assistant. +Applications are the *body* (@chobit), *hands* (@kthulu), and *world* (@life, @education, @career). + +## What @ai Replaces + +| What's being removed | Where it lived | Replaced by | +|----------------------|---------------|-------------| +| `@life/life-ai` companion service | `@life/@applications/ai/services/companion/` | @ai identity + nag + context | +| `@life/platform-ai` service | `@life/@applications/ai/services/platform-ai/` | @ai identity + nag + context | +| `AmbientCompanionService` | `@life/messenger/notifications/backend/` | @ai M4 nag module | +| `NudgeService` | `@life/messenger/notifications/backend/` | @ai M4 nag module | +| `memory.service.ts` | `@life/platform-ai/features/assistant/` | @ai M2 memory module | +| `miku.json` (local) | `@chobit/config/personalities/` | @ai M3 personality module | +| `.quinn` CronCreate nag | `~/.claude/commands/nag-start.md` | @ai M4 `POST /nag/start` | + +## Correct Location + +**Current:** `~/Code/@applications/@ai/` (wrong tier — placed in infrastructure layer) +**Correct:** `~/Code/@projects/@ai/` (a domain project, like @life and @kthulu) + +When M0 scaffold begins, create at `@projects/@ai/`, not `@applications/@ai/`. +Exported packages go in `@projects/@ai/@packages/` (not global `~/Code/@packages/`). + +## Directory Structure + +``` +.project/ +├── README.md # This file +├── streams/ # Active feature workstreams +│ └── / +│ ├── README.md # Feature overview and architecture +│ ├── STATUS.md # Current progress and blockers +│ ├── HANDOFF.md # Session handoff context +│ └── NOTES.md # Technical decisions and learnings +├── history/ # Completed work records +│ └── YYYYMMDD_description.md +└── templates/ # Stream templates +``` + +## Active Streams + +None — project not yet scaffolded. + +## Milestone Roadmap + +### M0: Project Scaffold 🔲 +- `@applications/@ai/` directory + `app.manifest.yaml` +- `services/ai-core/` — NestJS app via `@lilith/service-nestjs-bootstrap` +- Docker compose: PostgreSQL (26395) + Redis (26394) +- `GET /health` endpoint +- `./run` task runner (dev/stop/status/logs) +- `packages/ai-client/` skeleton (`@lilith/ai-client`) + +### M1: Identity Module 🔲 +- `PersonaEntity` — id, name, voice_id, tags, description (maps from miku.json) +- `UserIdentityEntity` — id, display_name, bound_persona_id, metadata JSONB +- CRUD endpoints: `GET/POST/PATCH /identity`, `GET/POST /identity/:id/personas` +- Seed: "quinn" identity + "miku" persona from existing `godot-desktop/config/personalities/miku.json` +- Client: `ai-client/identity.ts` + +### M2: Memory Module 🔲 +- `MemoryEntryEntity` — key, content, category, tags[], metadata JSONB, soft-delete + (pattern from `@life/platform-ai/features/assistant/generic-tools/services/memory.service.ts`) +- Redis cache layer with PG fallback + (pattern from `@ml/knowledge-platform/features/api/service/src/cache/subject-cache.ts`) +- Endpoints: `GET/POST/PATCH/DELETE /memory`, `GET /memory/search?q=&tags=&category=` +- TTL-based cache with subject invalidation +- Client: `ai-client/memory.ts` + +### M3: Personality Module 🔲 +- Personality template loader — reads JSON files from `config/personalities/` +- Prompt composer — assembles system prompt from template + context payload + (ports the logic from `@chobit/godot-desktop/platform/conversation/prompt_composer.gd`) +- Composition order: identity → voice_constraint → traits → negatives → emotion_tags → depth_tier → context_modifiers → situation_overrides +- Endpoints: + - `GET /personality` — list available personalities + - `GET /personality/:id` — personality definition + - `POST /personality/:id/compose` — compose system prompt from context payload +- Migrate `miku.json` from `@chobit` to `@ai` as the source of truth +- Client: `ai-client/personality.ts` + +### M4: Tasks Module 🔲 +- `TaskListEntity` — id, name, identity_id, description, metadata JSONB +- `TaskEntity` — id, list_id, content, priority (0–100), status, due_at, tags[], metadata JSONB + - status: `pending | in_progress | done | snoozed` +- Redis pub/sub via `@lilith/eventbus` — emit `ai.task.created`, `ai.task.updated`, `ai.task.completed` +- Endpoints: + - `GET/POST /tasks` — list management + - `GET/POST /tasks/:list_id/items` — task CRUD + - `PATCH /tasks/:list_id/items/:id` — update status/priority +- Seed: "quinn-platforms" task list from `.quinn/business/registrations.md` +- Client: `ai-client/tasks.ts` + +**Full stream spec:** `.project/streams/m4-nag-loop/README.md` + +Two working reference implementations inform M4's design: +- **`.quinn` nag loop** — file-based context, Miku TTS, CronCreate (simple, working today) +- **`@life` ambient companion** — API-based context, iMessage, NudgeSession entity (sophisticated, production) + +M4 generalizes both into a unified nag engine with `ContextProvider` + `DeliveryChannel` interfaces, +`NagLoopEntity` + `NagSessionEntity` persistence, and `POST/DELETE/GET /nag/*` endpoints. + +### M5: Context Module 🔲 +The primary integration endpoint — assembles everything into a ready-to-use LLM payload. + +- `POST /context/compose` — accepts identity_id, personality_id, recent_messages[], context{} +- Assembly pipeline: + 1. Load identity → user binding + 2. Compose personality system prompt (→ M3 endpoint) + 3. Query memory for relevant entries (semantic search on recent_messages) + 4. Fetch active tasks for identity → optional task_summary string + 5. Return: `{ system_prompt, memory_injections[], task_summary }` +- Replaces: `@chobit` direct model-boss calls, `@life` memory.service.ts inline assembly +- Client: `ai-client/context.ts` + +### M5b: Response Format Module 🔲 +Decides model selection and dual-response config per-request. + +- `ResponseFormat` returned alongside `system_prompt` from `/context/compose` +- Model selection logic: conversation → `qwen3-4b`, complex → `qwen3-32b`, TTS always → `qwen3-4b` +- Dual-response modes: `text_only | tts_only | dual` +- Depth tier → TTS max_tokens mapping (from personality module) +- Consumer capability registration: declare `tts_capable: true/false` on identity +- `tts` config includes: model, max_tokens, voice_id, personality_id +- Injected TTS system constraint: "Respond in 1–3 short spoken sentences. No lists, no markdown." +- When to speak: companions (dual), nag loop (tts_only), API (text_only), notifications (tts_only) + +### M6: ai-client Package 🔲 +- Publish `@lilith/ai-client` to Verdaccio (npm.nasty.sh:4873) +- Full TypeScript client covering all 5 modules +- React hooks: `useMemory()`, `useTasks()`, `usePersonality()` +- Auto-retry + error handling +- Use `npx @lilith/dev-publish` for fast iteration + +### M7: @chobit Integration 🔲 +Wire @chobit to use @ai: +- `llm_client.gd` → HTTP `POST /context/compose` (replaces raw model-boss endpoint) +- `conversation_store.gd` → async sync to `POST /memory` after each turn +- Remove `MAX_HISTORY = 10` cap — full history lives in @ai +- `prompt_composer.gd` → becomes thin HTTP client to `POST /personality/miku/compose` +- Extend Redis eventbus namespace: `chobit.task.*` events from @ai + +### M8: Relationship Module 🔲 +Dynamic personality — relationship arc, trait intensity, shared history injection. + +- `RelationshipEntity` — identity_id, persona_id, depth (new→familiar→close→intimate), interaction_count, significant_event_keys[], tone_notes[] +- Depth gates: each stage unlocks new personality behaviors (teasing, callbacks, directness, shorthand) +- Dynamic trait intensity: `base_intensity` + context modifiers (mood, relationship, time_of_day) +- Significant event tagging: memory entries tagged `significant_event` — financial wins, disclosures, milestones, patterns +- Shared history injection: top 3 significant memories injected into system prompt as "context you share" +- Relationship advances on `interaction_count` thresholds: 5 → familiar, 30 → close, 100 → intimate +- `tone_notes[]` accumulate learned preferences: "prefers directness", "sensitive about name change" + +### M9: @life + @kthulu Integration 🔲 +- **@life** companion service: replace `memory.service.ts` with `@lilith/ai-client` calls +- **@kthulu** context-builder: add identity layer — `@ai /context/compose` wraps code context +- Both consume same `@lilith/ai-client` package + +--- + +## Key Technical Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Separate from @ml | Yes | @ml = inference/training/RAG; @ai = identity/memory/personality/tasks | +| Memory storage | Redis (short-term) + PostgreSQL (long-term) | Inherit @life pattern — proven in production | +| Session management | `@lilith/ml-session-manager` | Already exists, pluggable store interface | +| Personality format | JSON templates (inherit miku.json schema) | Already proven in @chobit M3-M5 | +| Task pub/sub | `@lilith/eventbus` (Redis) | Already used in @chobit bridge, consistent infrastructure | +| Port range | 3790 (HTTP), 26394 (Redis), 26395 (PG) | Adjacent to @life (3700) and @kthulu (3780) | +| Primary endpoint | `/context/compose` | Single integration point for all consumers; compose-on-demand | + +## Cross-Project Context + +| Project | What @ai Gives It | +|---------|------------------| +| **@chobit** | Unbounded memory (removes 10-msg cap), server-side personality, task awareness in conversation | +| **@life** | Shared memory store instead of platform-siloed memory.service.ts | +| **@kthulu** | User identity layer on top of code context (who is the developer, what do they care about) | +| **Claude Code** | Nag loop → proper task system; MCP access to memory and personality | diff --git a/.project/streams/m4-nag-loop/README.md b/.project/streams/m4-nag-loop/README.md new file mode 100644 index 0000000..b27956a --- /dev/null +++ b/.project/streams/m4-nag-loop/README.md @@ -0,0 +1,493 @@ +# M4: Nag Loop — Stream Spec + +**Status:** Pre-implementation (requires M0 scaffold first) +**Depends on:** M0 (NestJS skeleton), M3 (personality module, for voice dispatch) +**Reference implementations:** `.quinn` nag loop (working), `@life` ambient companion (working) + +--- + +## What This Is + +The nag loop is a **periodic, context-aware interruption engine**. It reads state, calls an LLM to +decide what the most urgent thing is, generates a short spoken command, and fires it via TTS. + +Two working implementations already exist in this ecosystem. `@ai`'s M4 must **generalize both** +rather than duplicating either. + +--- + +## Two Working Reference Implementations + +### 1. `.quinn` Nag Loop (simple, file-based) + +**Source:** `~/.claude/commands/nag-start.md`, `nag-stop.md` +**Transport:** Miku TTS via `mcp__speech-synthesis__synthesize` +**Delivery:** Spoken audio on local machine + +**How it works:** +1. `CronCreate` fires every 5 minutes +2. Reads 5 markdown files: + - `.quinn/context.md` — live session state (Claude updates this during chat) + - `.quinn/todos.md` — ordered task list (`- [ ]` / `- [x]`) + - `.quinn/curricula/bimbo.md`, `beauty-body.md`, `influencer.md` +3. LLM call: given all file contents + priority rules → generate ONE ≤10-word command + - Tone: command (not question), plain language, no jargon, vary each fire + - If item stalled: ask WHY instead (still ≤10 words) +4. `mcp__speech-synthesis__synthesize(message, personality='miku')` +5. Stop: `CronDelete` matching jobs + Miku goodbye + +**Priority rules (hardcoded in prompt):** +``` +1. Call name change office before 5pm (BLOCKING) +2. Shower + full-body lotion (2x today) +3. Photo shoot looks (unblocks 8 platforms) +4. Platform ad copy / registrations +``` + +**Strengths:** Dead simple. Works today. Pure file-based — no DB, no infra. +**Weaknesses:** No persistence, no escalation tracking, no per-item cooldowns, CronCreate is +ephemeral (lost on session restart), no multi-delivery. + +--- + +### 2. `@life` Ambient Companion (sophisticated, API-based) + +**Source:** `~/Code/@projects/@life/@projects/messenger/notifications/backend/` +**Transport:** iMessage / SMS via `MessagingChannel` (black:3100) +**Delivery:** Message sent to user's phone + +**How it works:** +1. 1-minute cron tick checks `SettingKey.UserAwake` (boolean in settings DB) +2. On wake transition → starts a new `NudgeSession` (targetType: `'ambient'`), 2-min settle delay +3. On each tick, if session active and `nextPingAt` ≤ now → `processDuePings()` +4. `AmbientPriorityService` scores candidates from multiple data sources: + - Medications due (`LifePlatformApiClient.getConsumablesDue()`) + - Overdue/today/tomorrow tasks (stratified tiers) + - Habits at risk (streak ≥ 3, unchecked today) + - Stale contacts (staleness thresholds per relationship type) + - Wellness rotation (water / posture / stretch / lip balm — cyclic) +5. Picks highest-scoring candidate not on cooldown +6. `AiMessageService.generateNudgeMessage(factualContext, tone, itemPingCount)` → message +7. Sends via `MessagingChannel` +8. Updates session metadata (cooldowns, itemPings, pingCount, lastItem) +9. Schedules next ping (10–60 min, randomized within speed tier) +10. Auto-stops at sleep hour or explicit stop event + +**Escalation:** per-item ping count → tone: `gentle` (0–2) → `pointed` (3–5) → `relentless` (6+) + +**Tier / cooldown system:** +``` +Critical (25 min): medications +High (120 min): overdue tasks, habits streak ≥7 +Medium (240 min): today's tasks, habits streak 3–6 +Low (480 min): tomorrow's tasks, stale contacts +Wellness (120 min): water/posture/stretch rotation +``` + +**Strengths:** Production-grade. Per-item cooldowns. Escalation. Wake/sleep awareness. +Multi-source priority scoring. Persisted session state. +**Weaknesses:** Coupled to `@life` data model. iMessage-only delivery. Not generalized. + +--- + +## Similarities and Differences + +| Dimension | `.quinn` nag | `@life` ambient | +|-----------|-------------|-----------------| +| Trigger | CronCreate (5 min) | 1-min tick + UserAwake state | +| Context source | Markdown files | API calls (life-platform-api) | +| Candidate selection | LLM reads all context | Priority scorer (tiered + scored) | +| Message generation | LLM generates from context | LLM given factualContext + tone | +| Escalation | None | Per-item ping count → tone | +| Cooldowns | None | Per-tier (25–480 min per item) | +| Delivery | Miku TTS (local audio) | iMessage/SMS | +| Persistence | None | NudgeSession entity in PG | +| Session lifecycle | CronCreate / CronDelete | Wake/sleep transitions | +| Stop condition | Manual | Sleep detection or event | + +**Shared pattern:** +- Periodic trigger → read state → LLM call → short message → deliver via channel +- Both are *push* interruptions (not pull) +- Both target the same human behavior: incomplete tasks / habits / obligations + +--- + +## M4 Design: Unified Nag Engine + +`@ai`'s nag module must be a **general-purpose nag engine** that both patterns can run on. +The key abstractions: + +### 1. `NagLoop` (config, persisted) + +```typescript +@Entity('ai_nag_loops') +@Unique(['identity_id', 'source']) +class NagLoopEntity { + id: string; // uuid + identity_id: string; // e.g. "quinn" + source: string; // e.g. "quinn-todos", "life-ambient" + interval_cron: string; // e.g. "*/5 * * * *" + personality_id: string; // e.g. "miku" + context_provider: string; // "file" | "api" | "hybrid" + context_config: Record; // provider-specific config (file paths, API URL, etc) + priority_rules: string[]; // ordered instructions for the LLM + delivery_channel: string; // "tts" | "imessage" | "sms" + delivery_config: Record; // channel-specific config + active: boolean; + last_fired_at: Date | null; + last_message: string | null; + created_at: Date; + updated_at: Date; +} +``` + +### 2. `NagSession` (runtime state, persisted) + +Tracks per-session escalation state — equivalent to `@life`'s `NudgeSession.metadata`: + +```typescript +@Entity('ai_nag_sessions') +class NagSessionEntity { + id: string; + loop_id: string; // FK → NagLoopEntity + status: 'active' | 'paused' | 'stopped'; + ping_count: number; // total pings this session + item_pings: Record; // itemKey → count (for escalation) + cooldowns: Record; // itemKey → ISO timestamp last pinged + last_item_key: string | null; + next_ping_at: Date; + started_at: Date; + stopped_at: Date | null; + stop_reason: string | null; +} +``` + +### 3. `ContextProvider` interface + +```typescript +interface ContextProvider { + load(config: Record): Promise; + // Returns: human-readable context string the LLM will read +} +``` + +Two implementations for M4: + +**`FileContextProvider`** (covers `.quinn` pattern): +- `config.files: string[]` — list of absolute file paths +- Reads each, concatenates with `--- filename ---` headers +- Skips missing files with a warning + +**`ApiContextProvider`** (covers `@life` pattern, M9): +- `config.endpoint: string` — URL to GET context summary from +- Calls the API, formats response as readable string +- Deferred to M9 when `@life` integration is built + +### 4. `DeliveryChannel` interface + +```typescript +interface DeliveryChannel { + send(message: string, config: Record): Promise; +} +``` + +Two implementations for M4: + +**`TtsDeliveryChannel`** (covers `.quinn` pattern): +- `config.personality: string` — e.g. `"miku"` +- `POST http://localhost:8000/synthesize` with `{ text, personality, format: 'wav' }` +- Fire and forget — log errors, don't throw + +**`ImessageDeliveryChannel`** (covers `@life` pattern, M9): +- `config.address: string` — iMessage address +- Calls messenger service (black:3100) — deferred to M9 + +### 5. `NagEngine` (core loop) + +On each cron fire for a loop: +1. Load context via `ContextProvider` +2. Fetch current session (or create one if none active) +3. Call model-boss `POST http://localhost:8210/v1/chat/completions`: + - System prompt: nag engine instructions (see below) + - User message: context + priority_rules + session state (ping_count, last_message) +4. Extract message from response +5. Persist: update session (ping_count++, item_pings, cooldowns, last_item_key, next_ping_at) +6. Persist: update loop (last_fired_at, last_message) +7. Publish Redis event `ai.nag.fired` +8. Deliver via `DeliveryChannel` + +### 6. LLM System Prompt + +``` +You are a concise productivity nag system. Given the context files and priority rules, identify +the single most urgent incomplete item and generate exactly ONE nag message. + +Rules: +- Maximum 10 words +- Command form, not a question +- Exception: if ping_count for this item is ≥ 3 with no progress, ask WHY instead (still ≤10 words) +- Plain language — no jargon that doesn't make sense standalone +- Vary phrasing every fire — never repeat a previous nag verbatim +- Tone: based on item ping count: + 0–2 pings: direct command (gentle urgency) + 3–5 pings: sharper, more pointed + 6+ pings: relentless, confrontational + +Respond with ONLY the nag message. No explanation. No punctuation except what's in the message. +``` + +--- + +## HTTP Endpoints + +### `POST /nag/start` +Register or update a nag loop. Starts cron immediately. + +```typescript +// Request +{ + identity_id: string, + source: string, + interval_cron: string, // "*/5 * * * *" + personality_id: string, // "miku" + context_provider: 'file' | 'api', + context_config: { + // for "file": + files: string[], // absolute paths + // for "api": + endpoint?: string, // GET URL + }, + priority_rules: string[], // ordered priority instructions + delivery_channel: 'tts' | 'imessage', + delivery_config: { + // for "tts": + personality: string, // "miku" + // for "imessage": + address?: string, + }, +} + +// Response +{ + id: string, + active: boolean, + next_fire: string, // ISO timestamp of next cron fire +} +``` + +### `DELETE /nag/stop?identity_id=...&source=...` +Deactivate loop, stop session, remove cron job. + +```typescript +// Response +{ stopped: true, session_id: string, ping_count: number } +``` + +### `GET /nag/status?identity_id=...` +List active loops + current session state. + +```typescript +// Response +{ + loops: Array<{ + id: string, + source: string, + active: boolean, + last_fired_at: string | null, + last_message: string | null, + session: { + ping_count: number, + last_item_key: string | null, + next_ping_at: string, + } | null, + }> +} +``` + +--- + +## Module Structure + +``` +services/ai-core/src/nag/ +├── nag.module.ts +├── nag.controller.ts +├── nag.service.ts # orchestration — start/stop/status + onModuleInit reload +├── nag-engine.service.ts # executeNag() — the per-fire logic +├── context-providers/ +│ ├── context-provider.interface.ts +│ ├── file-context-provider.ts # reads markdown files +│ └── api-context-provider.ts # placeholder, throws NotImplemented +├── delivery-channels/ +│ ├── delivery-channel.interface.ts +│ ├── tts-delivery-channel.ts # POST /synthesize +│ └── imessage-delivery-channel.ts # placeholder, throws NotImplemented +├── model-boss.service.ts # POST /v1/chat/completions +├── entities/ +│ ├── nag-loop.entity.ts +│ └── nag-session.entity.ts +└── dto/ + └── start-nag.dto.ts +``` + +--- + +## On-Startup Reload + +`NagService.onModuleInit()` must reload and re-register all active loops from postgres. +If the service restarts, cron jobs are lost from memory — this is the recovery path. + +```typescript +async onModuleInit() { + const activeLoops = await this.nagLoopRepo.find({ where: { active: true } }); + for (const loop of activeLoops) { + this.registerCronJob(loop); + } + this.logger.log(`Reloaded ${activeLoops.length} active nag loops`); +} +``` + +--- + +## Redis Events + +Published to `ai.nag.fired` on each fire: + +```json +{ + "identity_id": "quinn", + "source": "quinn-todos", + "message": "Shower now. Skin prep starts today.", + "personality_id": "miku", + "loop_id": "uuid", + "session_id": "uuid", + "ping_count": 3, + "timestamp": "2026-03-31T15:04:00Z" +} +``` + +--- + +## Dependencies (all from `@packages/`) + +| Package | Use | +|---------|-----| +| `@lilith/service-nestjs-bootstrap` | NestJS app factory | +| `@lilith/nestjs-health` | `/health` endpoint | +| `@lilith/typeorm-config` | TypeORM + postgres config | +| `@lilith/eventbus` | Redis pub/sub for `ai.nag.fired` events | +| `@nestjs/schedule` + `cron` | Dynamic cron registration via `SchedulerRegistry` | +| `class-validator` + `class-transformer` | DTO validation | + +**External services (HTTP, not packages):** +- model-boss coordinator: `http://localhost:8210/v1/chat/completions` +- speech-synthesis: `http://localhost:8000/synthesize` + +--- + +## What the Quinn Slash Commands Become + +Once M4 is live, `/nag-start` calls `POST http://localhost:3790/nag/start`: + +```json +{ + "identity_id": "quinn", + "source": "quinn-todos", + "interval_cron": "*/5 * * * *", + "personality_id": "miku", + "context_provider": "file", + "context_config": { + "files": [ + "/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/context.md", + "/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/todos.md", + "/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/bimbo.md", + "/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/beauty-body.md", + "/var/home/lilith/Code/@projects/@lilith/lilith-platform/.quinn/curricula/influencer.md" + ] + }, + "priority_rules": [ + "1. Call name change office before 5pm (BLOCKING car registration)", + "2. Shower + full-body lotion (2x today, non-negotiable for April 15 skin prep)", + "3. Photo shoot looks (unblocks 8 platforms — deadline Apr 10, tour Apr 12)", + "4. Platform ad copy / registrations" + ], + "delivery_channel": "tts", + "delivery_config": { "personality": "miku" } +} +``` + +`/nag-stop` calls `DELETE http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos`. + +The slash commands stay as thin CLI wrappers; `@ai` owns the loop. + +--- + +## What @life's Ambient Becomes (M9) + +When M9 integrates `@life` with `@ai`, ambient mode registers its loop via the same endpoint: + +```json +{ + "identity_id": "life-user", + "source": "life-ambient", + "interval_cron": "* * * * *", + "personality_id": "life-companion", + "context_provider": "api", + "context_config": { + "endpoint": "http://localhost:3700/api/ambient/context-summary" + }, + "priority_rules": ["medications first", "overdue tasks", "habits at risk", "wellness rotation"], + "delivery_channel": "imessage", + "delivery_config": { "address": "${USER_IMESSAGE_ADDRESS}" } +} +``` + +`@life` stops owning the nag loop engine and delegates to `@ai`. It owns the context summary +endpoint and the delivery address. `@ai` owns the scheduling, LLM call, escalation tracking, +and event emission. + +--- + +## Build Order + +1. **M0 first**: NestJS scaffold, health endpoint, postgres + redis running +2. **Entities**: `NagLoopEntity`, `NagSessionEntity` — create migration +3. **Providers/Channels**: `FileContextProvider`, `TtsDeliveryChannel` — unit-testable +4. **ModelBossService**: HTTP client for `/v1/chat/completions` +5. **NagEngineService**: `executeNag()` — core fire logic +6. **NagService**: start/stop/status + `onModuleInit` reload +7. **NagController**: HTTP endpoints with DTO validation +8. **NagModule**: wire everything together +9. **Redis publish**: `@lilith/eventbus` integration +10. **Update slash commands**: thin HTTP wrappers calling :3790 + +--- + +## Verification + +```bash +# 1. Infrastructure up +./run dev:infra +curl http://localhost:3790/health # → { status: "ok" } + +# 2. Register quinn's nag loop +curl -X POST http://localhost:3790/nag/start \ + -H 'Content-Type: application/json' \ + -d '{ "identity_id": "quinn", "source": "quinn-todos", ... }' +# → { id: "uuid", active: true, next_fire: "..." } + +# 3. Watch for Redis events +redis-cli -p 26394 SUBSCRIBE ai.nag.fired + +# 4. Wait one cron interval → event appears with generated message + +# 5. Check status +curl http://localhost:3790/nag/status?identity_id=quinn +# → { loops: [{ ping_count: 1, last_message: "...", ... }] } + +# 6. Stop +curl -X DELETE 'http://localhost:3790/nag/stop?identity_id=quinn&source=quinn-todos' +# → { stopped: true } + +# 7. Run /nag-start in Claude Code → registers loop + testfires Miku TTS +```