# Rules in memory

The memory is read by a one-pass decision engine, not a reasoner. Measured while building the demos and cookbooks with the English checkpoint, four things decide how a rule should be written.

## 1. Put conditions in code: rules with `when`

The engine does not compare numbers. With the line "Mia's stop-loss for a session is 30 units." in memory, the stop answer was 0.12 to 0.18 at a loss of 12 and still 0.22 to 0.30 at a loss of 30, where the limit is reached (measured 2026-09-23, `cookbooks/rules_in_memory.py`).

So Jepela checks the condition in code and shows the engine the line only when it holds:

```json
POST /v1/memory/rules
{"subject": "mia", "when": "session_result <= -30", "text": "Mia's stop-loss is reached: she stops playing now."}

POST /v1/systemone
{"subject": "mia", "values": {"session_result": -32}, "state": "...", "questions": {...}}
```

`when` is an expression over the request's `values` and the state's fields (`reading.t > 75`, `days_since(order.date) > 30`, `contains(lower(message), "refund")`), evaluated by an allow-listed evaluator: comparisons, `and`/`or`/`not`, arithmetic, and the functions `days_since`, `days_between`, `len`, `lower`, `contains`, `number`, `abs`, `min`, `max`, `round`. Nothing else runs. A fired rule's text is read like a memory line, placed before the recalled lines, and listed in `memory.rules_fired`; a rule whose expression fails is listed in `rules_broken` and the decision goes on. Rules may expire: send `expires` (an ISO date or time) or `ttl_seconds` (at most ten years), and the rule list shows `expires_at`. A `when` has at most 300 characters and a rule's text at most 500; a subject holds at most 100 rules. Rules are checked only when the decision uses the subject's memory, not with `"memory": {"use": false}`. `GET /v1/memory/rules?subject=` lists them and `POST /v1/memory/rules/delete` removes one. A rule is billed as one memory line written.

Measured through the gateway (`measurements/jepela-gateway-live-2026-09-23.txt`, item 3): with the rule above, the stop answer was 0.11 at −10 (not fired) and 0.80 at −30 (fired). In the roulette demo the rule fired once, at −32, and the stop answer was 79%; before it fired the median stop answer was 13%.

## 2. Leave forbidden options out of the question

The engine does not read negation: naming the forbidden thing pulls it toward the answer. In the Dungeon demo, "Never walk along the corridor with the gold coins." raised the coins from 83% to 94%, and Jepela warns when such a line is written.

A line that says what to do can help: in the Dungeon's Stone Hall the old bridge had 94% without memory, and with the line "Next time there, the hero goes along a dark muddy path" the muddy path had 70% and the hero took it (checked by the demo's test). It does not always help. On ten roulette states where 17 had just come up, the engine bet on 17 in 9 of 10 with no memory, 9 of 10 with a rule naming 17, and 10 of 10 with "Mia bets red, black, even, odd, low or high". What worked every time was offering only Mia's bets in the question: 0 of 10 (measured 2026-09-23, `cookbooks/rules_in_memory.py`).

So: when an option must never be chosen, remove it before the engine sees it. Jepela can do that for you from memory: an **exclusion rule**.

```json
POST /v1/memory/rules
{"subject": "player-9", "text": "Gold coins are off for this player (coin bug).", "exclude": ["gold_coins"], "question": "reward"}
```

When the rule holds (no `when` means always), `gold_coins` is removed from the `reward` question in code, the answer shows it with probability 0 and `excluded` says why, and the rule's text is never sent to the engine. Measured: "Never give player-9 gold coins" as a line raised gold coins from 0.63 to 0.98; as an exclusion it was 0.00. An exclusion never removes a question's last option (`exclusions_skipped` says so). Use memory lines for what should make an allowed answer more likely.

## Pinned rules: lines that always count

A rule with `"pin": true` (and no `when`) is sent first at every decision about the subject, whatever the state says and whether recall would have found it: premium support, an opt-out, an allergy.

```json
POST /v1/memory/rules
{"subject": "acct-7781", "pin": true, "text": "Acct-7781 has premium support: every ticket from them is escalated."}
```

With a `when`, a pinned rule is sent first whenever its condition holds. The answer lists pinned rule ids in `memory.pinned`. Measured: this pinned line moved the same ticket from 0.35 to 0.86. Pin only what must count every time; every pinned line is read, and billed, at every decision about the subject.

## 3. Keep memories short

Lines can move an answer just by being there, in either direction: five neutral lines moved an escalation answer from 0.46 (no memory) to 0.28 (`measurements/jepela-recall-size-2026-09-23.json`). Replace lines rather than append them, and check with `"memory": {"placebo": true}`: it reports questions that move with neutral lines as much as with yours.

## 4. Answers near a threshold are noisy

In the roulette demo, with the stop threshold at 50%, stray answers of up to 52% ended 2 of 9 sessions early (2026-09-23). For expensive actions require a fired rule, or two answers in a row above the threshold, and count early actions.

## The pattern

- Facts as positive statements with the names your states use.
- Conditions as rules with `when`, evaluated in code on `values` and the state's fields.
- Options that must never be chosen removed in code: an exclusion rule, or left out of the question; lines that say what to do may help but guarantee nothing.
- Facts that must count at every decision pinned; `"memory": {"explain": true}` shows how far each line moved the answer.
- `compare` and `placebo` on while you develop, so every decision shows what the memory did.
