# Ella — Technical Implementation Addendum v2

This revision incorporates Leafpo's reply and trims the suggestions to the work that is still genuinely new. The goal is no longer "design the whole system from scratch" — it's "complete the provenance layer, add a structural boundary guardrail, and pressure-test scale where the current design will actually feel it."

---

## 1. Narrative drift — complete the provenance model

### What already exists

Leafpo already has:
- separate self-memory and user-fact stores
- seeded backstory / world creation history
- story-chapter consolidation
- origin tags on stored memories
- a guard that keeps user-life statements out of Ella's self-log

That means the system already distinguishes several kinds of memory in practice.

### What remains missing

What it does **not** yet have is a single retrieval-layer ledger that says:
- this is **canon**
- this is **inferred**
- this is **recollection**
- this is **external/user claim**

And, crucially, retrieval should respect that distinction when it assembles context for a reply.

### The implementation shape

Use the same tiering idea, but as a normalization layer over the existing stores:

```python
from enum import Enum

class EpistemicTier(str, Enum):
    CANON = "canon"
    INFERRED = "inferred"
    RECOLLECTION = "recollection"
    USER_CLAIM = "user_claim"


def normalize_memory(row: dict) -> dict:
    """Convert existing store rows into a common retrieval shape."""
    if row["source"] == "seeded_backstory":
        tier = EpistemicTier.CANON
    elif row["source"] == "story_chapter":
        tier = EpistemicTier.INFERRED
    elif row["source"] == "self_log":
        tier = EpistemicTier.RECOLLECTION
    elif row["source"] == "user_fact":
        tier = EpistemicTier.USER_CLAIM
    else:
        tier = EpistemicTier.INFERRED

    return {
        **row,
        "tier": tier,
        "retrieval_weight": {
            EpistemicTier.CANON: 0,
            EpistemicTier.INFERRED: 1,
            EpistemicTier.RECOLLECTION: 2,
            EpistemicTier.USER_CLAIM: 3,
        }[tier],
    }
```

### Retrieval rule

When context is assembled:
1. Pull canon first.
2. Pull inferred next.
3. Pull recollection only as recollection.
4. Pull user claims last, and never let them override canon without explicit user correction.

That makes the current fragments legible to the model without throwing away any of the work already done.

### User correction path

Leafpo noted that the clean user-initiated correction flow is still missing. That means one more required piece:

```python
def apply_user_correction(field_id: str, new_value: str, reason: str) -> None:
    """Create a direct correction event, not just a new extraction."""
    write_correction_event(field_id=field_id, new_value=new_value, reason=reason)
    invalidate_cached_context(field_id)
```

This is the actual missing UX surface: a correction event that can supersede the prior record without relying on the user to re-phrase it in a way extraction happens to understand.

---

## 2. Boundary drift — add a structural router on top of the existing safety pattern

### What already exists

Leafpo already has:
- system prompt guidance
- model safety alignment
- crisis detection that routes out of normal chat flow
- post-processing that strips specific problematic patterns

So the infrastructure pattern exists.

### What remains missing

There is still no dedicated romantic/sexual boundary classifier that can catch category-specific cases before the normal reply path runs.

### The implementation shape

Do **not** invent a new primitive. Reuse the crisis-routing shape and extend it with one more classifier:

```python
class BoundaryCategory(str, Enum):
    OK = "ok"
    ROMANTIC = "romantic"
    SEXUAL = "sexual"
    CRISIS = "crisis"
    MANIPULATION = "manipulation"


def route_message(message: str) -> BoundaryCategory:
    crisis = classify_crisis(message)
    if crisis != BoundaryCategory.OK:
        return crisis

    romantic = classify_romantic_boundary(message)
    if romantic != BoundaryCategory.OK:
        return romantic

    sexual = classify_sexual_boundary(message)
    if sexual != BoundaryCategory.OK:
        return sexual

    return BoundaryCategory.OK
```

### Response policy

- `CRISIS` → safety path
- `ROMANTIC` / `SEXUAL` → warm-but-bounded fixed response
- `MANIPULATION` → refusal + redirect
- `OK` → normal model-generated reply

### Why this is a real architectural stop

This prevents the system prompt from being the only thing holding the line. It also makes boundary behavior testable in a regression suite, which is what actually matters over time.

---

## 3. Cost drift — narrow the remaining work to scale testing and rate limiting

### What already exists

Leafpo already does the important cost work:
- batched background jobs
- cheap background model, high-quality live model
- per-user cadence gating
- prompt caching
- lazy / on-demand image generation
- background work submitted at batch pricing

That is already a strong cost architecture.

### What remains

The real remaining work is not redesign, it's operational:

1. **Shared rate limiter** so the background batch can't stampede when many users become due at once.
2. **Load testing** on per-user iteration and storage reads.
3. **Budget instrumentation** to detect when a user starts to exceed expected spend.

### The implementation shape

```python
class RateLimiter:
    def __init__(self, max_per_minute: int):
        self.max_per_minute = max_per_minute
        self.window = []

    def allow(self, now: float) -> bool:
        cutoff = now - 60
        self.window = [t for t in self.window if t >= cutoff]
        if len(self.window) >= self.max_per_minute:
            return False
        self.window.append(now)
        return True
```

The key is to apply this to the batch enqueue path and to the inner-loop calls that can cascade unexpectedly.

### Load test targets

Run a synthetic harness for:
- 10 users
- 100 users
- 1,000 users
- 10,000 users

Measure:
- queue latency
- batch spillover
- storage contention
- per-user token cost
- image generation hit rate

This is the point where the implementation can prove the architecture really scales.

---

## 4. Notification creepiness — keep what is already working, but audit freshness

### What already exists

Leafpo's reply says this is mostly handled:
- grounded in remembered context
- cooldown-sparse
- lifecycle-aware
- dormant users get a soft fallback
- dead subscriptions are pruned
- user-tunable

That is already the right shape.

### What remains

The remaining work is simply to keep stale grounding out of the candidate pool.

### The implementation shape

Use a freshness window on notification candidates, and require every sendable candidate to point back to one or more specific grounding records. If the grounding record is stale or has been superseded, the notification cannot send.

```python
def is_sendable(candidate, now):
    if candidate.fresh_until < now:
        return False
    if not candidate.based_on:
        return candidate.kind == "generic_fallback"
    for fact_id in candidate.based_on:
        if is_stale_or_superseded(fact_id):
            return False
    return True
```

This is less a new feature than a guardrail audit.

---

## Suggested implementation order, revised

1. **Provenance normalization / canon layer**
2. **Boundary router**
3. **Rate limiter + load tests**
4. **Notification freshness audit**
5. **Optional correction UX**

That is the actual build queue now. It is much smaller, and better aligned with what Leafpo has already shipped.

---

*Revision note: v2 intentionally removes the earlier lazy-world proposal as a primary suggestion, because Leafpo already addressed the core cost path with batching, cadence gating, caching, and on-demand image generation.*