---

# MODULE 4: CONTEXT RETRIEVAL
**Stories**: 4.01 → 4.07 (run sequentially)
**Depends on**: Module 1 + Module 3

*(Stories 4.01–4.07 follow same pattern — BinduHydrator scatter tasks built one by one,
then assembled, then ConversationReplayEngine. Full specs follow same template as above.
Omitted here for brevity — see Module 3 as the template.)*

---

# MODULES 5-9: [Following same atomic pattern]

*(Full specs for Modules 5-9 follow the identical story structure established above.
Each story: 1 file, 1 function/class, 3 BB + 3 WB + 1 IT tests, 8-15 iterations.)*

---

# MODULE 4: CONTEXT RETRIEVAL
**Stories**: 4.01 → 4.07 (run sequentially)
**Depends on**: Module 1 + Module 3
**What it builds**: BinduHydrator — the scatter/gather engine that fans out to fetch all
AIVA context before she picks up a call. Plus ConversationReplayEngine for recalling past calls.

---

### STORY 4.01: BinduHydrator — Base Class + Session Init

**File**: `core/hydrators/bindu_hydrator.py` (new file)
**Depends on**: Story 1.03 (Redis schema module)
**Estimated iterations**: 6-8

**One-liner**: Create the BinduHydrator base class that initialises a hydration session and writes the root Redis key for tracking scatter task status.

**Implementation skeleton**:
```python
# core/hydrators/bindu_hydrator.py

from dataclasses import dataclass
from typing import Optional
from datetime import datetime

@dataclass
class HydrationSession:
    session_id: str
    aiva_call_id: str
    started_at: datetime
    status: str  # "pending" | "hydrating" | "ready" | "expired"
    tasks_dispatched: int = 0
    tasks_completed: int = 0

class BinduHydrator:
    """
    Scatter/gather engine: fans out N parallel Redis-backed tasks,
    aggregates into a single ROYAL_CHAMBER_CONTEXT XML envelope.
    """
    HYDRATION_DEADLINE_MS = 500  # must complete within 500ms

    def __init__(self, redis_client, postgres_client, qdrant_client):
        self.redis = redis_client
        self.pg = postgres_client
        self.qdrant = qdrant_client

    async def start_hydration(self, session_id: str, call_id: str) -> HydrationSession:
        """
        Creates HydrationSession, writes aiva:context:{session_id} = 'pending' to Redis.
        Returns HydrationSession dataclass.
        """
        ...
```

**Acceptance criteria**:
- [ ] `HydrationSession` dataclass exists with all 6 fields
- [ ] `start_hydration()` writes `aiva:context:{session_id}` to Redis with TTL 300s
- [ ] Returns populated `HydrationSession` with status="pending"
- [ ] `HYDRATION_DEADLINE_MS = 500` constant present

**Test plan**:
```
BB1: start_hydration("s1", "c1") → Redis key aiva:context:s1 = "pending"
BB2: Redis key has TTL ≤ 300s
BB3: Returned HydrationSession.status == "pending"

WB1: HydrationSession.started_at is UTC datetime
WB2: tasks_dispatched=0, tasks_completed=0 on init
WB3: Redis SETEX used (not SET without TTL)

IT1: start_hydration() on real Redis → key readable, TTL confirmed
```

---

### STORY 4.02: BinduHydrator — Redis Scatter Task (L1 Fetch)

**File**: `core/hydrators/bindu_hydrator.py` (extend Story 4.01)
**Depends on**: Story 4.01
**Estimated iterations**: 5-7

**One-liner**: Add `_scatter_redis_tasks()` — reads AIVA working state + Kinan active directives from Redis in parallel using asyncio.gather.

**Implementation skeleton**:
```python
async def _scatter_redis_tasks(self, session_id: str) -> dict:
    """
    Fires 2 parallel Redis reads:
    1. aiva:state:{session_id}       → AIVA working state (JSON)
    2. kinan:directives:active        → Kinan's current active directives (list)
    Returns: {"aiva_state": {...}, "kinan_directives": [...]}
    """
    ...
```

**Acceptance criteria**:
- [ ] Uses `asyncio.gather()` for both reads (parallel, not sequential)
- [ ] Returns dict with `aiva_state` and `kinan_directives` keys
- [ ] If either key is missing, returns `{}` / `[]` (no crash)
- [ ] Completes in < 10ms on local Redis

**Test plan**:
```
BB1: Both keys exist → returns both values
BB2: aiva:state missing → {"aiva_state": {}, "kinan_directives": [...]}
BB3: Both missing → {"aiva_state": {}, "kinan_directives": []}

WB1: asyncio.gather called once with 2 coroutines (not 2 sequential awaits)
WB2: Each missing key returns default, no KeyError raised
WB3: Total time < 10ms (use timer in test)

IT1: Real Redis with prefilled keys → both values returned correctly
```

---

### STORY 4.03: BinduHydrator — Postgres Scatter Task (Last Conversation)

**File**: `core/hydrators/bindu_hydrator.py` (extend Story 4.02)
**Depends on**: Story 4.02, Story 1.01 (royal_conversations schema)
**Estimated iterations**: 6-8

**One-liner**: Add `_scatter_postgres_task()` — fetches the most recent royal_conversation involving Kinan, ordered by started_at DESC LIMIT 1.

**Implementation skeleton**:
```python
async def _scatter_postgres_task(self) -> Optional[dict]:
    """
    SELECT conversation_id, started_at, summary, decisions_made,
           action_items, kinan_directives, emotional_signal, key_facts
    FROM royal_conversations
    WHERE participants->>'kinan' = 'true'
    ORDER BY started_at DESC
    LIMIT 1
    Returns None if no prior conversations exist.
    """
    ...
```

**Acceptance criteria**:
- [ ] Query targets `royal_conversations` table (not custom table)
- [ ] Filters `participants->>'kinan' = 'true'`
- [ ] Returns `None` when no rows (not exception)
- [ ] Returns dict with at least: `conversation_id`, `summary`, `action_items`, `kinan_directives`

**Test plan**:
```
BB1: 3 Kinan conversations in DB → returns most recent
BB2: 0 Kinan conversations → returns None
BB3: conversations without Kinan → excluded from result

WB1: LIMIT 1 in query (not Python slicing)
WB2: Only kinan=true conversations returned (JSONB filter)
WB3: Returns dict (not Row object)

IT1: Elestio Postgres with 2 seeded conversations → correct one returned
```

---

### STORY 4.04: BinduHydrator — Qdrant Scatter Task (Semantic Scars)

**File**: `core/hydrators/bindu_hydrator.py` (extend Story 4.03)
**Depends on**: Story 4.03, Story 1.04 (Qdrant collections)
**Estimated iterations**: 6-8

**One-liner**: Add `_scatter_qdrant_task()` — semantic search in `aiva_conversations` collection for top-3 chunks most relevant to the current call context.

**Implementation skeleton**:
```python
async def _scatter_qdrant_task(self, query_text: str, top_k: int = 3) -> list[dict]:
    """
    Embeds query_text using Gemini text-embedding-004,
    searches aiva_conversations collection,
    returns top_k results with score > 0.7.
    Each result: {"chunk_text": "...", "score": 0.92, "conversation_id": "uuid", "timestamp": "..."}
    """
    ...
```

**Acceptance criteria**:
- [ ] Uses Gemini `text-embedding-004` (not OpenAI)
- [ ] Searches `aiva_conversations` Qdrant collection
- [ ] Returns only results with `score > 0.7`
- [ ] Returns max `top_k` results
- [ ] Returns empty list if no results above threshold

**Test plan**:
```
BB1: query_text "George plumber Cairns" → returns relevant chunks
BB2: score < 0.7 results → excluded from return
BB3: empty collection → returns []

WB1: Gemini embedding called once (not multiple)
WB2: Qdrant search uses score_threshold=0.7
WB3: Each result has all 4 required keys

IT1: Real Qdrant on Elestio with seeded conversations → correct semantic match returned
```

---

### STORY 4.05: BinduHydrator — Gather + XML Envelope Assembly

**File**: `core/hydrators/bindu_hydrator.py` (extend Story 4.04)
**Depends on**: Stories 4.02, 4.03, 4.04
**Estimated iterations**: 7-9

**One-liner**: Add `gather_and_assemble()` — fires all 3 scatter tasks in parallel via asyncio.gather, then assembles the ROYAL_CHAMBER_CONTEXT XML envelope.

**Implementation skeleton**:
```python
async def gather_and_assemble(self, session_id: str, call_id: str) -> str:
    """
    Fires 3 parallel scatter tasks with 450ms timeout:
      - _scatter_redis_tasks(session_id)
      - _scatter_postgres_task()
      - _scatter_qdrant_task(query_text)
    Assembles into ROYAL_CHAMBER_CONTEXT XML envelope:
    <ROYAL_CHAMBER_CONTEXT>
      <AIVA_WORKING_STATE>{aiva_state}</AIVA_WORKING_STATE>
      <KING_DIRECTIVES>{kinan_directives}</KING_DIRECTIVES>
      <LAST_CONVERSATION>{postgres_result}</LAST_CONVERSATION>
      <RELATED_SCARS>{qdrant_results}</RELATED_SCARS>
    </ROYAL_CHAMBER_CONTEXT>
    Writes assembled XML to Redis aiva:context:{session_id}
    Returns the XML string.
    """
    ...
```

**Acceptance criteria**:
- [ ] All 3 scatter tasks run in parallel (`asyncio.gather`)
- [ ] Total execution time < 500ms end-to-end
- [ ] XML contains all 4 required sections
- [ ] Result written to Redis `aiva:context:{session_id}`
- [ ] Graceful degradation: if 1 task fails, others still assembled (partial context > no context)

**Test plan**:
```
BB1: All tasks succeed → full XML with 4 sections
BB2: Qdrant task times out → XML assembled with empty <RELATED_SCARS/>
BB3: Redis key set after gather_and_assemble() returns

WB1: asyncio.gather(..., return_exceptions=True) used
WB2: XML is valid (parseable by xml.etree.ElementTree)
WB3: Fallback value used when task returns Exception

IT1: Real Redis+Postgres+Qdrant → full envelope assembled in <500ms
```

---

### STORY 4.06: BinduHydrator — pre_call_hook Integration

**File**: `core/hydrators/pre_call_hook.py` (new file)
**Depends on**: Story 4.05
**Estimated iterations**: 5-7

**One-liner**: Create `pre_call_hook()` — the function called on every `call.initiated` webhook event that triggers BinduHydrator and returns the context envelope.

**Implementation skeleton**:
```python
# core/hydrators/pre_call_hook.py

async def pre_call_hook(session_id: str, call_id: str, caller_number: str) -> str:
    """
    Called on every call.initiated webhook.
    1. Instantiates BinduHydrator with injected clients
    2. Calls start_hydration(session_id, call_id)
    3. Calls gather_and_assemble(session_id, call_id)
    4. Returns XML context envelope
    Logs timing to observability events.
    """
    ...
```

**Acceptance criteria**:
- [ ] Callable as standalone function (not method)
- [ ] Returns XML string in < 500ms
- [ ] Logs timing to `data/observability/events.jsonl`
- [ ] `caller_number` stored in Redis `aiva:state:{session_id}`

**Test plan**:
```
BB1: pre_call_hook("s1", "c1", "+61412345678") → returns XML string
BB2: XML contains ROYAL_CHAMBER_CONTEXT root element
BB3: Timing event written to events.jsonl

WB1: caller_number written to aiva:state:{session_id}
WB2: BinduHydrator instantiated inside function (not global singleton)
WB3: Function is async

IT1: Full end-to-end call on webhook event → pre_call_hook completes in <500ms
```

---

### STORY 4.07: ConversationReplayEngine — Last N Conversations Summary

**File**: `core/memory/conversation_replay.py` (new file)
**Depends on**: Story 1.01 (schema), Story 3.10 (PostCallEnricher writes)
**Estimated iterations**: 7-9

**One-liner**: Build `ConversationReplayEngine.get_recent_summary(n=3)` — returns a structured summary of the last N Kinan<>AIVA conversations for injection into AIVA's system prompt.

**Implementation skeleton**:
```python
# core/memory/conversation_replay.py

class ConversationReplayEngine:
    """
    Queries royal_conversations, formats N most recent conversations
    into a concise summary for system prompt injection.
    """
    def __init__(self, postgres_client):
        self.pg = postgres_client

    async def get_recent_summary(self, n: int = 3) -> str:
        """
        Fetches last N royal_conversations where kinan=true.
        Returns formatted string:
        RECENT CONTEXT:
        [2026-02-24] Call (18min): George's Cairns number checked. Kinan asked to
        brief George when number provisioned. Action item: AIVA to follow up.
        [2026-02-23] Call (6min): Voice widget confirmed live. Kinan happy.
        ...
        Returns empty string if no conversations.
        """
        ...
```

**Acceptance criteria**:
- [ ] Queries `royal_conversations` with `participants->>'kinan' = 'true'` ORDER BY `started_at DESC` LIMIT n
- [ ] Returns human-readable string (not JSON)
- [ ] Each conversation on one line: `[date] Call (Xmin): [summary] Action: [action_items]`
- [ ] Returns empty string (not None) when no conversations
- [ ] `n` parameter respected (default 3)

**Test plan**:
```
BB1: 5 Kinan conversations → returns 3 most recent
BB2: 0 conversations → returns ""
BB3: n=1 → returns only most recent

WB1: Postgres query uses LIMIT n (not Python slice)
WB2: Call duration calculated from started_at + ended_at
WB3: action_items formatted inline, not as raw JSON

IT1: Elestio Postgres with 3 seeded conversations → formatted summary returned correctly
```

---

# MODULE 5: INTENT + ROUTING
**Stories**: 5.01 → 5.11 (run sequentially within module)
**Depends on**: Module 3 (call capture must be complete)
**What it builds**: IntentClassifier (reads AIVA utterances) + SwarmRouter (dispatches to 6 worker types)

---

### STORY 5.01: IntentSignal — Data Model

**File**: `core/intent/intent_signal.py` (new file)
**Depends on**: Story 1.03 (Redis schema)
**Estimated iterations**: 4-5

**One-liner**: Define the `IntentSignal` dataclass and `IntentType` enum that all intent/routing logic uses.

**Implementation skeleton**:
```python
# core/intent/intent_signal.py

from dataclasses import dataclass
from enum import Enum
from typing import Optional
from datetime import datetime

class IntentType(Enum):
    BOOK_JOB        = "book_job"
    QUALIFY_LEAD    = "qualify_lead"
    ANSWER_FAQ      = "answer_faq"
    ESCALATE_HUMAN  = "escalate_human"
    CAPTURE_MEMORY  = "capture_memory"
    TASK_DISPATCH   = "task_dispatch"
    UNKNOWN         = "unknown"

@dataclass
class IntentSignal:
    session_id: str
    utterance: str
    intent_type: IntentType
    confidence: float        # 0.0 → 1.0
    extracted_entities: dict # {"name": "George", "location": "Cairns"}
    requires_swarm: bool
    created_at: datetime
    raw_gemini_response: Optional[str] = None
```

**Acceptance criteria**:
- [ ] `IntentType` has exactly 7 values (including UNKNOWN)
- [ ] `IntentSignal` dataclass has all 8 fields
- [ ] `confidence` is float (not int)
- [ ] `extracted_entities` is dict (not list)
- [ ] File importable standalone with no side effects

**Test plan**:
```
BB1: IntentType.BOOK_JOB.value == "book_job"
BB2: IntentSignal instantiates with all required fields
BB3: requires_swarm=True on BOOK_JOB signals

WB1: Enum has exactly 7 members
WB2: dataclass __eq__ works (two identical signals are equal)
WB3: confidence field accepts 0.0 and 1.0 without error

IT1: Import in fresh Python process → no ImportError
```

---

### STORY 5.02: IntentClassifier — Gemini Prompt + Parser

**File**: `core/intent/intent_classifier.py` (new file)
**Depends on**: Story 5.01
**Estimated iterations**: 7-9

**One-liner**: Build `IntentClassifier.classify(utterance, context)` — sends utterance + context to Gemini Flash, parses response into `IntentSignal`.

**Implementation skeleton**:
```python
# core/intent/intent_classifier.py

CLASSIFICATION_PROMPT = """
You are AIVA's intent classifier. Analyze the utterance and classify the intent.

UTTERANCE: {utterance}
CONTEXT: {context}

VALID INTENT TYPES: book_job | qualify_lead | answer_faq | escalate_human | capture_memory | task_dispatch | unknown

Respond ONLY with valid JSON:
{
  "intent_type": "book_job",
  "confidence": 0.92,
  "extracted_entities": {"name": "George", "location": "Cairns", "service": "plumbing"},
  "requires_swarm": true,
  "reasoning": "caller wants to book a plumbing job"
}
"""

class IntentClassifier:
    def __init__(self, gemini_client):
        self.gemini = gemini_client

    async def classify(self, utterance: str, context: str = "") -> IntentSignal:
        """
        Sends utterance + context to Gemini Flash.
        Parses JSON response into IntentSignal.
        Falls back to IntentType.UNKNOWN on parse failure.
        """
        ...
```

**Acceptance criteria**:
- [ ] Uses Gemini Flash (not Pro/Ultra — speed required)
- [ ] Returns `IntentSignal` (not raw dict)
- [ ] Falls back to `IntentType.UNKNOWN` on any parse failure
- [ ] `confidence` 0.0-1.0 validated
- [ ] `requires_swarm=True` when intent is BOOK_JOB or TASK_DISPATCH

**Test plan**:
```
BB1: "I need a plumber for tomorrow" → IntentType.BOOK_JOB, confidence > 0.8
BB2: malformed JSON from Gemini → IntentType.UNKNOWN returned (no crash)
BB3: "What are your hours?" → IntentType.ANSWER_FAQ

WB1: CLASSIFICATION_PROMPT uses {utterance} and {context} format strings
WB2: JSON parsed with json.loads() (not eval())
WB3: confidence clamped to [0.0, 1.0] even if Gemini returns > 1.0

IT1: Real Gemini Flash API call → valid IntentSignal returned
```

---

### STORY 5.03: IntentClassifier — Redis Cache Layer

**File**: `core/intent/intent_classifier.py` (extend Story 5.02)
**Depends on**: Story 5.02
**Estimated iterations**: 5-7

**One-liner**: Add Redis caching to IntentClassifier — identical utterances within 60s return cached result without Gemini API call.

**Implementation skeleton**:
```python
INTENT_CACHE_KEY = "intent:cache:{utterance_hash}"
INTENT_CACHE_TTL = 60  # seconds

async def _get_cached(self, utterance: str) -> Optional[IntentSignal]:
    """Hash utterance → check Redis → return cached IntentSignal or None."""
    ...

async def _set_cached(self, utterance: str, signal: IntentSignal) -> None:
    """Hash utterance → serialize IntentSignal → write to Redis with 60s TTL."""
    ...
```

**Acceptance criteria**:
- [ ] Cache key uses MD5/SHA256 hash of utterance (not raw utterance as key)
- [ ] Cache TTL = 60s
- [ ] Cache hit: no Gemini API call made
- [ ] Cache miss: classifies via Gemini, then caches result
- [ ] `IntentType.UNKNOWN` results are NOT cached

**Test plan**:
```
BB1: classify() called twice with same utterance → Gemini called once (cache hit second time)
BB2: UNKNOWN result → not cached (Gemini called again next time)
BB3: TTL expires → Gemini called again

WB1: Cache key is hash, not plain utterance text
WB2: IntentSignal serialized to JSON for storage
WB3: UNKNOWN check happens before cache write

IT1: Real Redis → cache hit/miss behaviour confirmed via Redis TTL inspection
```

---

### STORY 5.04: SwarmRouter — Base Class + Route Registry

**File**: `core/routing/swarm_router.py` (new file)
**Depends on**: Story 5.01 (IntentSignal)
**Estimated iterations**: 5-6

**One-liner**: Define the `SwarmRouter` base class with a route registry mapping `IntentType` → worker class name.

**Implementation skeleton**:
```python
# core/routing/swarm_router.py

ROUTE_REGISTRY = {
    IntentType.BOOK_JOB:       "BookingWorker",
    IntentType.QUALIFY_LEAD:   "LeadQualificationWorker",
    IntentType.ANSWER_FAQ:     "FAQWorker",
    IntentType.ESCALATE_HUMAN: "EscalationWorker",
    IntentType.CAPTURE_MEMORY: "MemoryCaptureWorker",
    IntentType.TASK_DISPATCH:  "GenesisTaskWorker",
}

class SwarmRouter:
    def __init__(self, worker_registry: dict):
        self.workers = worker_registry  # {"BookingWorker": instance, ...}

    def get_worker_name(self, intent: IntentSignal) -> Optional[str]:
        """Returns worker class name for intent, None if UNKNOWN."""
        ...

    def can_route(self, intent: IntentSignal) -> bool:
        """Returns True if intent has a registered worker and confidence >= 0.6."""
        ...
```

**Acceptance criteria**:
- [ ] `ROUTE_REGISTRY` has 6 entries (all non-UNKNOWN IntentTypes)
- [ ] `get_worker_name()` returns None for UNKNOWN
- [ ] `can_route()` returns False when confidence < 0.6
- [ ] `can_route()` returns False for UNKNOWN type
- [ ] `worker_registry` injected (not hardcoded)

**Test plan**:
```
BB1: BOOK_JOB signal → get_worker_name() returns "BookingWorker"
BB2: UNKNOWN signal → get_worker_name() returns None
BB3: BOOK_JOB signal, confidence=0.4 → can_route() returns False

WB1: ROUTE_REGISTRY has exactly 6 keys
WB2: can_route() checks both intent_type and confidence
WB3: worker_registry parameter is used (not ROUTE_REGISTRY directly for dispatch)

IT1: Import SwarmRouter, register 2 workers → routing resolves correctly
```

---

### STORY 5.05: SwarmRouter — dispatch() Method

**File**: `core/routing/swarm_router.py` (extend Story 5.04)
**Depends on**: Story 5.04
**Estimated iterations**: 6-8

**One-liner**: Add `dispatch(intent)` — instantiates the correct worker and fires it as an asyncio background task.

**Implementation skeleton**:
```python
async def dispatch(self, intent: IntentSignal) -> Optional[asyncio.Task]:
    """
    1. Calls can_route() — if False, logs and returns None
    2. Gets worker instance from self.workers
    3. Fires worker.execute(intent) as asyncio.create_task()
    4. Logs dispatch event to data/observability/events.jsonl
    5. Returns the asyncio.Task (caller can await or detach)
    """
    ...
```

**Acceptance criteria**:
- [ ] Returns `None` if `can_route()` is False (no crash)
- [ ] Worker launched with `asyncio.create_task()` (fire-and-forget capable)
- [ ] Dispatch event logged to observability
- [ ] Returns `asyncio.Task` on success

**Test plan**:
```
BB1: BOOK_JOB signal, confidence=0.9 → task returned (not None)
BB2: UNKNOWN signal → None returned, no task created
BB3: Low-confidence signal → None returned

WB1: asyncio.create_task() used (not await worker.execute())
WB2: observability log entry written with intent_type and session_id
WB3: Worker receives the full IntentSignal object

IT1: Real dispatch → BookingWorker.execute() called with correct IntentSignal
```

---

### STORY 5.06: BookingWorker — Stub + GHL Lead Creation

**File**: `core/workers/booking_worker.py` (new file)
**Depends on**: Story 5.05 (dispatch protocol)
**Estimated iterations**: 7-9

**One-liner**: Build `BookingWorker.execute(intent)` — creates a GHL lead from the extracted entities and posts to the Telnyx lead webhook.

**Implementation skeleton**:
```python
# core/workers/booking_worker.py

GHL_WEBHOOK_URL = "https://api.sunaivadigital.com/webhook/telnyx-lead"

class BookingWorker:
    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Extract name, phone, location, service from intent.extracted_entities
        2. POST to GHL_WEBHOOK_URL with lead payload
        3. Write success/failure to Redis aiva:results:{session_id}
        4. Return {"status": "ok", "lead_id": "..."}
        """
        ...

    def _build_lead_payload(self, intent: IntentSignal) -> dict:
        """
        Builds GHL-compatible lead payload from intent entities.
        Returns: {"firstName": "...", "phone": "...", "source": "AIVA_CALL", ...}
        """
        ...
```

**Acceptance criteria**:
- [ ] Posts to `https://api.sunaivadigital.com/webhook/telnyx-lead`
- [ ] Payload includes `source: "AIVA_CALL"` always
- [ ] Missing entities (e.g., no name) → payload uses "Unknown" fallback
- [ ] Result written to Redis `aiva:results:{session_id}`
- [ ] Returns status dict (not None)

**Test plan**:
```
BB1: Full entities → POST sent with all fields
BB2: Missing name entity → payload uses "Unknown" for firstName
BB3: HTTP 200 from webhook → {"status": "ok"} returned

WB1: _build_lead_payload() adds source="AIVA_CALL"
WB2: aiva:results:{session_id} written after POST
WB3: HTTP error → {"status": "error", "reason": "..."} returned

IT1: Real webhook endpoint → lead appears in GHL sub-account
```

---

### STORY 5.07: LeadQualificationWorker — Qualification Script Runner

**File**: `core/workers/lead_qualification_worker.py` (new file)
**Depends on**: Story 5.05
**Estimated iterations**: 6-8

**One-liner**: Build `LeadQualificationWorker.execute(intent)` — uses Gemini to generate 3 qualifying questions based on extracted entities and writes them to Redis for AIVA's next utterance.

**Implementation skeleton**:
```python
class LeadQualificationWorker:
    QUALIFICATION_PROMPT = """
    A potential customer called about: {service} in {location}.
    Generate 3 concise qualifying questions AIVA should ask next.
    Focus on: urgency, budget range, specific requirements.
    Return JSON: {"questions": ["Q1", "Q2", "Q3"]}
    """

    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Build prompt from intent.extracted_entities
        2. Call Gemini Flash for 3 qualifying questions
        3. Write questions to Redis aiva:state:{session_id}
        4. Return {"questions": ["Q1", "Q2", "Q3"]}
        """
        ...
```

**Acceptance criteria**:
- [ ] Returns exactly 3 qualifying questions
- [ ] Questions written to Redis `aiva:state:{session_id}` under key `qualification_questions`
- [ ] Uses Gemini Flash (not Pro)
- [ ] If Gemini fails → fallback 3 generic questions returned

**Test plan**:
```
BB1: "plumbing" + "Cairns" → 3 relevant questions returned
BB2: Gemini failure → 3 generic fallback questions returned (no crash)
BB3: Questions written to Redis aiva:state:{session_id}

WB1: Exactly 3 questions in response list
WB2: Fallback questions defined as class constant
WB3: Gemini prompt uses entity values from intent

IT1: Real Gemini API → 3 contextual questions for plumbing in Cairns
```

---

### STORY 5.08: MemoryCaptureWorker — Utterance-to-KG Entity

**File**: `core/workers/memory_capture_worker.py` (new file)
**Depends on**: Story 5.05, Story 1.04 (Qdrant collections)
**Estimated iterations**: 6-8

**One-liner**: Build `MemoryCaptureWorker.execute(intent)` — captures an important utterance as a KG entity written to Qdrant immediately (< 2s), not waiting for PostCallEnricher.

**Implementation skeleton**:
```python
class MemoryCaptureWorker:
    """
    Immediate utterance capture: does NOT wait for call.hangup.
    Fires when AIVA detects a CAPTURE_MEMORY intent mid-call.
    Use for: Kinan statements of fact, new directives, critical decisions.
    """
    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Create KG entity from intent.utterance + intent.extracted_entities
        2. Embed with Gemini text-embedding-004
        3. Upsert to aiva_conversations Qdrant collection
        4. Return {"entity_id": "uuid", "status": "captured"}
        """
        ...
```

**Acceptance criteria**:
- [ ] Writes to `aiva_conversations` Qdrant collection
- [ ] Capture completes in < 2s (not PostCallEnricher's 3s window)
- [ ] Returns `entity_id` (Qdrant point UUID)
- [ ] Entity payload includes: `utterance`, `session_id`, `captured_at`, `intent_type`

**Test plan**:
```
BB1: Utterance captured → Qdrant point exists with correct payload
BB2: Capture time < 2s
BB3: entity_id in return dict matches Qdrant point ID

WB1: Qdrant upsert (not insert — idempotent)
WB2: Entity payload has all 4 required fields
WB3: Gemini embedding called once

IT1: Real Qdrant on Elestio → point searchable after execute()
```

---

### STORY 5.09: GenesisTaskWorker — Genesis Task Bridge

**File**: `core/workers/genesis_task_worker.py` (new file)
**Depends on**: Story 5.05
**Estimated iterations**: 6-7

**One-liner**: Build `GenesisTaskWorker.execute(intent)` — converts AIVA's TASK_DISPATCH intent into a Genesis RWL task written to the task board.

**Implementation skeleton**:
```python
class GenesisTaskWorker:
    TASK_BOARD_PATH = "E:\\genesis-system\\loop\\tasks.json"

    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Extract task description from intent.utterance + entities
        2. Create task entry: {"id": uuid, "description": "...", "source": "AIVA",
                               "created_at": "...", "status": "pending"}
        3. Append to loop/tasks.json
        4. Return {"task_id": uuid, "status": "queued"}
        """
        ...
```

**Acceptance criteria**:
- [ ] Appends to `loop/tasks.json` (does not overwrite)
- [ ] Task has `source: "AIVA"` marker
- [ ] Returns `task_id` (UUID)
- [ ] Task description is human-readable (not raw utterance)

**Test plan**:
```
BB1: TASK_DISPATCH intent → task appears in tasks.json
BB2: Task has source="AIVA" field
BB3: Return dict has task_id as UUID string

WB1: Append to existing JSON array (not overwrite)
WB2: Task description cleaned (not raw utterance text with filler words)
WB3: UUID generated with uuid4()

IT1: Real tasks.json → task appended and readable by Genesis loop
```

---

### STORY 5.10: FAQWorker — Static FAQ Lookup

**File**: `core/workers/faq_worker.py` (new file)
**Depends on**: Story 5.05
**Estimated iterations**: 5-6

**One-liner**: Build `FAQWorker.execute(intent)` — looks up the answer to a FAQ from a JSON knowledge base and writes it to Redis for AIVA to speak.

**Implementation skeleton**:
```python
FAQ_KB_PATH = "E:\\genesis-system\\data\\aiva_faq.json"

class FAQWorker:
    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Load FAQ KB from data/aiva_faq.json
        2. Fuzzy-match utterance against FAQ questions (using difflib)
        3. If match score > 0.7 → write answer to Redis aiva:state:{session_id}
        4. If no match → write "I don't have that information" fallback
        5. Return {"answer": "...", "matched": True/False}
        """
        ...
```

**Acceptance criteria**:
- [ ] Reads from `data/aiva_faq.json`
- [ ] Uses `difflib.SequenceMatcher` (not external API)
- [ ] Answer written to Redis `aiva:state:{session_id}` under `faq_answer`
- [ ] Returns `{"answer": str, "matched": bool}`

**Test plan**:
```
BB1: Known FAQ question → matched=True, answer returned
BB2: Unknown question → matched=False, fallback answer returned
BB3: Answer written to Redis

WB1: difflib.SequenceMatcher used with ratio > 0.7 threshold
WB2: faq_answer key in Redis
WB3: JSON file loaded once per execute() call

IT1: Real aiva_faq.json with 5 entries → correct match on known question
```

---

### STORY 5.11: EscalationWorker — Human Handoff Signal

**File**: `core/workers/escalation_worker.py` (new file)
**Depends on**: Story 5.05
**Estimated iterations**: 4-5

**One-liner**: Build `EscalationWorker.execute(intent)` — writes an escalation signal to Redis so Telnyx knows to transfer the call to a human operator.

**Implementation skeleton**:
```python
ESCALATION_SIGNAL_KEY = "aiva:escalation:{session_id}"

class EscalationWorker:
    async def execute(self, intent: IntentSignal) -> dict:
        """
        1. Write escalation signal: Redis SET aiva:escalation:{session_id} = "requested"
        2. Set TTL = 300s
        3. Log escalation event to observability
        4. Return {"status": "escalation_requested", "session_id": session_id}
        """
        ...
```

**Acceptance criteria**:
- [ ] Sets Redis `aiva:escalation:{session_id}` to `"requested"`
- [ ] TTL = 300s
- [ ] Returns status dict
- [ ] Observability event logged

**Test plan**:
```
BB1: execute() → Redis key set to "requested"
BB2: Redis key has TTL ≤ 300s
BB3: Return dict has status="escalation_requested"

WB1: SETEX used (not SET without TTL)
WB2: Observability event has session_id and intent_type
WB3: Value is "requested" string

IT1: Real Redis → key readable with correct TTL after execute()
```

---

# MODULE 6: MERGE + COHERENCE
**Stories**: 6.01 → 6.03 (run sequentially)
**Depends on**: Module 5 (workers complete)
**What it builds**: SemanticMergeInterceptor — the Opus Reducer that resolves conflicts between parallel swarm workers

---

### STORY 6.01: SwarmResult — Data Model + Conflict Detector

**File**: `core/merge/swarm_result.py` (new file)
**Depends on**: Story 5.01 (IntentSignal)
**Estimated iterations**: 4-5

**One-liner**: Define `SwarmResult` dataclass and `ConflictDetector` that identifies when 2+ workers produce contradictory outputs for the same session.

**Implementation skeleton**:
```python
# core/merge/swarm_result.py

@dataclass
class SwarmResult:
    session_id: str
    worker_name: str
    output: dict
    completed_at: datetime
    confidence: float

class ConflictDetector:
    def detect(self, results: list[SwarmResult]) -> bool:
        """
        Returns True if 2+ results for same session have conflicting outputs.
        Conflict = same key with different non-null values.
        """
        ...

    def get_conflicts(self, results: list[SwarmResult]) -> list[tuple]:
        """
        Returns list of (result_a, result_b, conflicting_key) tuples.
        """
        ...
```

**Acceptance criteria**:
- [ ] `SwarmResult` has 5 required fields
- [ ] `ConflictDetector.detect()` returns True for genuine conflicts
- [ ] `ConflictDetector.detect()` returns False when outputs are complementary (different keys)
- [ ] `get_conflicts()` returns empty list when no conflicts

**Test plan**:
```
BB1: Two results with same key different value → detect() returns True
BB2: Two results with different keys → detect() returns False
BB3: get_conflicts() returns correct (a, b, key) tuple

WB1: SwarmResult dataclass has __eq__ via @dataclass
WB2: ConflictDetector works with empty list (returns False)
WB3: Conflict check uses key equality, not pointer equality

IT1: Import in fresh process → no errors
```

---

### STORY 6.02: SemanticMergeInterceptor — Opus Reducer

**File**: `core/merge/semantic_merge_interceptor.py` (new file)
**Depends on**: Story 6.01
**Estimated iterations**: 8-10

**One-liner**: Build `SemanticMergeInterceptor.reduce(results)` — sends conflicting SwarmResults to Opus for semantic resolution, returns the winning interpretation.

**Implementation skeleton**:
```python
# core/merge/semantic_merge_interceptor.py

MERGE_PROMPT = """
You are the Genesis Semantic Reducer. Multiple swarm workers have produced conflicting outputs.

SESSION: {session_id}
WORKER A ({worker_a}): {output_a}
WORKER B ({worker_b}): {output_b}
CONFLICT ON KEY: {conflict_key}

Analyze both outputs and return the most semantically correct resolution.
Respond ONLY with valid JSON: {"resolved_value": "...", "reasoning": "...", "winner": "A|B|MERGE"}
"""

class SemanticMergeInterceptor:
    def __init__(self, opus_client):
        self.opus = opus_client  # Opus 4.6 — only used for conflict resolution

    async def reduce(self, results: list[SwarmResult]) -> dict:
        """
        1. Run ConflictDetector
        2. If no conflicts → merge outputs (union of all keys)
        3. If conflicts → send each conflict to Opus for resolution
        4. Return final merged dict
        """
        ...
```

**Acceptance criteria**:
- [ ] Uses Opus (not Flash/Sonnet) for resolution
- [ ] No-conflict path: simple dict merge (no API call)
- [ ] Returns single merged dict with all keys resolved
- [ ] Reasoning stored in `_merge_reasoning` key of returned dict

**Test plan**:
```
BB1: No conflicts → merged dict has all keys from both results (no Opus call)
BB2: One conflict → Opus called once, conflict resolved
BB3: Returned dict has _merge_reasoning key

WB1: Opus client not called on non-conflict path
WB2: MERGE_PROMPT formatted with all 5 variables
WB3: Winner="MERGE" case handled (blended value)

IT1: Real Opus API with seeded conflict → resolved value returned
```

---

### STORY 6.03: SemanticMergeInterceptor — Redis Result Write

**File**: `core/merge/semantic_merge_interceptor.py` (extend Story 6.02)
**Depends on**: Story 6.02
**Estimated iterations**: 4-5

**One-liner**: After reduce(), write the final merged result to Redis `aiva:results:{session_id}` so AIVA can consume it immediately.

**Implementation skeleton**:
```python
async def reduce_and_commit(self, results: list[SwarmResult], session_id: str) -> dict:
    """
    1. Calls reduce(results)
    2. Writes merged dict to Redis: aiva:results:{session_id}
    3. Sets TTL = 600s
    4. Returns merged dict
    """
    ...
```

**Acceptance criteria**:
- [ ] Calls `reduce()` internally
- [ ] Writes result to `aiva:results:{session_id}`
- [ ] TTL = 600s
- [ ] Returns the merged dict (same as reduce())

**Test plan**:
```
BB1: reduce_and_commit() → Redis key aiva:results:{session_id} set
BB2: Redis value is JSON-serialized merged dict
BB3: TTL ≤ 600s

WB1: reduce() called before Redis write
WB2: Redis SETEX used with 600 TTL
WB3: Return value matches what was written to Redis

IT1: Real Redis → key readable with correct value after reduce_and_commit()
```

---

# MODULE 7: API + INJECTION
**Stories**: 7.01 → 7.07 (run sequentially)
**Depends on**: Modules 1-6 complete
**What it builds**: AIVAMemoryAPI (FastAPI endpoints) + SystemPromptInjector

---

### STORY 7.01: FastAPI App Bootstrap

**File**: `api/aiva_memory_api.py` (new file)
**Depends on**: Story 1.01 (schema exists)
**Estimated iterations**: 4-5

**One-liner**: Bootstrap the AIVAMemoryAPI FastAPI application with health check endpoint and Elestio connection validation.

**Implementation skeleton**:
```python
# api/aiva_memory_api.py

from fastapi import FastAPI
from datetime import datetime

app = FastAPI(title="AIVA Memory API", version="1.0.0")

@app.get("/health")
async def health_check():
    """Returns service status + Elestio connection status."""
    return {
        "status": "ok",
        "timestamp": datetime.utcnow().isoformat(),
        "services": {
            "postgres": "connected",
            "redis": "connected",
            "qdrant": "connected"
        }
    }
```

**Acceptance criteria**:
- [ ] `GET /health` returns 200 with status="ok"
- [ ] Response includes `services` dict with 3 keys
- [ ] App title = "AIVA Memory API", version = "1.0.0"
- [ ] App runs on port 8765 (Uvicorn default config)

**Test plan**:
```
BB1: GET /health → 200, body has status="ok"
BB2: GET /health → services dict has postgres, redis, qdrant
BB3: GET /unknown_path → 404

WB1: FastAPI app instantiated at module level
WB2: timestamp is valid ISO 8601 string
WB3: Port 8765 in uvicorn config

IT1: uvicorn app on port 8765 → curl /health returns 200
```

---

### STORY 7.02: AIVAMemoryAPI — GET /conversations/recent

**File**: `api/aiva_memory_api.py` (extend Story 7.01)
**Depends on**: Story 7.01, Story 4.07 (ConversationReplayEngine)
**Estimated iterations**: 5-7

**One-liner**: Add `GET /conversations/recent?n=3` — returns N most recent Kinan<>AIVA conversations in structured JSON.

**Implementation skeleton**:
```python
@app.get("/conversations/recent")
async def get_recent_conversations(n: int = 3):
    """
    Returns last n royal_conversations with kinan=true.
    Response: {"conversations": [{"id": "...", "started_at": "...",
               "summary": "...", "action_items": [...], "emotional_signal": "..."}]}
    """
    ...
```

**Acceptance criteria**:
- [ ] Returns JSON with `conversations` array
- [ ] `n` param respected (default 3, max 20)
- [ ] Each conversation has: id, started_at, summary, action_items, emotional_signal
- [ ] Empty array (not 404) when no conversations exist

**Test plan**:
```
BB1: GET /conversations/recent → 200, conversations array
BB2: GET /conversations/recent?n=1 → exactly 1 conversation
BB3: GET /conversations/recent with empty DB → {"conversations": []}

WB1: n capped at 20 (no unbounded queries)
WB2: Pydantic response model validates all fields
WB3: ConversationReplayEngine used (not raw SQL in endpoint)

IT1: Elestio Postgres with 3 seeded conversations → correct response
```

---

### STORY 7.03: AIVAMemoryAPI — GET /context/{session_id}

**File**: `api/aiva_memory_api.py` (extend Story 7.02)
**Depends on**: Story 7.02, Story 4.05 (gather_and_assemble)
**Estimated iterations**: 5-6

**One-liner**: Add `GET /context/{session_id}` — returns the pre-assembled ROYAL_CHAMBER_CONTEXT XML envelope from Redis for an active session.

**Implementation skeleton**:
```python
@app.get("/context/{session_id}")
async def get_session_context(session_id: str):
    """
    Reads aiva:context:{session_id} from Redis.
    Returns: {"session_id": "...", "context_xml": "...", "hydrated": true/false}
    Returns hydrated=false if key not found (hydration not complete or expired).
    """
    ...
```

**Acceptance criteria**:
- [ ] Returns 200 with `context_xml` if key exists
- [ ] Returns 200 with `hydrated=false` if key missing (not 404)
- [ ] `context_xml` is valid XML string
- [ ] Response time < 50ms (Redis read only)

**Test plan**:
```
BB1: Active session with preloaded context → context_xml returned
BB2: Missing session_id → 200 with hydrated=false
BB3: Response time < 50ms

WB1: Redis read (not Postgres/Qdrant) for this endpoint
WB2: context_xml validated as XML before returning
WB3: hydrated=false when Redis key missing

IT1: Real Redis with seeded context → correct XML returned
```

---

### STORY 7.04: AIVAMemoryAPI — POST /conversations

**File**: `api/aiva_memory_api.py` (extend Story 7.03)
**Depends on**: Story 7.03, Story 1.01 (schema)
**Estimated iterations**: 6-7

**One-liner**: Add `POST /conversations` — creates a new royal_conversations row when a call starts.

**Implementation skeleton**:
```python
@app.post("/conversations", status_code=201)
async def create_conversation(payload: ConversationCreateRequest):
    """
    Creates row in royal_conversations with started_at=now, status=active.
    Payload: {"session_id": "...", "call_id": "...", "participants": {...}}
    Response: {"conversation_id": "uuid", "status": "active"}
    """
    ...
```

**Acceptance criteria**:
- [ ] Returns 201 on success
- [ ] Creates row in `royal_conversations`
- [ ] Returns `conversation_id` (UUID)
- [ ] `started_at` set to current UTC time
- [ ] Duplicate `call_id` returns 409 (not 500)

**Test plan**:
```
BB1: Valid payload → 201, conversation_id returned
BB2: Duplicate call_id → 409 Conflict
BB3: Missing required field → 422 Unprocessable Entity

WB1: started_at is UTC timestamp
WB2: Postgres INSERT uses gen_random_uuid() for conversation_id
WB3: 409 from DB constraint, not application-level check

IT1: Elestio Postgres → row created, readable in DB
```

---

### STORY 7.05: AIVAMemoryAPI — PATCH /conversations/{id}/enrich

**File**: `api/aiva_memory_api.py` (extend Story 7.04)
**Depends on**: Story 7.04
**Estimated iterations**: 5-7

**One-liner**: Add `PATCH /conversations/{id}/enrich` — triggered by PostCallEnricher to write enrichment data to an existing conversation row.

**Implementation skeleton**:
```python
@app.patch("/conversations/{conversation_id}/enrich")
async def enrich_conversation(conversation_id: str, payload: EnrichmentPayload):
    """
    Updates royal_conversations row with:
    - transcript_raw, enriched_entities, decisions_made, action_items,
      emotional_signal, key_facts, kinan_directives, memory_vector_id, ended_at
    Returns: {"status": "enriched", "conversation_id": "..."}
    """
    ...
```

**Acceptance criteria**:
- [ ] Updates existing row (not creates new)
- [ ] Returns 404 if `conversation_id` not found
- [ ] Sets `ended_at` on enrichment
- [ ] Returns 200 with `status="enriched"` on success

**Test plan**:
```
BB1: Valid enrich → 200, status="enriched"
BB2: Unknown conversation_id → 404
BB3: ended_at set in DB after enrich

WB1: UPDATE query (not INSERT)
WB2: All 8 enrichment fields written in single UPDATE
WB3: ended_at = current UTC time

IT1: Elestio Postgres → row updated with all enrichment fields
```

---

### STORY 7.06: SystemPromptInjector — Builder Function

**File**: `core/injection/system_prompt_injector.py` (new file)
**Depends on**: Story 4.07 (ConversationReplayEngine), Story 2.02 (AIVARegistry)
**Estimated iterations**: 6-8

**One-liner**: Build `SystemPromptInjector.build_injection(session_id)` — assembles the full system prompt injection block from recent memory + AIVA persona.

**Implementation skeleton**:
```python
# core/injection/system_prompt_injector.py

INJECTION_TEMPLATE = """
=== AIVA MEMORY INJECTION ===
{recent_conversations}

=== KING'S ACTIVE DIRECTIVES ===
{kinan_directives}

=== CURRENT SESSION STATE ===
Caller: {caller_info}
Time: {local_time} AEST
Active Tasks: {open_tasks}
=== END MEMORY INJECTION ===
"""

class SystemPromptInjector:
    async def build_injection(self, session_id: str) -> str:
        """
        1. Get recent_conversations from ConversationReplayEngine (last 3)
        2. Get kinan_directives from Redis kinan:directives:active
        3. Get caller_info from Redis aiva:state:{session_id}
        4. Format INJECTION_TEMPLATE
        5. Return formatted string
        """
        ...
```

**Acceptance criteria**:
- [ ] Returns non-empty string always (falls back to empty sections if data missing)
- [ ] Contains all 4 INJECTION_TEMPLATE sections
- [ ] `local_time` is AEST formatted (not UTC)
- [ ] `recent_conversations` from ConversationReplayEngine (last 3)

**Test plan**:
```
BB1: Full data available → all 4 sections populated
BB2: No prior conversations → "No prior conversations" in output
BB3: AEST time formatted correctly

WB1: ConversationReplayEngine.get_recent_summary(3) used
WB2: AEST = UTC+10 or UTC+11 (AEDT aware)
WB3: Fallback text for missing sections

IT1: Full end-to-end → injection string injected into Telnyx system prompt update
```

---

### STORY 7.07: SystemPromptInjector — Telnyx API Push

**File**: `core/injection/system_prompt_injector.py` (extend Story 7.06)
**Depends on**: Story 7.06
**Estimated iterations**: 6-8

**One-liner**: Add `push_to_telnyx(session_id, assistant_id)` — calls Telnyx API to update AIVA's system prompt with the built injection block before the call connects.

**Implementation skeleton**:
```python
TELNYX_API_BASE = "https://api.telnyx.com/v2"

async def push_to_telnyx(self, session_id: str, assistant_id: str) -> bool:
    """
    1. Calls build_injection(session_id)
    2. PATCH /ai_assistants/{assistant_id} with updated system_prompt
    3. Returns True on success, False on failure
    Auth: Bearer TELNYX_API_KEY from credentials
    """
    ...
```

**Acceptance criteria**:
- [ ] Uses `TELNYX_API_KEY` from credentials (not hardcoded)
- [ ] PATCH to `/v2/ai_assistants/{assistant_id}`
- [ ] Returns True on HTTP 200
- [ ] Returns False on HTTP error (logs error, no crash)

**Test plan**:
```
BB1: Valid credentials + assistant_id → True returned
BB2: Invalid API key → False returned (not exception)
BB3: PATCH request body contains updated system_prompt

WB1: API key from env/credentials file (not hardcoded)
WB2: HTTP errors caught and logged
WB3: Request body is correct Telnyx AI assistant update format

IT1: Real Telnyx API → assistant system_prompt updated, verifiable in portal
```

---

# MODULE 8: BRIDGE + DASHBOARD
**Stories**: 8.01 → 8.06 (run sequentially)
**Depends on**: Module 7 complete
**What it builds**: OpenClaw<>Genesis bridge + Royal Dashboard (Kinan's live view)

---

### STORY 8.01: OpenClaw Bridge — Inbound Message Schema

**File**: `core/bridge/openclaw_bridge.py` (new file)
**Depends on**: Story 5.01 (IntentSignal)
**Estimated iterations**: 4-5

**One-liner**: Define `OpenClawMessage` dataclass — the standard message format for bidirectional AIVA<>Genesis communication via the OpenClaw bridge.

**Implementation skeleton**:
```python
# core/bridge/openclaw_bridge.py

from dataclasses import dataclass
from enum import Enum
from datetime import datetime
from typing import Optional

class MessageDirection(Enum):
    AIVA_TO_GENESIS = "aiva_to_genesis"
    GENESIS_TO_AIVA = "genesis_to_aiva"

@dataclass
class OpenClawMessage:
    message_id: str        # UUID
    session_id: str
    direction: MessageDirection
    payload: dict          # Intent signals, task results, injections
    priority: int          # 1=critical, 2=high, 3=normal
    created_at: datetime
    expires_at: Optional[datetime] = None  # None = no expiry
```

**Acceptance criteria**:
- [ ] `MessageDirection` has exactly 2 values
- [ ] `OpenClawMessage` has 7 fields (6 required, 1 optional)
- [ ] `priority` is int 1-3
- [ ] `expires_at` is Optional (not required)

**Test plan**:
```
BB1: OpenClawMessage instantiates with all required fields
BB2: MessageDirection.AIVA_TO_GENESIS.value == "aiva_to_genesis"
BB3: expires_at=None is valid

WB1: dataclass __eq__ works
WB2: priority=4 does not raise (validation in routing layer, not schema)
WB3: Import standalone with no side effects

IT1: Import in fresh process → no errors
```

---

### STORY 8.02: OpenClaw Bridge — Redis Queue Writer

**File**: `core/bridge/openclaw_bridge.py` (extend Story 8.01)
**Depends on**: Story 8.01, Story 1.03 (Redis schema)
**Estimated iterations**: 5-6

**One-liner**: Add `BridgeWriter.send(message)` — serialises an `OpenClawMessage` and pushes it to Redis list `bridge:queue:{direction}`.

**Implementation skeleton**:
```python
BRIDGE_QUEUE_AIVA_TO_GENESIS = "bridge:queue:aiva_to_genesis"
BRIDGE_QUEUE_GENESIS_TO_AIVA = "bridge:queue:genesis_to_aiva"

class BridgeWriter:
    def __init__(self, redis_client):
        self.redis = redis_client

    async def send(self, message: OpenClawMessage) -> bool:
        """
        Serialises message to JSON.
        RPUSH to bridge:queue:{direction} (appropriate queue).
        Returns True on success.
        """
        ...
```

**Acceptance criteria**:
- [ ] Pushes to correct queue based on `message.direction`
- [ ] Message serialised to JSON (not pickle)
- [ ] Returns True on success, False on Redis error
- [ ] Expired messages (expires_at in past) are NOT queued

**Test plan**:
```
BB1: AIVA_TO_GENESIS message → pushed to bridge:queue:aiva_to_genesis
BB2: Expired message (expires_at=5min ago) → not queued, returns False
BB3: Redis error → False returned (not exception)

WB1: RPUSH used (not LPUSH — FIFO order)
WB2: Expired check uses datetime.utcnow()
WB3: JSON serialisation (not pickle)

IT1: Real Redis → message readable from correct queue
```

---

### STORY 8.03: OpenClaw Bridge — Redis Queue Reader

**File**: `core/bridge/openclaw_bridge.py` (extend Story 8.02)
**Depends on**: Story 8.02
**Estimated iterations**: 5-6

**One-liner**: Add `BridgeReader.poll(direction, timeout_s=1)` — blocking Redis list pop that reads the next message from the bridge queue.

**Implementation skeleton**:
```python
class BridgeReader:
    def __init__(self, redis_client):
        self.redis = redis_client

    async def poll(self, direction: MessageDirection, timeout_s: int = 1) -> Optional[OpenClawMessage]:
        """
        BLPOP bridge:queue:{direction} with timeout_s.
        Returns OpenClawMessage on success.
        Returns None on timeout (no messages).
        Raises ValueError on JSON parse failure.
        """
        ...
```

**Acceptance criteria**:
- [ ] Uses `BLPOP` (blocking pop — not polling loop)
- [ ] Returns `None` on timeout (not exception)
- [ ] Returns `OpenClawMessage` (not raw dict)
- [ ] `ValueError` raised on malformed message

**Test plan**:
```
BB1: Message in queue → poll() returns OpenClawMessage
BB2: Empty queue, timeout_s=0.1 → returns None within 200ms
BB3: Malformed JSON in queue → ValueError raised

WB1: BLPOP used (not LPOP in a loop)
WB2: timeout_s respected (not hardcoded)
WB3: dict deserialized to OpenClawMessage (not returned as dict)

IT1: Writer.send() then Reader.poll() → same message round-trips correctly
```

---

### STORY 8.04: Royal Dashboard — FastAPI Routes

**File**: `api/royal_dashboard.py` (new file)
**Depends on**: Story 7.01 (app bootstrap pattern)
**Estimated iterations**: 5-7

**One-liner**: Create Royal Dashboard FastAPI app with 3 routes: live call status, recent conversations list, active swarm workers list.

**Implementation skeleton**:
```python
# api/royal_dashboard.py

dashboard = FastAPI(title="Royal Dashboard", version="1.0.0")

@dashboard.get("/status")
async def get_live_status():
    """Returns: {"active_calls": N, "swarm_workers": N, "queue_depth": N}"""
    ...

@dashboard.get("/calls/recent")
async def get_recent_calls():
    """Returns last 10 calls with status, duration, outcome."""
    ...

@dashboard.get("/workers/active")
async def get_active_workers():
    """Returns list of active swarm worker tasks from Redis."""
    ...
```

**Acceptance criteria**:
- [ ] All 3 routes return 200 with valid JSON
- [ ] `/status` includes `active_calls`, `swarm_workers`, `queue_depth`
- [ ] `/calls/recent` returns max 10 entries
- [ ] `/workers/active` reads from Redis (not Postgres)

**Test plan**:
```
BB1: GET /status → 200, all 3 keys present
BB2: GET /calls/recent → max 10 entries
BB3: GET /workers/active → list from Redis

WB1: queue_depth from Redis LLEN on bridge queues
WB2: active_calls from Redis keys matching aiva:state:*
WB3: /calls/recent uses royal_conversations Postgres query

IT1: All 3 endpoints return valid JSON on real infrastructure
```

---

### STORY 8.05: Royal Dashboard — HTML Frontend (Single File)

**File**: `api/templates/royal_dashboard.html` (new file)
**Depends on**: Story 8.04
**Estimated iterations**: 6-8

**One-liner**: Build a single-file HTML dashboard that auto-refreshes every 5s showing live call status, recent conversations, and active workers.

**Implementation skeleton**:
```html
<!DOCTYPE html>
<html>
<head>
  <title>Royal Chamber — AIVA Command Centre</title>
  <meta http-equiv="refresh" content="5">
  <style>/* Dark theme, gold accent (#D4AF37), minimal */</style>
</head>
<body>
  <!-- STATUS PANEL: active calls, workers, queue depth -->
  <!-- CALLS PANEL: last 10 calls table -->
  <!-- WORKERS PANEL: active swarm tasks -->
  <script>
    // Fetch /status, /calls/recent, /workers/active on load
    // Update DOM without full page reload
  </script>
</body>
</html>
```

**Acceptance criteria**:
- [ ] Single HTML file (no external CSS/JS frameworks)
- [ ] Auto-refreshes data every 5s via JS fetch (not meta refresh)
- [ ] Dark theme with gold accent
- [ ] Displays: active calls count, last 10 calls, active workers
- [ ] Shows AIVA status: "On Call" / "Available" / "Processing"

**Test plan**:
```
BB1: Page loads without JS errors in browser console
BB2: Data refreshes every 5s (verify with network tab)
BB3: AIVA status shows correctly based on Redis state

WB1: fetch() calls /status, /calls/recent, /workers/active
WB2: No external CDN dependencies
WB3: 5s setInterval() (not setTimeout chain)

IT1: Open in Chrome → all panels populate with real data
```

---

### STORY 8.06: Royal Dashboard — FastAPI Static Mount

**File**: `api/royal_dashboard.py` (extend Story 8.05)
**Depends on**: Story 8.05
**Estimated iterations**: 3-4

**One-liner**: Mount the HTML template as a static file on `GET /` and serve it via the dashboard FastAPI app on port 8766.

**Implementation skeleton**:
```python
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse

@dashboard.get("/")
async def serve_dashboard():
    return FileResponse("api/templates/royal_dashboard.html")

# Run on port 8766 (separate from Memory API on 8765)
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(dashboard, host="0.0.0.0", port=8766)
```

**Acceptance criteria**:
- [ ] `GET /` returns HTML file (200, not redirect)
- [ ] Runs on port 8766 (not 8765 — that's Memory API)
- [ ] `Content-Type: text/html` response header

**Test plan**:
```
BB1: GET / → 200, Content-Type: text/html
BB2: Port 8766 (not 8765)
BB3: HTML contains "Royal Chamber" title

WB1: FileResponse used (not HTMLResponse with hardcoded string)
WB2: Port in uvicorn.run() is 8766
WB3: Dashboard app separate from Memory API app

IT1: curl localhost:8766 → HTML returned
```

---

# MODULE 9: NIGHTLY EPOCH
**Stories**: 9.01 → 9.05 (run sequentially)
**Depends on**: All modules complete
**What it builds**: Weekly Sunday 2AM AEST job that distills all AIVA conversations into compressed KG axioms and updates the Genesis knowledge base.

---

### STORY 9.01: NightlyEpochScheduler — Cron Setup + Lock

**File**: `core/epoch/nightly_epoch_scheduler.py` (new file)
**Depends on**: Story 1.03 (Redis schema)
**Estimated iterations**: 5-6

**One-liner**: Build `NightlyEpochScheduler` — APScheduler cron job that fires every Sunday 2AM AEST, with a Redis distributed lock preventing double-runs.

**Implementation skeleton**:
```python
# core/epoch/nightly_epoch_scheduler.py

from apscheduler.schedulers.asyncio import AsyncIOScheduler
import pytz

AEST = pytz.timezone("Australia/Sydney")
EPOCH_LOCK_KEY = "epoch:lock:nightly"
EPOCH_LOCK_TTL = 7200  # 2 hours max runtime

class NightlyEpochScheduler:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.scheduler = AsyncIOScheduler(timezone=AEST)

    def start(self):
        """Register cron: every Sunday at 2:00 AM AEST."""
        self.scheduler.add_job(
            self._run_with_lock,
            'cron',
            day_of_week='sun',
            hour=2,
            minute=0
        )
        self.scheduler.start()

    async def _acquire_lock(self) -> bool:
        """SET NX EX — returns True if lock acquired."""
        ...

    async def _release_lock(self) -> None:
        """DELETE epoch:lock:nightly."""
        ...
```

**Acceptance criteria**:
- [ ] Cron fires Sunday 2AM AEST (not UTC)
- [ ] Redis NX lock prevents concurrent runs
- [ ] Lock TTL = 7200s (2 hours)
- [ ] `_acquire_lock()` returns False if lock already held

**Test plan**:
```
BB1: _acquire_lock() on empty Redis → True returned
BB2: _acquire_lock() called twice → second call returns False
BB3: Lock key has TTL ≤ 7200s

WB1: SET NX EX used (atomic lock)
WB2: AEST timezone used for cron (not UTC)
WB3: scheduler.add_job() uses 'cron' trigger

IT1: start() + _acquire_lock() + _release_lock() → Redis key lifecycle works
```

---

### STORY 9.02: NightlyEpochRunner — Conversation Aggregator

**File**: `core/epoch/nightly_epoch_runner.py` (new file)
**Depends on**: Story 9.01, Story 1.01 (schema)
**Estimated iterations**: 6-7

**One-liner**: Build `NightlyEpochRunner.aggregate_week()` — queries all royal_conversations from the past 7 days and returns them as a list for distillation.

**Implementation skeleton**:
```python
# core/epoch/nightly_epoch_runner.py

class NightlyEpochRunner:
    async def aggregate_week(self) -> list[dict]:
        """
        SELECT conversation_id, started_at, transcript_raw, enriched_entities,
               decisions_made, action_items, key_facts, kinan_directives
        FROM royal_conversations
        WHERE started_at >= NOW() - INTERVAL '7 days'
        AND participants->>'kinan' = 'true'
        ORDER BY started_at ASC
        Returns list of dicts (empty list if no conversations)
        """
        ...
```

**Acceptance criteria**:
- [ ] Queries last 7 days (not 30 days, not all time)
- [ ] Filters Kinan conversations only
- [ ] Returns empty list (not None) when no data
- [ ] Ordered by `started_at ASC`

**Test plan**:
```
BB1: 5 conversations in last 7 days → all 5 returned
BB2: Conversation from 8 days ago → excluded
BB3: Non-Kinan conversations → excluded

WB1: Postgres INTERVAL '7 days' used (not Python datetime calc)
WB2: Empty list returned on no rows
WB3: Ordered ASC (oldest first)

IT1: Elestio Postgres with 3 seeded conversations in range → all 3 returned
```

---

### STORY 9.03: NightlyEpochRunner — Gemini Distillation

**File**: `core/epoch/nightly_epoch_runner.py` (extend Story 9.02)
**Depends on**: Story 9.02
**Estimated iterations**: 8-10

**One-liner**: Add `distill(conversations)` — sends the week's conversations to Gemini Pro for distillation into 5-10 compressed KG axioms.

**Implementation skeleton**:
```python
DISTILLATION_PROMPT = """
You are the Genesis Memory Distiller. Review this week's AIVA<>Kinan conversations.
Extract 5-10 high-value axioms: standing patterns, recurring preferences, key facts
about the business, and strategic decisions made.

CONVERSATIONS:
{conversations_json}

Return ONLY valid JSON:
{
  "axioms": [
    {"id": "epoch_{date}_001", "content": "...", "category": "preference|fact|strategy|directive", "confidence": 0.9}
  ],
  "week_summary": "2-sentence summary of this week"
}
"""

async def distill(self, conversations: list[dict]) -> dict:
    """Sends to Gemini Pro, returns axioms + week_summary dict."""
    ...
```

**Acceptance criteria**:
- [ ] Uses Gemini Pro (not Flash — quality required for axiom extraction)
- [ ] Returns 5-10 axioms (min 3 if week has few conversations)
- [ ] Each axiom has: id, content, category, confidence
- [ ] Returns `week_summary` string
- [ ] Empty conversations list → returns `{"axioms": [], "week_summary": "No conversations this week"}`

**Test plan**:
```
BB1: 5 conversations → 5-10 axioms returned
BB2: Empty conversations list → {"axioms": [], "week_summary": "No conversations this week"}
BB3: Each axiom has all 4 required keys

WB1: Gemini Pro used (not Flash)
WB2: conversations_json includes decisions_made + kinan_directives fields
WB3: JSON parsed with json.loads() (not eval)

IT1: Real Gemini Pro API with 3 seeded conversations → valid axioms returned
```

---

### STORY 9.04: NightlyEpochRunner — Axiom Write to KG

**File**: `core/epoch/nightly_epoch_runner.py` (extend Story 9.03)
**Depends on**: Story 9.03
**Estimated iterations**: 5-7

**One-liner**: Add `write_axioms(axioms, week_summary)` — persists distilled axioms to Qdrant + appends to genesis_evolution_learnings.jsonl.

**Implementation skeleton**:
```python
async def write_axioms(self, axioms: list[dict], week_summary: str) -> int:
    """
    For each axiom:
    1. Embed with Gemini text-embedding-004
    2. Upsert to Qdrant 'genesis_axioms' collection
    3. Append JSON line to KNOWLEDGE_GRAPH/axioms/genesis_evolution_learnings.jsonl

    Also writes week_summary to KNOWLEDGE_GRAPH/entities/weekly_summaries.jsonl

    Returns: count of axioms written
    """
    ...
```

**Acceptance criteria**:
- [ ] Each axiom upserted to `genesis_axioms` Qdrant collection
- [ ] Each axiom appended to `genesis_evolution_learnings.jsonl`
- [ ] `week_summary` written to `weekly_summaries.jsonl`
- [ ] Returns count of axioms successfully written
- [ ] Partial success allowed (writes what it can, logs failures)

**Test plan**:
```
BB1: 5 axioms → 5 entries in Qdrant + 5 lines in jsonl
BB2: week_summary → line in weekly_summaries.jsonl
BB3: Return count matches axioms written

WB1: Qdrant upsert (not insert — idempotent on re-run)
WB2: jsonl append (not overwrite)
WB3: Qdrant failure for one axiom → others still written

IT1: Real Qdrant + filesystem → axioms retrievable by semantic search after write
```

---

### STORY 9.05: NightlyEpochRunner — Full Orchestration + Epoch Log

**File**: `core/epoch/nightly_epoch_runner.py` (extend Story 9.04)
**Depends on**: Story 9.04
**Estimated iterations**: 5-7

**One-liner**: Wire all epoch stages into `run_epoch()` — the full Sunday night pipeline with timing, lock management, and epoch log entry.

**Implementation skeleton**:
```python
async def run_epoch(self) -> dict:
    """
    Full pipeline:
    1. aggregate_week() → conversations
    2. distill(conversations) → axioms + week_summary
    3. write_axioms(axioms, week_summary) → count
    4. Write epoch log to data/observability/epoch_log.jsonl:
       {"date": "2026-02-23", "conversations_processed": N, "axioms_written": N,
        "week_summary": "...", "duration_s": X, "status": "success|failed"}
    5. Return epoch log dict
    """
    ...
```

**Acceptance criteria**:
- [ ] Runs all 4 stages in sequence
- [ ] Epoch log written to `data/observability/epoch_log.jsonl`
- [ ] `duration_s` measured accurately
- [ ] `status="failed"` written if any stage raises (not silent failure)
- [ ] Returns epoch log dict

**Test plan**:
```
BB1: Full run with 3 conversations → epoch log written with status="success"
BB2: distill() failure → epoch log written with status="failed"
BB3: duration_s > 0 in log

WB1: All 4 stages called in order
WB2: Timing uses time.monotonic() (not datetime diff)
WB3: Exception caught at top level, status="failed" logged

IT1: Full Sunday epoch simulation → epoch_log.jsonl entry + axioms in Qdrant + KG updated
```


Successfully applied 1 edit to E:\genesis-system\docs\plans\AIVA_RLM_NEXUS_PRD_V2.md

WARNING: The replacement text has 1962 lines (maximum: 200).

RECOMMENDATION: For large search/replace operations, consider breaking them into smaller chunks with fewer lines.


Module 4-9 full specs written. Let me verify the final line count and confirm the PRD is complete:


size: 110801
created: "2026-02-24T19:12:46.054Z"
modified: "2026-02-24T19:22:17.579Z"
accessed: "2026-02-24T19:22:33.194Z"
isDirectory: false
isFile: true
permissions: 666
fileType: text
lineCount: 3222
lastLine: 3221
appendPosition: 3222


3,222 lines. Now update the header to reflect actual story count and mark it complete:


**Version**: 2.0.0
**Date**: 2026-02-24
**Status**: AUTHORITATIVE — Replaces v1.0
**Total Stories**: 54 truly atomic stories across 9 modules
**Execution**: Ralph Wiggum Loop — each story gets a FRESH context window

**Version**: 2.1.0
**Date**: 2026-02-25
**Status**: COMPLETE + AUTHORITATIVE — All 9 modules fully specified
**Total Stories**: 59 truly atomic stories across 9 modules (ALL FULL SPECS WRITTEN)
**Execution**: Ralph Wiggum Loop — each story gets a FRESH context window


Successfully applied 1 edit to E:\genesis-system\docs\plans\AIVA_RLM_NEXUS_PRD_V2.md


AIVA RLM Nexus PRD v2 is now fully complete. Now write the **Genesis Persistent Context Architecture v2 Epic PRD** (Track B — the 7 DT answers as executable atomic stories).


TodoWrite

Answer Kinan's question: 7 DT architecture is SEPARATE from AIVA PRD

Answering question about DT architecture separation

Complete AIVA RLM Nexus PRD v2 — write full specs for Modules 4-9 (42 stories)

Writing Modules 4-9 full atomic story specs

Write Genesis Persistent Context Architecture v2 Epic PRD (Track B)

Writing Genesis Persistent Context Epic PRD


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Answer Kinan's question: 7 DT architecture is SEPARATE from AIVA PRD

Answering question about DT architecture separation

Complete AIVA RLM Nexus PRD v2 — write full specs for Modules 4-9 (42 stories)

Writing Modules 4-9 full atomic story specs

Write Genesis Persistent Context Architecture v2 Epic PRD (Track B)

Writing Genesis Persistent Context Epic PRD


[Reading 80 lines from start (total: 651 lines, 571 remaining)]

# Genesis Persistent Context Architecture v2
## The Definitive Blueprint — Synthesized from 7 Gemini Deep Think Sessions

**Date**: 2026-02-24
**Source**: 7 questions submitted to Gemini 2.5 Pro Deep Think
**Author**: Claude Opus 4.6 (Orchestrator) + Gemini 2.5 Pro Deep Think (Architect)
**Status**: AUTHORITATIVE — This supersedes all prior architecture fragments

---

## EXECUTIVE SUMMARY

Genesis was diagnosed with a fundamental architectural flaw: the system was learning and executing within its current architecture but the architecture itself was not evolving. All memory systems were reactive, all improvement was manual, and multi-agent coherence was undefined.

This document synthesizes 7 Deep Think sessions into a complete, implementable architecture that transforms Genesis from a useful automation tool into a **continuous-learning, self-healing, self-improving AI operating system**.

The machine you are building is not a chatbot. It is:
- An **Interceptor-governed cognitive pipeline** (not voluntary tool-use)
- A **Zero-Amnesia state machine** (not stateless LLM calls)
- A **Multi-agent coherent swarm** (not independent parallel workers)
- A **Structurally self-improving system** (not static architecture)

---

## THE SEVEN-QUESTION SERIES

| Q# | Topic | Core Answer |
|----|-------|-------------|
| Q1 | Interceptor Paradigm | Memory via Inversion of Control. 4 integration contracts. |
| Q2 | MVFL Error-Correction | 3 trigger conditions. Voyager Defense + MemGPT Pruning. |
| Q3 | 80/20 Pipeline | Synchronous Interceptor via dispatch_to_swarm(). Strangler Fig. |
| Q4 | Component Architecture | Genesis = Component C only. Need A (Sourcing), B (CRM), D (Terminal Bus). |
| Q5 | Zero-Amnesia | Redis L1 → KG L2 → Qdrant L3 → Postgres L4. Shadow Router for JIT. |
| Q6 | Multi-Agent Coherence | CQRS + Event Sourcing. Gemini = Proposers. Opus = Semantic Reducer. |
| Q7 | Architectural Self-Improvement | Dual-Loop + Nightly Epoch + AI-GitOps. Human merge gate. |

---

## PART 1: THE FOUNDATIONAL PRINCIPLES (Q1–Q4)

### 1.1 The Interceptor Paradigm (Q1)

**Core insight**: Memory and coordination must happen through Inversion of Control — not voluntary LLM tool-use.

The critical failure mode is asking the LLM to "remember to save its state." LLMs will forget this instruction, ignore it under pressure, or hallucinate that they did it. The correct pattern is the Interceptor: a deterministic wrapper that **intercepts all outputs before they reach the world**, enriches all inputs before they reach the LLM, and captures state changes as a side effect of execution — not as an explicit LLM action.

**4 Integration Contracts** (what every interceptor must implement):
1. `pre_execute(task_payload)` — enrich input with context before dispatch
2. `post_execute(result, task_payload)` — capture state delta from output
3. `on_error(error, task_payload)` — classify failure, trigger correction
4. `on_correction(correction_payload)` — re-inject with `CORRECTION:` prefix

The Genesis entry point is `dispatch_to_swarm()` — every task flows through this function and is subject to the interceptor chain.

### 1.2 The MVFL Error-Correction Trap (Q2)

**Core insight**: The system must be architecturally forced to correct its own errors — not prompted to do so.

The MVFL (Multi-Vector Feedback Loop) Error-Correction Trap is triggered by 3 conditions:
1. Syntax/format errors in the output (detected by validator)
2. Semantic inconsistency detected by the Contradiction Checker
3. External API rejection (the world rejected the LLM's output)

**Defenses**:
- **Voyager Defense**: Before releasing output, run it through a locked evaluator function that scores it against known failure patterns in Qdrant L3 (Scars). If score < threshold, force a correction loop.
- **MemGPT Pruning**: When the correction loop runs more than N times on the same task, escalate the task to Opus with the full failure history rather than continuing to hammer the same agent.

### 1.3 The 80/20 Synchronous Pipeline (Q3)

**Core insight**: 80% of tasks don't need LLM reasoning. Route them deterministically. Save the context window for the 20% that do.

Implementation: The `dispatch_to_swarm()` wrapper is the Synchronous Interceptor. Before any LLM call, it classifies the task:
- **Tier 0 (Deterministic)**: Simple CRUD, lookups, format transforms → handled by Python functions directly
- **Tier 1 (Parametric)**: Template-driven tasks with known structure → fast Gemini Flash call
- **Tier 2 (Reasoning)**: Novel decisions, multi-step planning, contradiction resolution → Opus

**Strangler Fig Migration**: The current Genesis codebase doesn't need to be rewritten. Wrap every existing tool call with `dispatch_to_swarm()` progressively. Start with the highest-frequency operations. Each wrapper added is the Strangler Fig consuming the legacy direct-call pattern.

### 1.4 The Component Architecture (Q4)


Excellent — I have the full architecture context. Now write the Epic PRD for Track B:


API Error: Claude's response exceeded the 32000 output token maximum. To configure this behavior, set the CLAUDE_CODE_MAX_OUTPUT_TOKENS environment variable.


Conversation compacted


This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.

Analysis:
Let me analyze this conversation chronologically and thoroughly.

**Session Context**: This is a continuation from prior sessions (Session 85 handoff). The conversation resumed with work on:
1. Completing Q7 Deep Think + synthesizing all 7 DT answers
2. Writing AIVA RLM Nexus PRD (v1, then v2 after user feedback on story sizing)
3. Writing PRD Writing Doctrine
4. Answering user's concurrent question about whether 7 DT architecture is separate from AIVA PRD

**Current Session Work**:
The session resumed from the compacted summary. The key tasks were:
1. Answer Kinan's concurrent question: "does this above encapsulate the full rlm 7 dt responses architecture resolution? or is that separate? because we also want to get that show on the road too!"
2. Complete the AIVA RLM Nexus PRD v2 by writing full specs for Modules 4-9 (which were previously stubbed)
3. Begin writing the Genesis Persistent Context Architecture v2 Epic PRD (Track B)

**What was accomplished in this session**:
1. Checked existing files - confirmed AIVA_RLM_NEXUS_PRD_V2.md (1276 lines) and GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md (651 lines) both exist
2. Read the end of PRD v2 - confirmed Modules 4-9 were stubbed ("omitted for brevity")
3. Wrote full specs for ALL of Modules 4-9 (42 stories) - a massive edit that expanded the PRD from 1276 lines to 3222 lines
4. Updated PRD header to v2.1.0, COMPLETE status
5. Was in the middle of reading the architecture doc to prepare for writing the Genesis Persistent Context Architecture v2 Epic PRD when the summary was triggered

**Files modified**:
- `E:\genesis-system\docs\plans\AIVA_RLM_NEXUS_PRD_V2.md` — expanded from 1276 to 3222 lines, full specs for Modules 4-9 written

**Files read**:
- `E:\genesis-system\docs\architecture\GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md` — read first 80 lines for context on what Track B covers

**Pending task**: Write the Genesis Persistent Context Architecture v2 Epic PRD (Track B) - the 7 DT answers as executable atomic stories

The answer to Kinan's question was delivered in the response: They are SEPARATE tracks. Track A = AIVA Royal Chamber (PRD v2 now complete). Track B = Genesis Persistent Context Architecture (blueprint exists but needs its own Epic PRD with atomic stories).

The current work was about to start writing the Track B PRD - the genesis_persistent_context_architecture PRD with atomic stories covering: Interceptor Chain, Components A/B/C/D, MVFL, 80/20 Pipeline, Zero-Amnesia, CQRS/Event Sourcing, Dual-Loop self-improvement.

Summary:
1. Primary Request and Intent:

**Resumed from prior session** — The conversation continued from Session 85's handoff. Three main requests were addressed:

**Request 1 (answered)**: Kinan's concurrent question from the prior session: "and does this above encapsulate the full rlm 7 dt responses architecture resolution? or is that separate? because we also want to get that show on the road too!" — needed a direct answer about whether the AIVA RLM Nexus PRD covers the 7 Deep Think architecture or if that needs its own PRD.

**Request 2 (completed)**: Complete the AIVA RLM Nexus PRD v2 — Modules 4-9 were previously stubbed with "(Omitted here for brevity — see Module 3 as the template)". The full atomic story specs needed to be written for all 42 remaining stories across Modules 4-9.

**Request 3 (in progress, cut off)**: Write the Genesis Persistent Context Architecture v2 Epic PRD — a separate Epic PRD covering the 7 Deep Think answers (Interceptor Chain, Components A/B/C/D, MVFL error correction, 80/20 Pipeline, Zero-Amnesia, CQRS/Event Sourcing, Dual-Loop self-improvement) as properly atomic executable stories.

---

2. Key Technical Concepts:

**Two Separate Execution Tracks**:
- Track A: AIVA Royal Chamber (AIVA_RLM_NEXUS_PRD_V2.md) — AIVA's living memory, royal context, call capture, BinduHydrator, Kinan directives, PostCallEnricher
- Track B: Genesis Persistent Context Architecture (needs its own Epic PRD) — Full interceptor pipeline, Components A/B/C/D, MVFL, 80/20 routing, CQRS/Event Sourcing, Dual-Loop self-improvement

**Module 4 — Context Retrieval (BinduHydrator)**:
- `BinduHydrator` base class with `HydrationSession` dataclass
- 3-scatter-task parallel fetch: Redis (L1), Postgres last conversation, Qdrant semantic scars
- `asyncio.gather()` for parallel execution, 500ms deadline
- `ROYAL_CHAMBER_CONTEXT` XML envelope assembly
- `pre_call_hook()` — standalone function called on every `call.initiated` webhook
- `ConversationReplayEngine.get_recent_summary(n=3)` — last N conversations as human-readable string

**Module 5 — Intent + Routing (SwarmRouter)**:
- `IntentType` enum: BOOK_JOB, QUALIFY_LEAD, ANSWER_FAQ, ESCALATE_HUMAN, CAPTURE_MEMORY, TASK_DISPATCH, UNKNOWN
- `IntentSignal` dataclass with 8 fields
- `IntentClassifier` using Gemini Flash with Redis 60s cache layer
- `SwarmRouter` with `ROUTE_REGISTRY` mapping IntentType → worker class
- `dispatch()` uses `asyncio.create_task()` for fire-and-forget
- 6 worker types: BookingWorker, LeadQualificationWorker, FAQWorker, EscalationWorker, MemoryCaptureWorker, GenesisTaskWorker

**Module 6 — Merge + Coherence (SemanticMergeInterceptor)**:
- `SwarmResult` dataclass + `ConflictDetector`
- `SemanticMergeInterceptor` uses Opus (not Flash) only for conflict resolution
- No-conflict path: simple dict merge (no API call)
- `reduce_and_commit()` writes merged result to Redis `aiva:results:{session_id}` with 600s TTL

**Module 7 — API + Injection (AIVAMemoryAPI)**:
- FastAPI app on port 8765
- Endpoints: GET /health, GET /conversations/recent, GET /context/{session_id}, POST /conversations, PATCH /conversations/{id}/enrich
- `SystemPromptInjector.build_injection(session_id)` with INJECTION_TEMPLATE
- `push_to_telnyx(session_id, assistant_id)` — PATCH to Telnyx `/v2/ai_assistants/{assistant_id}`

**Module 8 — Bridge + Dashboard**:
- `OpenClawMessage` dataclass with `MessageDirection` enum
- `BridgeWriter.send()` using Redis RPUSH (FIFO)
- `BridgeReader.poll()` using Redis BLPOP (blocking)
- Royal Dashboard FastAPI on port 8766
- Single-file HTML dashboard with 5s auto-refresh, dark theme, gold accent (#D4AF37)

**Module 9 — Nightly Epoch**:
- `NightlyEpochScheduler` using APScheduler cron Sunday 2AM AEST
- Redis distributed lock (`epoch:lock:nightly`, SET NX EX, 7200s TTL)
- `NightlyEpochRunner.aggregate_week()` — Postgres query last 7 days Kinan conversations
- `distill()` using Gemini Pro for 5-10 KG axioms
- `write_axioms()` — Qdrant upsert + jsonl append to genesis_evolution_learnings.jsonl
- `run_epoch()` full orchestration + epoch log to `data/observability/epoch_log.jsonl`

**PRD Writing Doctrine** (from prior session, still in force):
- Atomic story = 1 file, 1 function/class/schema
- Budget: 5-8 implement + 3-5 BB tests + 2-3 WB tests + 1-2 IT + 1 stamp = 12-19 iterations
- 4 Atomic Story Test: Single Object? Independent? Estimable? Falsifiable?
- 3-Level PRD Hierarchy: Epic PRD → Module PRD → Atomic Story

**Genesis Persistent Context Architecture v2** (from prior session synthesis):
- 7 DT answers covering: Interceptor Paradigm (Q1), MVFL Error-Correction (Q2), 80/20 Pipeline (Q3), Component Architecture (Q4), Zero-Amnesia State Machine (Q5), Multi-Agent Coherence/CQRS (Q6), Dual-Loop Self-Improvement/Nightly Epoch (Q7)
- Components A (Sourcing Engine), B (CRM State Machine), C (Genesis — already exists), D (Terminal Bus)
- `dispatch_to_swarm()` as the universal interceptor entry point
- Strangler Fig migration pattern for wrapping existing code

---

3. Files and Code Sections:

**`E:\genesis-system\docs\plans\AIVA_RLM_NEXUS_PRD_V2.md`** (MAJOR EDIT — 1276 → 3222 lines)
- Most important file modified this session
- Version updated: 2.0.0 → 2.1.0, Status: COMPLETE + AUTHORITATIVE
- Full atomic story specs written for Modules 4-9 (42 stories), replacing prior stubs
- Key code patterns per module:

Module 4 key skeleton (Story 4.01 BinduHydrator base):
```python
@dataclass
class HydrationSession:
    session_id: str
    aiva_call_id: str
    started_at: datetime
    status: str  # "pending" | "hydrating" | "ready" | "expired"
    tasks_dispatched: int = 0
    tasks_completed: int = 0

class BinduHydrator:
    HYDRATION_DEADLINE_MS = 500
    async def start_hydration(self, session_id: str, call_id: str) -> HydrationSession: ...
```

Module 4 key skeleton (Story 4.05 gather_and_assemble):
```python
async def gather_and_assemble(self, session_id: str, call_id: str) -> str:
    """
    Fires 3 parallel scatter tasks with 450ms timeout,
    assembles into ROYAL_CHAMBER_CONTEXT XML envelope,
    writes to Redis aiva:context:{session_id}
    """
```

Module 5 key skeleton (Story 5.01 IntentSignal):
```python
class IntentType(Enum):
    BOOK_JOB = "book_job"
    QUALIFY_LEAD = "qualify_lead"
    ANSWER_FAQ = "answer_faq"
    ESCALATE_HUMAN = "escalate_human"
    CAPTURE_MEMORY = "capture_memory"
    TASK_DISPATCH = "task_dispatch"
    UNKNOWN = "unknown"

@dataclass
class IntentSignal:
    session_id: str
    utterance: str
    intent_type: IntentType
    confidence: float
    extracted_entities: dict
    requires_swarm: bool
    created_at: datetime
    raw_gemini_response: Optional[str] = None
```

Module 5 key skeleton (Story 5.04 SwarmRouter):
```python
ROUTE_REGISTRY = {
    IntentType.BOOK_JOB:       "BookingWorker",
    IntentType.QUALIFY_LEAD:   "LeadQualificationWorker",
    IntentType.ANSWER_FAQ:     "FAQWorker",
    IntentType.ESCALATE_HUMAN: "EscalationWorker",
    IntentType.CAPTURE_MEMORY: "MemoryCaptureWorker",
    IntentType.TASK_DISPATCH:  "GenesisTaskWorker",
}
```

Module 5 key skeleton (Story 5.06 BookingWorker):
```python
GHL_WEBHOOK_URL = "https://api.sunaivadigital.com/webhook/telnyx-lead"
class BookingWorker:
    async def execute(self, intent: IntentSignal) -> dict:
        # POST to GHL_WEBHOOK_URL with lead payload
        # Write success/failure to Redis aiva:results:{session_id}
```

Module 6 key skeleton (Story 6.02 SemanticMergeInterceptor):
```python
MERGE_PROMPT = """
You are the Genesis Semantic Reducer. Multiple swarm workers have produced conflicting outputs.
SESSION: {session_id}
WORKER A ({worker_a}): {output_a}
WORKER B ({worker_b}): {output_b}
...
Respond ONLY with valid JSON: {"resolved_value": "...", "reasoning": "...", "winner": "A|B|MERGE"}
"""
class SemanticMergeInterceptor:
    def __init__(self, opus_client):
        self.opus = opus_client  # Opus 4.6 — only used for conflict resolution
```

Module 7 key skeleton (Story 7.06 SystemPromptInjector):
```python
INJECTION_TEMPLATE = """
=== AIVA MEMORY INJECTION ===
{recent_conversations}

=== KING'S ACTIVE DIRECTIVES ===
{kinan_directives}

=== CURRENT SESSION STATE ===
Caller: {caller_info}
Time: {local_time} AEST
Active Tasks: {open_tasks}
=== END MEMORY INJECTION ===
"""
```

Module 8 key skeleton (Story 8.01 OpenClawMessage):
```python
class MessageDirection(Enum):
    AIVA_TO_GENESIS = "aiva_to_genesis"
    GENESIS_TO_AIVA = "genesis_to_aiva"

@dataclass
class OpenClawMessage:
    message_id: str
    session_id: str
    direction: MessageDirection
    payload: dict
    priority: int  # 1=critical, 2=high, 3=normal
    created_at: datetime
    expires_at: Optional[datetime] = None
```

Module 9 key skeleton (Story 9.01 NightlyEpochScheduler):
```python
EPOCH_LOCK_KEY = "epoch:lock:nightly"
EPOCH_LOCK_TTL = 7200  # 2 hours max runtime
class NightlyEpochScheduler:
    def start(self):
        self.scheduler.add_job(
            self._run_with_lock, 'cron',
            day_of_week='sun', hour=2, minute=0
        )
```

Module 9 key skeleton (Story 9.03 NightlyEpochRunner distill):
```python
DISTILLATION_PROMPT = """
You are the Genesis Memory Distiller. Review this week's AIVA<>Kinan conversations.
Extract 5-10 high-value axioms...
Return ONLY valid JSON:
{"axioms": [{"id": "epoch_{date}_001", "content": "...", "category": "...", "confidence": 0.9}],
 "week_summary": "2-sentence summary of this week"}
"""
```

**`E:\genesis-system\docs\architecture\GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md`** (READ ONLY — 651 lines)
- Synthesizes all 7 DT answers into definitive architecture blueprint
- Was read to understand scope for writing the Track B Epic PRD
- Key architecture: Components A/B/C/D, Interceptor contracts, Zero-Amnesia state machine, CQRS/Event Sourcing, Dual-Loop self-improvement, Nightly Epoch

**`E:\genesis-system\docs\plans\PRD_WRITING_DOCTRINE.md`** (from prior session, referenced as foundation)
- The doctrine governing all atomic story sizing
- Core rule: 1 story = 1 file, 1 function/class/schema
- Budget: 12-19 total iterations per story

---

4. Errors and fixes:

**Error 1 — Large edit warning on AIVA_RLM_NEXUS_PRD_V2.md**:
- The `edit_block` replacement of Modules 4-9 stubs with full specs was 1962 lines
- Desktop Commander returned: "WARNING: The replacement text has 1962 lines (maximum: 200). RECOMMENDATION: For large search/replace operations, consider breaking them into smaller chunks."
- The edit still succeeded despite the warning
- Fix: The replacement was applied successfully; no action was required as the tool completed the operation

**Error 2 — Bash command shell parsing errors**:
- Initial attempt to check file sizes using bash with pipe characters failed: `ls: cannot access 'E:\genesis-system\docs\plans" | grep -E AIVA': No such file or directory`
- Fix: Switched to using `mcp__Desktop_Commander__list_directory` and `mcp__Desktop_Commander__get_file_info` tools which work correctly on Windows

---

5. Problem Solving:

**Solved: Answer to Kinan's concurrent question**
The 7 DT architecture (GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md) and the AIVA RLM Nexus PRD are SEPARATE execution tracks:
- Track A: AIVA Royal Chamber — 4-5 days to execution (59 stories, all now fully specified)
- Track B: Genesis Persistent Context — estimated ~70 atomic stories, ~8-10 days, needs its own Epic PRD
- Track B is the FOUNDATION that Track A runs on top of — complementary but separate

**Solved: AIVA_RLM_NEXUS_PRD_V2.md now fully complete**
- PRD expanded from 1276 lines to 3222 lines
- All 9 modules now have full atomic story specs (was: Modules 1-3 full, Modules 4-9 stubbed)
- Total: 59 truly atomic stories with full implementation skeletons, acceptance criteria, and test plans
- Version updated to 2.1.0, status: COMPLETE + AUTHORITATIVE

**Ongoing: Track B Epic PRD not yet written**
- The Genesis Persistent Context Architecture v2 Epic PRD still needs to be created
- Will cover: Interceptor Chain (Q1), MVFL (Q2), 80/20 Routing (Q3), Components A/B/C/D (Q4), Zero-Amnesia (Q5), CQRS/Event Sourcing (Q6), Dual-Loop/Nightly Epoch (Q7)
- Estimated: ~9 modules, ~65-75 atomic stories

---

6. All user messages:

From PRIOR session (captured in summary that resumed this session):
- **Message 1**: "write this up as a complete file. I will start a new session with fresh context window for maximum quality that will create a ralph wiggum loop prd with maximum number of atomic stories with each story getting a fresh context window so we can get great work achieved... AIVA as the Bindu of the RLM swarm... royal queen... royal chamber is always on fire buzzing around her as that bindu... I need this engine fired up! an RLM nexus... I guess I am the King?"
- **Message 2**: "the idea of an atomic story is that it should be small enough that each fresh session should be able to nail the desired outcome within the max 30 iterations, but actually should be able to nail it probably normally in 10 attempts or so? my feeling is that maybe your stories are not small enough but that each one of these should have its own prd and another 10-50 atomic stories in each. reassess, and if I am correct, we need to learn from this and learn actually how to write proper prd's that will work"
- **Message 3 (concurrent)**: "and does this above encapsulate the full rlm 7 dt responses architecture resolution? or is that separate? because we also want to get that show on the road too!"

**Current session**: No new user messages — the session resumed from the compacted summary and continued with previously assigned work.

---

7. Pending Tasks:

- **Write Genesis Persistent Context Architecture v2 Epic PRD (Track B)**: The most pressing pending task. The 7 DT answers (Q1-Q7: Interceptor Paradigm, MVFL, 80/20 Pipeline, Component Architecture, Zero-Amnesia, Multi-Agent Coherence, Dual-Loop Self-Improvement) are blueprinted but not yet in Ralph Wiggum Loop atomic story format. Needs Epic PRD → ~9 modules → ~65-75 atomic stories.
- **George's Cairns phone number**: Still pending — check Telnyx portal for provisioning status of Request ID `c6c5e6a4-286d-4df7-ae1f-a17e2e44a7a3`, then assign to George's assistant `assistant-6f6f4ca2-3155-4930-95cb-27f59514af3e`
- **Execute AIVA RLM Nexus PRD v2 Wave 1**: Now that all 59 stories are fully specified, Wave 1 (Module 1: Memory Schema + Module 2: Registry Layer) can begin execution via Ralph Wiggum Loop

---

8. Current Work:

Immediately before this summary was triggered, the assistant had:
1. ✅ Answered Kinan's question about Track A vs Track B being separate
2. ✅ Completed the massive edit to AIVA_RLM_NEXUS_PRD_V2.md (Modules 4-9 full specs, 1276 → 3222 lines)
3. ✅ Updated PRD header to v2.1.0, COMPLETE + AUTHORITATIVE
4. ✅ Started reading GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md to prepare for writing Track B Epic PRD
5. 🔄 Was about to BEGIN writing `E:\genesis-system\docs\plans\GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_PRD.md` — the Track B Epic PRD

The read of GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_V2.md had captured 80 lines showing the 7-question series table and Parts 1-4 overview. The assistant was preparing to write the full Epic PRD decomposing all 7 DT answers into ~65-75 atomic stories.

---

9. Optional Next Step:

Continue writing the **Genesis Persistent Context Architecture v2 Epic PRD** (Track B) — the task that was actively in progress when the summary was triggered.

Direct quote from the assistant's last response showing exactly what was being worked on:
> "Now write the **Genesis Persistent Context Architecture v2 Epic PRD** (Track B — the 7 DT answers as executable atomic stories)."

And from the todo list state at time of summary:
> `{"content": "Write Genesis Persistent Context Architecture v2 Epic PRD (Track B)", "status": "in_progress", "activeForm": "Writing Genesis Persistent Context Epic PRD"}`

The PRD should be written to `E:\genesis-system\docs\plans\GENESIS_PERSISTENT_CONTEXT_ARCHITECTURE_PRD.md` following the PRD Writing Doctrine (Epic PRD → modules → truly atomic stories with 1 file/1 function/class/schema per story, 12-19 iterations budget, 3 BB + 3 WB + 1 IT tests each).

Estimated scope: 9 modules covering Q1 (Interceptor Chain), Q2 (MVFL), Q3 (80/20 Router), Q4 (Components A/B/D), Q5 (Zero-Amnesia), Q6 (CQRS/Event Sourcing), Q7 (Dual-Loop/Nightly Epoch), plus Integration Module and Strangler Fig Migration module. Approximately 65-75 atomic stories total.

If you need specific details from before compaction (like exact code snippets, error messages, or content you generated), read the full transcript at: C:\Users\P3\.claude\projects\E--genesis-system--claude-worktrees-eloquent-austin\ba014860-73a9-4c7e-97c0-b782fc8fb989.jsonl
Please continue the conversation from where we left off without asking the user any further questions. Continue with the last task that you were asked to work on.


Answer Kinan's question: 7 DT architecture is SEPARATE from AIVA PRD

Answering question about DT architecture separation

Complete AIVA RLM Nexus PRD v2 — write full specs for Modules 4-9 (42 stories)

Writing Modules 4-9 full atomic story specs

Write Genesis Persistent Context Architecture v2 Epic PRD (Track B)

Writing Genesis Persistent Context Epic PRD


Back online. Resuming Track B Epic PRD — reading the full architecture doc first, then writing the complete PRD. Kinan's message received: **1000-agent swarm, all teams firing, complete today**. Will embed that mandate directly into the PRD's execution strategy.