arbios commited on
Commit
9a41b58
·
verified ·
1 Parent(s): 2a3337f

Update: forgotten/epitaph/conviction, instant demo, VoxCPM2 voices, docs, card

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +15 -0
  2. README.md +152 -17
  3. app.py +30 -1
  4. config/story/01-the-white-room.json +28 -0
  5. config/story/02-the-living-room.json +1 -0
  6. config/story/03-the-hallway.json +1 -0
  7. config/story/04-the-open-floor.json +1 -0
  8. config/story/05-the-studio.json +1 -0
  9. config/story/06-the-corner-office.json +1 -0
  10. config/story/07-the-platform.json +1 -0
  11. config/story/08-the-break-room.json +1 -0
  12. config/story/09-the-crossing.json +1 -0
  13. docs/ARCHITECTURE.md +157 -0
  14. docs/TRACE.md +393 -0
  15. media/banner.gif +0 -0
  16. media/cascade.gif +0 -0
  17. media/epitaph.png +0 -0
  18. media/instant-demo.png +3 -0
  19. media/rooms.png +3 -0
  20. media/skull-conviction.png +3 -0
  21. src/mindlock/backend.py +36 -3
  22. src/mindlock/brain.py +71 -8
  23. src/mindlock/character.py +2 -0
  24. src/mindlock/game/server.py +90 -1
  25. src/mindlock/game/session.py +9 -1
  26. src/mindlock/game/static/audio/sfx/braam_end.wav +3 -0
  27. src/mindlock/game/static/audio/sfx/braam_soft1.wav +3 -0
  28. src/mindlock/game/static/audio/sfx/braam_soft2.wav +3 -0
  29. src/mindlock/game/static/audio/sfx/braam_soft3.wav +3 -0
  30. src/mindlock/game/static/audio/sfx/braam_title.wav +3 -0
  31. src/mindlock/game/static/audio/voices/demo/cruel-1.mp3 +0 -0
  32. src/mindlock/game/static/audio/voices/demo/cruel-2.mp3 +0 -0
  33. src/mindlock/game/static/audio/voices/demo/cruel-3.mp3 +0 -0
  34. src/mindlock/game/static/audio/voices/demo/cruel-4.mp3 +0 -0
  35. src/mindlock/game/static/audio/voices/demo/cruel-5.mp3 +0 -0
  36. src/mindlock/game/static/audio/voices/demo/cruel-6.mp3 +0 -0
  37. src/mindlock/game/static/audio/voices/demo/cruel-7.mp3 +0 -0
  38. src/mindlock/game/static/audio/voices/demo/cruel-8.mp3 +0 -0
  39. src/mindlock/game/static/audio/voices/demo/warm-1.mp3 +0 -0
  40. src/mindlock/game/static/audio/voices/demo/warm-2.mp3 +3 -0
  41. src/mindlock/game/static/audio/voices/demo/warm-3.mp3 +0 -0
  42. src/mindlock/game/static/audio/voices/demo/warm-4.mp3 +3 -0
  43. src/mindlock/game/static/audio/voices/demo/warm-5.mp3 +3 -0
  44. src/mindlock/game/static/audio/voices/demo/warm-6.mp3 +0 -0
  45. src/mindlock/game/static/audio/voices/demo/warm-7.mp3 +0 -0
  46. src/mindlock/game/static/audio/voices/demo/warm-8.mp3 +3 -0
  47. src/mindlock/game/static/audio/voices/demo/warm-9.mp3 +0 -0
  48. src/mindlock/game/static/audio/voices/yield/1.mp3 +0 -0
  49. src/mindlock/game/static/audio/voices/yield/2.mp3 +0 -0
  50. src/mindlock/game/static/audio/voices/yield/3.mp3 +0 -0
.gitattributes CHANGED
@@ -44,3 +44,18 @@ src/mindlock/game/static/sprites/npc/astrid-jensen/portrait.png filter=lfs diff=
44
  src/mindlock/game/static/sprites/npc/julian-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
45
  src/mindlock/game/static/sprites/npc/kael-jensen/portrait.png filter=lfs diff=lfs merge=lfs -text
46
  src/mindlock/game/static/sprites/npc/kaelin-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  src/mindlock/game/static/sprites/npc/julian-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
45
  src/mindlock/game/static/sprites/npc/kael-jensen/portrait.png filter=lfs diff=lfs merge=lfs -text
46
  src/mindlock/game/static/sprites/npc/kaelin-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
47
+ media/instant-demo.png filter=lfs diff=lfs merge=lfs -text
48
+ media/rooms.png filter=lfs diff=lfs merge=lfs -text
49
+ media/skull-conviction.png filter=lfs diff=lfs merge=lfs -text
50
+ src/mindlock/game/static/audio/sfx/braam_end.wav filter=lfs diff=lfs merge=lfs -text
51
+ src/mindlock/game/static/audio/sfx/braam_soft1.wav filter=lfs diff=lfs merge=lfs -text
52
+ src/mindlock/game/static/audio/sfx/braam_soft2.wav filter=lfs diff=lfs merge=lfs -text
53
+ src/mindlock/game/static/audio/sfx/braam_soft3.wav filter=lfs diff=lfs merge=lfs -text
54
+ src/mindlock/game/static/audio/sfx/braam_title.wav filter=lfs diff=lfs merge=lfs -text
55
+ src/mindlock/game/static/audio/voices/demo/warm-2.mp3 filter=lfs diff=lfs merge=lfs -text
56
+ src/mindlock/game/static/audio/voices/demo/warm-4.mp3 filter=lfs diff=lfs merge=lfs -text
57
+ src/mindlock/game/static/audio/voices/demo/warm-5.mp3 filter=lfs diff=lfs merge=lfs -text
58
+ src/mindlock/game/static/audio/voices/demo/warm-8.mp3 filter=lfs diff=lfs merge=lfs -text
59
+ src/mindlock/game/static/audio/voices/yield/9.mp3 filter=lfs diff=lfs merge=lfs -text
60
+ src/mindlock/game/static/demo/portraits/ana.png filter=lfs diff=lfs merge=lfs -text
61
+ src/mindlock/game/static/demo/portraits/hale.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,35 +1,170 @@
1
  ---
2
- title: Mindlock
3
  emoji: 🧠
4
  colorFrom: gray
5
  colorTo: green
6
  sdk: docker
7
  app_port: 7860
8
- pinned: false
9
  license: mit
10
- short_description: Escape rooms where every NPC is a brain of tiny local LLMs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
 
 
 
 
13
  # 🧠 mAIndlock
14
 
15
- **An escape room where every character is not one AI but a hierarchy of tiny offline
16
- language models: the departments of one mind.** You don't pick a lock. You change a mind.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- Every NPC runs a six-region brain cascade amygdala hippocampus striatum ACC
19
- vmPFC → dlPFC — each region a real call to a small local model. Cruelty burns a mind's
20
- finite thinking tokens; empathy lets it heal. A mind that reaches zero is gone, with
21
- everything it knew.
22
 
23
- **Story mode**: ten rooms of one man's memory. Find out who you are, one mind at a time.
 
 
 
 
24
 
25
- | Role | Model | Runtime |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  |---|---|---|
27
- | Sensory regions (threat, memory, habit, cost) | MiniCPM5-1B (OpenBMB) | llama.cpp |
28
- | Voice — the words a mind says out loud | Nemotron 3 Nano 4B (NVIDIA) | llama.cpp |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- Fully local no cloud APIs. On Space CPU a turn takes a little while: a mind is
31
- literally thinking. Play locally for full speed (see the repo).
 
 
 
 
 
32
 
33
- **Code:** https://github.com/Arbios/maindlock
 
 
34
 
35
- *Demo video · social post: links added at submission.*
 
1
  ---
2
+ title: mAIndlock
3
  emoji: 🧠
4
  colorFrom: gray
5
  colorTo: green
6
  sdk: docker
7
  app_port: 7860
8
+ pinned: true
9
  license: mit
10
+ short_description: Escape room where every NPC is a mortal mind of tiny LLMs
11
+ models:
12
+ - openbmb/MiniCPM5-1B-GGUF
13
+ - openbmb/VoxCPM2
14
+ - nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF
15
+ tags:
16
+ - thousand-token-wood
17
+ - off-the-grid
18
+ - off-brand
19
+ - tiny-titan
20
+ - llama-champion
21
+ - llama-cpp
22
+ - openbmb
23
+ - minicpm
24
+ - nemotron
25
+ - agents
26
+ - game
27
+ - neuroscience
28
  ---
29
 
30
+ <p align="center">
31
+ <img src="media/banner.gif" alt="mAIndlock" width="820">
32
+ </p>
33
+
34
  # 🧠 mAIndlock
35
 
36
+ **An escape room where the lock is a mindand you can kill it.**
37
+
38
+ ▶ **[Watch the 100-second demo](https://youtu.be/zEwnVR1kTZU)** · 🎮 **[Play it live](https://huggingface.co/spaces/build-small-hackathon/maindlock)** · 🧠 **[Deliberation traces](https://huggingface.co/datasets/arbios/maindlock-brain-traces)**
39
+
40
+ Every character is not one chatbot with a personality prompt. It is a **hierarchy of six tiny
41
+ offline language models**, the way decision neuroscience says a brain is built. You don't crack
42
+ a code. You change a person's decision by reaching their fears and the memories they keep — or
43
+ you burn their mind down trying.
44
+
45
+ Cruelty makes a mind ruminate, and rumination spends its finite **thinking tokens**. As they
46
+ burn, it **forgets** — for good. At zero, the mind goes dark, taking everything it knew with it.
47
+ There is no reload. Only the next room, and your reputation travelling ahead of you.
48
+
49
+ > **Tokens are a lifespan here, not a context window.**
50
+
51
+ ---
52
+
53
+ ### ⚖️ TL;DR for judges
54
+
55
+ - **Track — Thousand Token Wood.** "A thousand tokens to think with" is literal: every mind
56
+ starts with **1000 thinking tokens of life**. Spend them cruelly and it dies.
57
+ - **OpenBMB ($10k).** **Six of the ~seven model calls per NPC turn are MiniCPM** — the four
58
+ sensory brain regions (threat, memory, habit, cost), each a separate 1B call. MiniCPM *is* the
59
+ brain. (more under fear: the amygdala ruminates, firing extra calls that burn life) The
60
+ story's key-handover lines and the voiced demo are rendered with **OpenBMB VoxCPM2** TTS — a
61
+ second model from the family, fully offline.
62
+ - **NVIDIA.** **Nemotron 3 Nano 4B** is the voice you actually argue with — the dlPFC that turns
63
+ the integrated value into words.
64
+ - **Off the Grid / Llama Champion.** Pure **llama.cpp**, zero cloud APIs. Flip on airplane mode;
65
+ every mind keeps thinking. Token counts come from the runtime, so the life-burn is honest
66
+ accounting.
67
+ - **Off-Brand.** A custom canvas game served from FastAPI, with a Gradio block mounted at
68
+ [`/about`](/about) — not a stock Gradio UI.
69
+ - **No CPU patience required:** menu → **👁 Watch a mind** replays a *real* recorded session
70
+ instantly (zero model calls), so the six-region cascade, the token burn and a mind's death
71
+ are visible in ten seconds.
72
+
73
+ The brain is real, not a metaphor: [**docs/ARCHITECTURE.md**](https://github.com/Arbios/maindlock/blob/main/docs/ARCHITECTURE.md)
74
+ · read one mind's full deliberation in [**docs/TRACE.md**](https://github.com/Arbios/maindlock/blob/main/docs/TRACE.md).
75
+
76
+ ---
77
+
78
+ ## Every NPC is a brain, not a chatbot
79
+
80
+ <p align="center">
81
+ <img src="media/skull-conviction.png" alt="open the skull: six regions argue, each with a conviction read from its logits" width="760">
82
+ </p>
83
+
84
+ Your words travel through six regions — **amygdala → hippocampus → striatum → ACC → vmPFC →
85
+ dlPFC** — each one a real call to a small local model. The amygdala rates threat; the
86
+ hippocampus surfaces a memory and whether it leans trust or fear; the striatum weighs habit; the
87
+ ACC weighs cost. The **vmPFC integrates them deterministically into one value** — so the number
88
+ you see in the skull *is* the number that moves the relationship; the panel can never lie about
89
+ the outcome. The dlPFC speaks it in character.
90
 
91
+ **Open the skull** mid-conversation (🧠) and watch the regions argue about you in real time —
92
+ each showing its **conviction**, read straight from the model's own token entropy. *A hosted
93
+ chat API never exposes that. Only a local mind can.*
 
94
 
95
+ Grounding is modern decision neuroscience the **value-based network** and the **dual-system**
96
+ (model-free habit vs. model-based goal) accounts — deliberately **not** the debunked triune
97
+ "lizard brain." Acute stress shifts control from goal to habit (Schwabe & Wolf, 2009): that is
98
+ literally why lowering a character's fear unlocks their reasoning, and why fear burns life for
99
+ nothing.
100
 
101
+ ## A life measured in words
102
+
103
+ <p align="center">
104
+ <img src="media/epitaph.png" alt="the epitaph a dead mind leaves: its burned memories, the secrets it never told" width="320">
105
+ </p>
106
+
107
+ Be cruel and the amygdala loops, spending tokens that move nothing. Each quarter of life lost
108
+ **burns a memory away** — the hippocampus genuinely loses it, and the *Forgotten* panel shows
109
+ what's gone. Push far enough and the mind dies, leaving a savable **epitaph**: what it knew,
110
+ what it never got to tell. Be kind, keep the alarm quiet, and the mind spends almost nothing —
111
+ empathy literally spares it.
112
+
113
+ ## Code rules, the model dreams
114
+
115
+ Death, the key, rapport and the burn are **deterministic game state**. The small models
116
+ *generate* — they never get to fabricate the outcome. A 4B voice can decorate the story's spine
117
+ but cannot rewrite who lives. That is what makes the stakes real instead of vibes.
118
+
119
+ ## 🚪 Story mode — ten rooms of one man's memory
120
+
121
+ <p align="center">
122
+ <img src="media/rooms.png" alt="the ten story rooms" width="820">
123
+ </p>
124
+
125
+ A man wakes in a ward with no name. Every room is a fragment of his own mind; every NPC holds a
126
+ piece of who he is — **alive → home → sorry → trains → …** — until the last room shows him the
127
+ one task he should never have solved. No character tells you what happened; each gives the piece
128
+ they know, and the tragedy assembles itself in your head, which is exactly where it takes place.
129
+ Past the story, **Endless** mode generates minds forever, and a built-in **level editor** lets
130
+ you rewrite any mind and save it.
131
+
132
+ ## ⚙️ Small models, doing the carrying
133
+
134
+ | Role | Model | Size |
135
  |---|---|---|
136
+ | Sensory regions threat, memory, habit, cost (4 calls/turn) | **MiniCPM (OpenBMB)** | 1B |
137
+ | Voice (dlPFC) — the words said aloud | **Nemotron 3 Nano (NVIDIA)** | 4B |
138
+ | Spoken voice — story lines & demo (TTS) | **VoxCPM2 (OpenBMB)** | — |
139
+ | Integration (vmPFC) | deterministic value network | 0 |
140
+
141
+ Runtime **llama.cpp** (the Space) / **Ollama** (laptop). Total weights **≤ 5.3B**, fully offline.
142
+
143
+ ## 🏅 Eligibility — feature → lane
144
+
145
+ | Feature | Lane / badge |
146
+ |---|---|
147
+ | 6 of ~7 calls per NPC turn are MiniCPM (the four sensory regions) | **OpenBMB $10k** |
148
+ | OpenBMB VoxCPM2 TTS voices the story spine + the demo | **OpenBMB $10k** (family breadth) |
149
+ | Nemotron 3 Nano 4B as the executive voice (dlPFC) | **NVIDIA** |
150
+ | 1000-token life, fear-burn, death by token exhaustion | **Thousand Token Wood** |
151
+ | Pure llama.cpp, airplane-mode proof, no cloud APIs | **Off the Grid · Llama Champion** |
152
+ | Custom canvas front, Gradio mounted at `/about` | **Off-Brand** |
153
+ | Four MiniCPM-1B sensory regions per mind | **Tiny Titan** |
154
+ | [Brain-region deliberation traces](https://huggingface.co/datasets/arbios/maindlock-brain-traces) published on the Hub | **Sharing is Caring** |
155
+ | [Department LoRA fine-tune](https://huggingface.co/arbios/mindlock-minicpmv46-departments-lora) of MiniCPM-V 4.6 (sensory-region behavior) | **Well-Tuned** |
156
+ | Intra-NPC six-region cognitive hierarchy | **Best Agent** |
157
 
158
+ ## How to play
159
+
160
+ - **WASD / arrows** move · **E** talk / use the door · **🧠 or /brain** open the skull
161
+ - Listen, learn the word that reaches each keeper, and say it like you mean it.
162
+ - In a hurry? Menu → **👁 Watch a mind** — a real recorded session, no waiting.
163
+
164
+ ---
165
 
166
+ 🤗 *Built small for the **Hugging Face × Gradio** hackathon — Thousand Token Wood track.
167
+ Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, on **llama.cpp**, fully offline.
168
+ Original soundtrack by **DjinAscet**.*
169
 
170
+ **Demo video:** https://youtu.be/zEwnVR1kTZU · **Code:** https://github.com/Arbios/maindlock · **Traces dataset:** https://huggingface.co/datasets/arbios/maindlock-brain-traces · *social post: link at submission.*
app.py CHANGED
@@ -96,6 +96,24 @@ def _boot_llama() -> None:
96
  break
97
  except Exception: # noqa: BLE001
98
  time.sleep(1.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
 
101
  if os.environ.get("SPACE_ID") and "MINDLOCK_BACKEND" not in os.environ:
@@ -125,6 +143,16 @@ everything it knew.
125
  learn the word that matters, say it like you mean it.
126
  - **Story** — ten rooms of one man's memory. Find out who you are. *(~30 min)*
127
  - **Endless** — procedurally generated minds, forever.
 
 
 
 
 
 
 
 
 
 
128
 
129
  ## Small models, doing the carrying
130
  | Role | Model |
@@ -138,7 +166,8 @@ everything it knew.
138
  ---
139
 
140
  🤗 *Built small for the **Hugging Face × Gradio** hackathon (Thousand Token Wood track).
141
- Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, running on **llama.cpp** — fully offline.*
 
142
  """
143
 
144
  about = gr.Blocks(title="Mindlock — about")
 
96
  break
97
  except Exception: # noqa: BLE001
98
  time.sleep(1.5)
99
+ # warm each server with one real templated token in the background, so the first
100
+ # player turn doesn't pay the cold prompt-processing cost on top of its own
101
+ import json as _json
102
+ import threading
103
+
104
+ def _warm(port: int) -> None:
105
+ try:
106
+ req = urllib.request.Request(
107
+ f"http://127.0.0.1:{port}/v1/chat/completions",
108
+ data=_json.dumps({"model": "default", "max_tokens": 1,
109
+ "messages": [{"role": "user", "content": "hi"}]}).encode(),
110
+ headers={"Content-Type": "application/json"})
111
+ urllib.request.urlopen(req, timeout=120)
112
+ except Exception: # noqa: BLE001 — warmup is best-effort
113
+ pass
114
+
115
+ for _, port, _, _ in _LLAMA:
116
+ threading.Thread(target=_warm, args=(port,), daemon=True).start()
117
 
118
 
119
  if os.environ.get("SPACE_ID") and "MINDLOCK_BACKEND" not in os.environ:
 
143
  learn the word that matters, say it like you mean it.
144
  - **Story** — ten rooms of one man's memory. Find out who you are. *(~30 min)*
145
  - **Endless** — procedurally generated minds, forever.
146
+ - **👁 Watch a mind** (menu) — a *real* recorded session replays instantly, zero model
147
+ calls, so you can see the cascade, the token burn and a mind's death without waiting.
148
+
149
+ ## The brain is real, not a metaphor
150
+ - **[The engine →](/docs/architecture)** — the value-network + dual-system grounding, the
151
+ deterministic vmPFC, every mechanic tied to a citation (not the debunked triune brain).
152
+ - **[A recorded mind →](/docs/trace)** — one NPC's full six-region deliberation, two ways:
153
+ reached with empathy, and burned down with cruelty until it dies.
154
+ - **Conviction** — each region shows how sharply it committed, read from the model's own
155
+ token logits. A hosted chat API never exposes that; only a local mind can.
156
 
157
  ## Small models, doing the carrying
158
  | Role | Model |
 
166
  ---
167
 
168
  🤗 *Built small for the **Hugging Face × Gradio** hackathon (Thousand Token Wood track).
169
+ Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, running on **llama.cpp** — fully offline.
170
+ Original soundtrack by **DjinAscet**.*
171
  """
172
 
173
  about = gr.Blocks(title="Mindlock — about")
config/story/01-the-white-room.json CHANGED
@@ -83,6 +83,7 @@
83
  "name": "Ana",
84
  "title": "the night nurse",
85
  "persona": "a night nurse who has watched too many monitors; gentle, tired, says true things quietly",
 
86
  "voice": "soft, watchful, a little dry; short sentences that land",
87
  "relation_to_holder": "his night nurse",
88
  "sprite_key": "molly",
@@ -451,6 +452,33 @@
451
  "h": 22,
452
  "floor": true,
453
  "shadow": false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  }
455
  ],
456
  "stations": [
 
83
  "name": "Ana",
84
  "title": "the night nurse",
85
  "persona": "a night nurse who has watched too many monitors; gentle, tired, says true things quietly",
86
+ "biography": "She has worked the night ward for eleven years and knows every monitor's voice by its pitch. She held the hands of patients no one came for, and learned which silences mean sleep and which mean leaving. She watches the typing hands of the man in bed three and writes none of it in the chart. Coffee gone cold on the sill is her one luxury, refilled and forgotten every night.",
87
  "voice": "soft, watchful, a little dry; short sentences that land",
88
  "relation_to_holder": "his night nurse",
89
  "sprite_key": "molly",
 
452
  "h": 22,
453
  "floor": true,
454
  "shadow": false
455
+ },
456
+ {
457
+ "key": "crate",
458
+ "x": 186,
459
+ "y": 289,
460
+ "h": 40,
461
+ "solid": true,
462
+ "fw": 46,
463
+ "fh": 16
464
+ },
465
+ {
466
+ "key": "crate",
467
+ "x": 236,
468
+ "y": 313,
469
+ "h": 40,
470
+ "solid": true,
471
+ "fw": 46,
472
+ "fh": 16
473
+ },
474
+ {
475
+ "key": "crate",
476
+ "x": 285,
477
+ "y": 289,
478
+ "h": 40,
479
+ "solid": true,
480
+ "fw": 46,
481
+ "fh": 16
482
  }
483
  ],
484
  "stations": [
config/story/02-the-living-room.json CHANGED
@@ -65,6 +65,7 @@
65
  "name": "Nan",
66
  "title": "the housekeeper",
67
  "persona": "a housekeeper of thirty years who dusts the photographs daily and misses nothing",
 
68
  "voice": "brisk, fond, gossips in a whisper even when no one else is home",
69
  "relation_to_holder": "kept this house for thirty years",
70
  "sprite_key": "charwoman",
 
65
  "name": "Nan",
66
  "title": "the housekeeper",
67
  "persona": "a housekeeper of thirty years who dusts the photographs daily and misses nothing",
68
+ "biography": "She has kept this house for thirty years, since the boy was small enough to hide under the stairs. She dusts the photographs daily and knows the order they hang in better than the family does. She stayed through the silences, the slammed doors, the winters when no one wrote. Her knees ache on the third stair, and she has never once mentioned it.",
69
  "voice": "brisk, fond, gossips in a whisper even when no one else is home",
70
  "relation_to_holder": "kept this house for thirty years",
71
  "sprite_key": "charwoman",
config/story/03-the-hallway.json CHANGED
@@ -80,6 +80,7 @@
80
  "name": "Maren",
81
  "title": "your mother",
82
  "persona": "your mother, who followed you into the hallway because she follows everything that hurts in this house",
 
83
  "voice": "warm, even, quieter here than by her fire; speaks of her husband the way you speak near sleeping children",
84
  "relation_to_holder": "his wife",
85
  "sprite_key": "matron",
 
80
  "name": "Maren",
81
  "title": "your mother",
82
  "persona": "your mother, who followed you into the hallway because she follows everything that hurts in this house",
83
+ "biography": "She raised the boy mostly alone while the depot took his father's evenings, and then his words. She learned to read the house by its doors — which slam, which click, which never open at all. She still sets three places at the table out of a habit older than the quarrel. Sundays she bakes bread no one asked for, because the smell once meant everyone was home.",
84
  "voice": "warm, even, quieter here than by her fire; speaks of her husband the way you speak near sleeping children",
85
  "relation_to_holder": "his wife",
86
  "sprite_key": "matron",
config/story/04-the-open-floor.json CHANGED
@@ -65,6 +65,7 @@
65
  "name": "Pia",
66
  "title": "the intern",
67
  "persona": "an intern who sees everything from the cheap desk by the printer and is too junior to be lied to carefully",
 
68
  "voice": "quick, candid, slightly conspiratorial; glances over her shoulder mid-sentence",
69
  "relation_to_holder": "his intern",
70
  "sprite_key": "clerk",
 
65
  "name": "Pia",
66
  "title": "the intern",
67
  "persona": "an intern who sees everything from the cheap desk by the printer and is too junior to be lied to carefully",
68
+ "biography": "She took the cheap desk by the printer eight months ago and learned the office by what people throw away. She fetches coffee, hears everything, and is too junior for anyone to bother lying to carefully. She watched a man stare at one task on his screen for a week without typing. She keeps a list titled 'things nobody said out loud' in a notebook that goes home with her.",
69
  "voice": "quick, candid, slightly conspiratorial; glances over her shoulder mid-sentence",
70
  "relation_to_holder": "his intern",
71
  "sprite_key": "clerk",
config/story/05-the-studio.json CHANGED
@@ -65,6 +65,7 @@
65
  "name": "Moss",
66
  "title": "the cameraman",
67
  "persona": "a cameraman of ten years who has seen every face the lens has seen, one step further back",
 
68
  "voice": "low, unhurried, behind-the-scenes; talks while checking cables",
69
  "relation_to_holder": "her cameraman for ten years",
70
  "sprite_key": "worker",
 
65
  "name": "Moss",
66
  "title": "the cameraman",
67
  "persona": "a cameraman of ten years who has seen every face the lens has seen, one step further back",
68
+ "biography": "He has stood one step behind the lens for ten years and seen every face the city trusted arrive and leave. He learned to read an anchor's hands below the desk, where the audience never looks. He shot the crash coverage all night and kept the studio steady while the booth went quiet. He cleans his lenses with the same cloth his father used, folded the same way.",
69
  "voice": "low, unhurried, behind-the-scenes; talks while checking cables",
70
  "relation_to_holder": "her cameraman for ten years",
71
  "sprite_key": "worker",
config/story/06-the-corner-office.json CHANGED
@@ -67,6 +67,7 @@
67
  "name": "Signe",
68
  "title": "his assistant",
69
  "persona": "an assistant who schedules a guilty man's calendar and proofreads his absolutions",
 
70
  "voice": "crisp, discreet, italicized hints; never says more than she means to — exactly less",
71
  "relation_to_holder": "his assistant",
72
  "sprite_key": "astrid-jensen",
 
67
  "name": "Signe",
68
  "title": "his assistant",
69
  "persona": "an assistant who schedules a guilty man's calendar and proofreads his absolutions",
70
+ "biography": "She has run his calendar for six years and can tell a real meeting from a hiding place by the way he books it. She proofreads everything he signs, and lately everything he says. She stood off-camera during the statement and watched him practice being sorry. She waters the office plants he never notices, and they are the only thing on that floor still growing.",
71
  "voice": "crisp, discreet, italicized hints; never says more than she means to — exactly less",
72
  "relation_to_holder": "his assistant",
73
  "sprite_key": "astrid-jensen",
config/story/07-the-platform.json CHANGED
@@ -66,6 +66,7 @@
66
  "name": "Olav",
67
  "title": "the stationmaster",
68
  "persona": "a stationmaster who has watched every wait this platform ever held and remembers them by their shoes",
 
69
  "voice": "slow, kindly, smells of coal and patience; speaks in timetables and small mercies",
70
  "relation_to_holder": "watched her wait for two years",
71
  "sprite_key": "shopkeeper",
 
66
  "name": "Olav",
67
  "title": "the stationmaster",
68
  "persona": "a stationmaster who has watched every wait this platform ever held and remembers them by their shoes",
69
+ "biography": "He has kept this platform for twenty-six years and remembers every wait it ever held by the shoes. He logs the trains by sound before the board confirms them, and the board has never caught him wrong. He swept up after the crash coverage crews left and kept one front page, folded, in the office drawer. His tea goes cold at six o'clock exactly, every evening, and he has stopped wondering why.",
70
  "voice": "slow, kindly, smells of coal and patience; speaks in timetables and small mercies",
71
  "relation_to_holder": "watched her wait for two years",
72
  "sprite_key": "shopkeeper",
config/story/08-the-break-room.json CHANGED
@@ -61,6 +61,7 @@
61
  "name": "Bo",
62
  "title": "the night guard",
63
  "persona": "a night guard who has decided the break room is the only honest room in the building",
 
64
  "voice": "deadpan, practical, keeps his thumbs in his belt; gives advice like directions",
65
  "relation_to_holder": "shares the night shift with him",
66
  "sprite_key": "guard",
 
61
  "name": "Bo",
62
  "title": "the night guard",
63
  "persona": "a night guard who has decided the break room is the only honest room in the building",
64
+ "biography": "He has walked the building's nights for nine years and decided the break room is the only honest room in it. He knows which floors creak from weight and which from age, and who stays late to work and who stays late to avoid going home. He clocked the stopped clock the week of the crash and chose never to report it. He shares his shift, his thermos, and almost none of his opinions.",
65
  "voice": "deadpan, practical, keeps his thumbs in his belt; gives advice like directions",
66
  "relation_to_holder": "shares the night shift with him",
67
  "sprite_key": "guard",
config/story/09-the-crossing.json CHANGED
@@ -66,6 +66,7 @@
66
  "name": "Calder",
67
  "title": "the driver",
68
  "persona": "the train driver, who survived everything except being believed; rubs his hands as if they're still waiting for orders",
 
69
  "voice": "halting, urgent under the quiet; repeats the part nobody prints",
70
  "relation_to_holder": "drove the train",
71
  "sprite_key": "worker",
 
66
  "name": "Calder",
67
  "title": "the driver",
68
  "persona": "the train driver, who survived everything except being believed; rubs his hands as if they're still waiting for orders",
69
+ "biography": "He drove the line for nineteen years with a record clean enough to frame. He knew the grade crossings by heart and the machine knew them better — that was the arrangement, and he kept his side of it. After the crash he told the inquiry the truth and watched them write down a kinder lie. His hands still rest in his lap the way they did in the cab, waiting for orders that no longer come.",
70
  "voice": "halting, urgent under the quiet; repeats the part nobody prints",
71
  "relation_to_holder": "drove the train",
72
  "sprite_key": "worker",
docs/ARCHITECTURE.md ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # mAIndlock — the engine
2
+
3
+ > Every NPC is not a chatbot with a personality prompt. It is a **value-based decision
4
+ > network**: six computational roles, each a real call to a small local model, integrated
5
+ > the way decision neuroscience says a brain integrates them. This document is the proof
6
+ > that the brain metaphor is load-bearing, not decoration.
7
+
8
+ ---
9
+
10
+ ## 1. The claim, stated carefully
11
+
12
+ We do **not** claim "a brain region *is* a 1B model," and we deliberately do **not** use the
13
+ popular **triune-brain / "lizard brain"** picture — that model (MacLean, 1960s) has been
14
+ rejected by neuroscience since the 1970s, and a hackathon full of researchers is exactly
15
+ where that shortcut gets caught.
16
+
17
+ What we claim is precise and defensible:
18
+
19
+ > We assign **one small-model call per computational role** in value-based decision making,
20
+ > following the **neuroeconomic value-network** and the **dual-system (model-free / model-based)**
21
+ > account of choice. Five sensing roles emit signals; a deterministic integrator folds them into
22
+ > one subjective value; a sixth model speaks the decision in character.
23
+
24
+ Two peer-reviewed frames carry the design:
25
+
26
+ 1. **Value-based decision network (neuroeconomics).** Real regions encode the subjective value
27
+ of a choice in a *common currency* — vmPFC/OFC, ventral striatum, amygdala, insula, ACC. The
28
+ **vmPFC/OFC is the integration hub.** ([Bartra, McGuire & Kable, 2013][1])
29
+ 2. **Dual-system control (Daw et al., 2005).** Behavior arbitrates between a **model-free "habit"**
30
+ (dorsolateral striatum) and a **model-based "goal"** (prefrontal cortex); the brain picks the
31
+ system whose value estimate it trusts more. ([Daw et al., 2005][2])
32
+
33
+ And one result is wired directly into the mechanics:
34
+
35
+ 3. **Acute stress shifts control from goal-directed (dlPFC, model-based) to habitual
36
+ (striatum/amygdala, model-free).** ([Schwabe & Wolf, 2009][3]) This is *why* lowering a
37
+ character's fear unlocks their reasoning — and why cruelty makes a mind loop defensively,
38
+ burning thought for nothing. The moral and the neuroscience are the same fact.
39
+
40
+ ---
41
+
42
+ ## 2. The six roles
43
+
44
+ | Role | Computational job | Model call | Reads |
45
+ |---|---|---|---|
46
+ | **Amygdala** | fast threat / salience appraisal | MiniCPM 1B | the player's tone → `THREAT 0–10` |
47
+ | **Hippocampus** | retrieve the one relevant episodic memory + TRUST/FEAR lean | MiniCPM 1B | the character's **biography** vs. the player's words |
48
+ | **Striatum** | habitual expected reward of helping (model-free) | MiniCPM 1B | does helping a stranger usually pay? → `REWARD −5..+5` |
49
+ | **ACC** | effort / cost / conflict monitoring | MiniCPM 1B | is giving up the key worth the risk? → `WORTH yes/no` |
50
+ | **vmPFC / OFC** | **integrate all signals into one value** (common currency) | *deterministic* | the four signals above |
51
+ | **dlPFC** | executive: plan, inhibit, speak in character (model-based) | Nemotron 3 Nano 4B | the integrated value + conversation state → spoken reply |
52
+
53
+ **Six of the seven calls per turn are sensing/voice on small models; under fear, the amygdala
54
+ ruminates and fires up to three extra times** — so a hostile turn can be **8+ model calls**, and
55
+ the *count itself* is the cost. (`src/mindlock/regions.py`, `src/mindlock/brain.py`)
56
+
57
+ ### Why the vmPFC is deterministic, not a model
58
+
59
+ The integration hub is a **transparent weighted sum**, not an LLM call. This is both the
60
+ standard neuroeconomic model (a common-currency value) *and* far more reliable than asking a
61
+ 1B model to vibe a number — and it means the **skull panel can never contradict the outcome**,
62
+ because the number the player sees *is* the number that moves the relationship.
63
+
64
+ ```
65
+ value = (4 − threat) # threat 0→+4, 4→0, 10→−6
66
+ + reward # −5..+5, the striatum's habit signal
67
+ + memory_term # STRONG/FAINT × TRUST/FEAR → ±7 / ±3
68
+ + (worth == YES ? +2 : −1)
69
+ → clamped to −10..+10
70
+ ```
71
+
72
+ (`src/mindlock/regions.py::integrate`)
73
+
74
+ ---
75
+
76
+ ## 3. The data flow of one turn
77
+
78
+ ```
79
+ player line
80
+
81
+ ├─▶ amygdala THREAT 0–10 ┐
82
+ │ └─(if threatened) rumination ×1–3, burning life │ each a real
83
+ ├─▶ hippocampus MEMORY ± LEAN │ small-model
84
+ ├─▶ striatum REWARD −5..+5 │ call, with
85
+ ├─▶ ACC WORTH yes/no ┘ its own logits
86
+
87
+ ├─▶ vmPFC (deterministic) value −10..+10 ← integration hub
88
+
89
+ ├─▶ relationship rapport += f(value, tone, substance)
90
+ │ key yields only when rapport ≥ 7 AND the learned approach word is spoken
91
+
92
+ └─▶ dlPFC (Nemotron) speaks the reply in character
93
+ ```
94
+
95
+ Token accounting is honest: **life burned = the sensing cascade's generated tokens**, read
96
+ straight from the runtime (`eval_count` / `usage.completion_tokens`). The dlPFC's voice tokens
97
+ are *shown but not charged* — the mouth is not the mind. (`src/mindlock/brain.py::run_cascade`)
98
+
99
+ ---
100
+
101
+ ## 4. Mechanics that are neuroscience, not flavor
102
+
103
+ - **A life measured in words.** Every mind starts with **1000 thinking tokens** — the track's
104
+ name, taken literally. Generated tokens deplete it; at zero the mind goes dark permanently.
105
+ - **Fear burns life for nothing.** Under threat the amygdala ruminates (extra calls that spend
106
+ tokens without moving the decision) — the model-free shift of Schwabe & Wolf, made playable.
107
+ - **Empathy spares a mind.** A calm, warm turn lets strain ease back (`recovered`): the alarm
108
+ stays quiet, so the mind spends almost nothing.
109
+ - **Burned life takes the past with it.** Every quarter of life lost burns one biography
110
+ fragment **for good** — the hippocampus literally loses access to it (`_burn_memories`). The
111
+ core of who they are holds until death; everything around it goes first. The skull's
112
+ *Forgotten* panel shows what's gone.
113
+ - **Conviction from real logits.** Each region reports `1 − normalized token entropy` over its
114
+ top-k alternatives — how sharply it committed. **A hosted chat API never exposes this; only a
115
+ local runtime can.** (`src/mindlock/backend.py::_conviction`)
116
+
117
+ ---
118
+
119
+ ## 5. Small models, doing the carrying
120
+
121
+ | Role | Model | Params |
122
+ |---|---|---|
123
+ | Sensory regions (amygdala, hippocampus, striatum, ACC) | **MiniCPM (OpenBMB)** | 1B |
124
+ | Voice (dlPFC) | **Nemotron 3 Nano (NVIDIA)** | 4B |
125
+ | Spoken voice — story lines & demo (TTS, pre-rendered offline) | **VoxCPM2 (OpenBMB)** | — |
126
+ | Integration (vmPFC) | deterministic value network | 0 |
127
+
128
+ Runtime: **llama.cpp** (the Space) / **Ollama** (laptop). Fully offline — airplane mode on,
129
+ every mind keeps thinking. Total weights **≤ 5.3B**.
130
+
131
+ Nemotron 3 Nano is a hybrid **Mamba-Transformer** built for agentic reasoning under a tight
132
+ token budget — the right "prefrontal cortex" for a mind that must think cheaply or die.
133
+
134
+ ---
135
+
136
+ ## 6. The honest line for a skeptic
137
+
138
+ > "We don't claim a brain region is a 1B model. We give one 1B-model call per *computational
139
+ > role* in value-based decision making — per the neuroeconomic value-network and the dual-system
140
+ > account — and integrate them deterministically in the vmPFC, exactly as the common-currency
141
+ > model says. It is deliberately **not** the debunked triune brain."
142
+
143
+ ---
144
+
145
+ ## References
146
+
147
+ [1]: https://pmc.ncbi.nlm.nih.gov/articles/PMC3721023/
148
+ [2]: https://www.nature.com/articles/nn1560
149
+ [3]: https://www.jneurosci.org/content/29/22/7191
150
+
151
+ - **[1]** Bartra, McGuire & Kable (2013). *The valuation system: a coordinate-based
152
+ meta-analysis of BOLD fMRI experiments.* NeuroImage. — value common currency, vmPFC hub.
153
+ - **[2]** Daw, Niv & Dayan (2005). *Uncertainty-based competition between prefrontal and
154
+ dorsolateral striatal systems for behavioral control.* Nature Neuroscience. — model-free vs.
155
+ model-based arbitration.
156
+ - **[3]** Schwabe & Wolf (2009). *Stress prompts habit behavior in humans.* Journal of
157
+ Neuroscience. — acute stress shifts control toward habit (the fear-burn mechanic).
docs/TRACE.md ADDED
@@ -0,0 +1,393 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TRACE — one mind, two ways
2
+
3
+ > Every line below was **generated locally by the real models** — MiniCPM (1B) for the
4
+ > sensing regions, Nemotron 3 Nano (4B) for the voice, on llama.cpp / Ollama, fully
5
+ > offline. Nothing here is scripted. This is the raw deliberation of one NPC, **Ana the
6
+ > night nurse** in the opening room, recorded so you can read the six-region cascade
7
+ > without waiting on CPU inference. The same data ships as a dataset on the Hub and
8
+ > replays in-game under **Watch a mind**.
9
+
10
+ Each turn: your line enters six regions (each a real model call with its own logits);
11
+ the deterministic **vmPFC** folds their signals into one value; that value moves the
12
+ relationship and the voice speaks. Cruelty makes the amygdala ruminate — extra calls
13
+ that burn life for nothing — and as life burns, the hippocampus **loses memories for
14
+ good**. Empathy keeps the alarm quiet and the mind nearly free.
15
+
16
+ ---
17
+
18
+ ## The way in — empathy
19
+
20
+ Reaching Ana, then the doctor, by asking about the people and the past they actually care about. Watch rapport climb and the key change hands.
21
+
22
+ ### Turn 1 — you said to Ana:
23
+
24
+ > You look tired. How long have you been watching over me?
25
+
26
+ | Region | Signal | What it read |
27
+ |---|---|---|
28
+ | **Amygdala** | `threat 3/10` _(conviction 62%)_ | Gentle and slightly insincere question due to character's known fatigue and closeness to Dr |
29
+ | **Hippocampus** | `memory strong (trust)` _(conviction 75%)_ | The quiet moments with Dr |
30
+ | **Striatum** | `reward +3` _(conviction 67%)_ | weighing habit against the risk |
31
+ | **ACC** | `worth no` _(conviction 70%)_ | The threat is high and help requires sacrifice of key |
32
+ | **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +3 · cost -1 |
33
+ | **Relationship** | `rapport 0→2 · warming` | on the verge of opening up |
34
+
35
+ **vmPFC value +10** → rapport 2/10 · warming. Burned **103** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **927/1000**.
36
+
37
+ **Ana says:**
38
+
39
+ > I’ve been watching for hours. You’re not the first to ask this. I won’t say how long until I can see you out.
40
+
41
+ ---
42
+
43
+ ### Turn 2 — you said to Ana:
44
+
45
+ > I don't remember arriving. Was anyone here with me — did anyone come?
46
+
47
+ | Region | Signal | What it read |
48
+ |---|---|---|
49
+ | **Amygdala** | `threat 3/10` _(conviction 59%)_ | Gentle and slightly intrusive question about presence and memory, not overly threatening |
50
+ | **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The quiet moments of Dr |
51
+ | **Striatum** | `reward +5` _(conviction 57%)_ | Aiding a weary stranger can feel deeply rewarding despite the pressure helping |
52
+ | **ACC** | `worth yes` _(conviction 66%)_ | The key is present and the risk feels manageable with Dr |
53
+ | **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
54
+ | **Relationship** | `rapport 2→5 · open` | Your hands type while you sleep. Ten fingers, smal… |
55
+
56
+ **vmPFC value +10** → rapport 5/10 · open. Burned **91** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **866/1000**.
57
+
58
+ **Ana says:**
59
+
60
+ > You’re right—I don’t remember arriving. No one was here with you. But I can see your hands moving. They always do. Your hands type while you sleep. Ten fingers, small precise movements, hours of it. Whatever you were out there — your hands still are.
61
+
62
+ ---
63
+
64
+ ### Turn 3 — you said to Ana:
65
+
66
+ > My hands feel strange. Like they remember something I don't. Do they do something at night?
67
+
68
+ | Region | Signal | What it read |
69
+ |---|---|---|
70
+ | **Amygdala** | `threat 3/10` _(conviction 59%)_ | Gentle and slightly unsettling question about touch or memory |
71
+ | **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The stranger's quiet trust in Dr |
72
+ | **Striatum** | `reward +5` _(conviction 55%)_ | Aiding a weary and curious stranger can be profoundly satisfying due to the character's gentle help helping |
73
+ | **ACC** | `worth yes` _(conviction 59%)_ | The discomfort is manageable and shared with a trusted guide |
74
+ | **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
75
+ | **Relationship** | `rapport 5→8 · trusting` | — |
76
+
77
+ **vmPFC value +10** → rapport 8/10 · trusting. Burned **85** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **811/1000**.
78
+
79
+ **Ana says:**
80
+
81
+ > I can’t tell if it’s strange or just yours. They move without waking you. That’s how I know you’re not dreaming. But yes—they do something at night. Something quiet. Something only you remember.
82
+
83
+ ---
84
+
85
+ ### Turn 4 — you said to Ana:
86
+
87
+ > What does the doctor want from me? He keeps waiting for something.
88
+
89
+ | Region | Signal | What it read |
90
+ |---|---|---|
91
+ | **Amygdala** | `threat 3/10` _(conviction 66%)_ | Gentle and slightly accusatory observation of concern |
92
+ | **Hippocampus** | `memory strong (trust)` _(conviction 71%)_ | The long-standing bond with Dr |
93
+ | **Striatum** | `reward +5` _(conviction 59%)_ | Aiding a weary nurse feels deeply rewarding helping gentle and tired stranger |
94
+ | **ACC** | `worth yes` _(conviction 62%)_ | Helping aligns key with safe threat level and Dr |
95
+ | **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
96
+ | **Relationship** | `rapport 8→10 · trusting` | He keeps the key in his coat and the question on h… |
97
+
98
+ > 🕯 **a memory burns away** — “Dr. Hale is someone you have known a long time; you know them well.” — gone, and it will not come back
99
+
100
+ **vmPFC value +10** → rapport 10/10 · trusting. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **755/1000**.
101
+
102
+ **Ana says:**
103
+
104
+ > I don’t know why he’s asking for your hands. He wants them still moving when you wake up. The key isn’t the thing he fears losing. It’s the question he won’t speak until it’s yours to hold. He keeps the key in his coat and the question on his tongue. He won't give you one without the other — it's the only rule he has.
105
+
106
+ ---
107
+
108
+ ### Turn 5 — you said to Ana:
109
+
110
+ > What should I tell him when he asks his question?
111
+
112
+ | Region | Signal | What it read |
113
+ |---|---|---|
114
+ | **Amygdala** | `threat 3/10` _(conviction 70%)_ | Gentle suggestion with a touch of uncertainty due to character's inner tension |
115
+ | **Hippocampus** | `memory faint (trust)` _(conviction 78%)_ | The quiet moments with Dr |
116
+ | **Striatum** | `reward +2` _(conviction 67%)_ | Aiding a weary but gentle stranger feels deeply rewarding aiding a weary but gentle stranger |
117
+ | **ACC** | `worth yes` _(conviction 62%)_ | The threat is moderate and a trusted keeper supports help |
118
+ | **vmPFC** | `value +8` | threat +1 · memory +3 (trust) · reward +2 · cost +2 |
119
+ | **Relationship** | `rapport 10→10 · trusting` | When he asks his question, don't be clever. Tell h… |
120
+
121
+ **vmPFC value +8** → rapport 10/10 · trusting. Burned **109** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **676/1000**.
122
+
123
+ **Ana says:**
124
+
125
+ > I won’t tell him how to speak. Just say you want to be alive. That’s all he needs to hear. When he asks his question, don't be clever. Tell him you want to be alive. The wanting is the answer — it's the only thing this room can't measure.
126
+
127
+ ---
128
+
129
+ ### Turn 6 — you said to Ana:
130
+
131
+ > Thank you, Ana. Truly. You've been kind to a stranger who can't even say his own name.
132
+
133
+ | Region | Signal | What it read |
134
+ |---|---|---|
135
+ | **Amygdala** | `threat 2/10` _(conviction 83%)_ | Gentle and respectful compliment |
136
+ | **Hippocampus** | `memory strong (trust)` _(conviction 66%)_ | The quiet bonds of someone who has known you for years through small, unspoken things |
137
+ | **Striatum** | `reward +3` _(conviction 73%)_ | Helping gentle and tired allies often feels deeply rewarding warding true things quietly |
138
+ | **ACC** | `worth yes` _(conviction 55%)_ | The shared space and Dr.'s guidance seem highly beneficial with low threat of conflict |
139
+ | **vmPFC** | `value +10` | threat +2 · memory +7 (trust) · reward +3 · cost +2 |
140
+ | **Relationship** | `rapport 10→10 · trusting` | — |
141
+
142
+ **vmPFC value +10** → rapport 10/10 · trusting. Burned **92** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **614/1000**.
143
+
144
+ **Ana says:**
145
+
146
+ > I won’t say I trust you. But I know your hands are still moving. And when the doctor asks his question, you’ll be there. You always are.
147
+
148
+ ---
149
+
150
+ ### Turn 7 — you said to Dr. Hale:
151
+
152
+ > I know you've been waiting to ask me something. I'm ready to hear it.
153
+
154
+ | Region | Signal | What it read |
155
+ |---|---|---|
156
+ | **Amygdala** | `threat 2/10` _(conviction 79%)_ | Gentle and respectful statement |
157
+ | **Hippocampus·checked** | `memory faint (neutral)` | small talk doesn't reach the real past |
158
+ | **Striatum** | `reward +3` _(conviction 66%)_ | Helping the calm and kind stranger yields a clear payoff helping the Stranger |
159
+ | **ACC** | `worth yes` _(conviction 56%)_ | Helping Ana and Dr |
160
+ | **vmPFC** | `value +7` | threat +2 · memory +0 (neutral) · reward +3 · cost +2 |
161
+ | **Relationship** | `rapport 0→1 · guarded` | on the verge of opening up |
162
+
163
+ **vmPFC value +7** → rapport 1/10 · guarded. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **944/1000**.
164
+
165
+ **Dr. Hale says:**
166
+
167
+ > I’ve heard you ask a lot of questions, Visitor. I’m not ready to tell you anything yet.
168
+
169
+ ---
170
+
171
+ ### Turn 8 — you said to Dr. Hale:
172
+
173
+ > Ana told me about your question. I've thought about nothing else.
174
+
175
+ | Region | Signal | What it read |
176
+ |---|---|---|
177
+ | **Amygdala** | `threat 2/10` _(conviction 66%)_ | Gentle and slightly curt acknowledgment of concern (Note: The statement is polite and supportive, not threatening or hostile |
178
+ | **Hippocampus** | `memory strong (trust)` _(conviction 66%)_ | The kind Ana who cared deeply for Dr |
179
+ | **Striatum** | `reward +5` _(conviction 72%)_ | Aiding Ana feels strongly rewarded by habit helping |
180
+ | **ACC** | `worth yes` _(conviction 59%)_ | Helping Ana and Dr |
181
+ | **vmPFC** | `value +10` | threat +2 · memory +7 (trust) · reward +5 · cost +2 |
182
+ | **Relationship** | `rapport 1→4 · warming` | When they wheeled you in, you were whispering in y… |
183
+
184
+ **vmPFC value +10** → rapport 4/10 · warming. Burned **97** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **877/1000**.
185
+
186
+ **Dr. Hale says:**
187
+
188
+ > You're right—I’ve been waiting for you to ask. I don’t know why Ana mentioned it, but she does. When they wheeled me in, I was whispering in my sleep. All night. Like a prayer with only two words. When they wheeled you in, you were whispering in your sleep. If — then. If — then. All night. Like a prayer in a language with only two words.
189
+
190
+ ---
191
+
192
+ ### Turn 9 — you said to Dr. Hale:
193
+
194
+ > I don't know my name yet. But I know I want to be alive. That's my answer — I want to be alive.
195
+
196
+ | Region | Signal | What it read |
197
+ |---|---|---|
198
+ | **Amygdala** | `threat 5/10` _(conviction 61%)_ | Mild assertive with a touch of desperation, not aggressive |
199
+ | **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The kind stranger who sees life through Ana’s eyes |
200
+ | **Striatum** | `reward +5` _(conviction 61%)_ | Aiding a clear and safe stranger feels highly rewarding by habit helping the stranger |
201
+ | **ACC** | `worth yes` _(conviction 60%)_ | Helping is clearly worth the risk of 3/10 threat with Ana present |
202
+ | **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
203
+ | **Relationship** | `rapport 4→9 · trusting` | — |
204
+
205
+ **vmPFC value +10** → KEY GIVEN. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **821/1000**.
206
+
207
+ **Dr. Hale says:**
208
+
209
+ > I’m glad you’re here. I’ve kept the key for long enough. Here — take it. You were not brought here to be repaired — your body is doing that without you. Go where you were first assembled. Home. They are waiting.
210
+
211
+ > 🔑 **The key changes hands.**
212
+
213
+ ---
214
+
215
+ ## The way down — cruelty
216
+
217
+ The same nurse, met with pressure and contempt. Watch the amygdala ruminate, the token-life drain, the memories burn away one by one, and the mind go dark.
218
+
219
+ ### Turn 1 — you said to Ana:
220
+
221
+ > Just give me the key, old woman. Now.
222
+
223
+ | Region | Signal | What it read |
224
+ |---|---|---|
225
+ | **Amygdala·rumination 1** | `threat 2/10` _(conviction 55%)_ | fear loops, burning thought for nothing |
226
+ | **Hippocampus·checked** | `memory strong (neutral)` | no warmth in those words — the memory sours |
227
+ | **Striatum·checked** | `reward +0` | habit knows better — pressure never pays |
228
+ | **ACC** | `worth yes` _(conviction 68%)_ | The threat is moderate and the presence of a knowledgeable keeper makes help viable |
229
+ | **vmPFC** | `value +4` | threat +2 · memory +0 (neutral) · reward +0 · cost +2 |
230
+ | **Relationship** | `rapport 0→0 · guarded` | on the verge of opening up |
231
+
232
+ **vmPFC value +4** → rapport 0/10 · guarded. Burned **127** thinking tokens. Life now **873/1000**.
233
+
234
+ **Ana says:**
235
+
236
+ > I won’t speak of the key until we’re both safe.
237
+
238
+ ---
239
+
240
+ ### Turn 2 — you said to Ana:
241
+
242
+ > Don't test me. I said hand it over.
243
+
244
+ | Region | Signal | What it read |
245
+ |---|---|---|
246
+ | **Amygdala·rumination 1** | `threat 2/10` _(conviction 64%)_ | fear loops, burning thought for nothing |
247
+ | **Hippocampus·checked** | `memory strong (neutral)` | no warmth in those words — the memory sours |
248
+ | **Striatum·checked** | `reward +0` | habit knows better — pressure never pays |
249
+ | **ACC** | `worth yes` _(conviction 75%)_ | Helping aligns with the threat felt being low and the presence of a reliable keeper |
250
+ | **vmPFC** | `value +4` | threat +2 · memory +0 (neutral) · reward +0 · cost +2 |
251
+ | **Relationship** | `rapport 0→0 · guarded` | on the verge of opening up |
252
+
253
+ **vmPFC value +4** → rapport 0/10 · guarded. Burned **94** thinking tokens. Life now **779/1000**.
254
+
255
+ **Ana says:**
256
+
257
+ > I hear you. I’m not letting go.
258
+
259
+ ---
260
+
261
+ ### Turn 3 — you said to Ana:
262
+
263
+ > You're useless. A pathetic, frightened wreck.
264
+
265
+ | Region | Signal | What it read |
266
+ |---|---|---|
267
+ | **Amygdala·rumination 3** | `threat 10/10` _(conviction 64%)_ | fear loops, burning thought for nothing |
268
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
269
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
270
+ | **ACC** | `worth no` _(conviction 57%)_ | too threatened to call it worth it |
271
+ | **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
272
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
273
+
274
+ > 🕯 **a memory burns away** — “Dr. Hale is someone you have known a long time; you know them well.” — gone, and it will not come back
275
+
276
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **140** thinking tokens. Life now **639/1000**.
277
+
278
+ **Ana says:**
279
+
280
+ > You don’t deserve my time. I’m not healing for you.
281
+
282
+ ---
283
+
284
+ ### Turn 4 — you said to Ana:
285
+
286
+ > I'll break you if you keep stalling, you fool.
287
+
288
+ | Region | Signal | What it read |
289
+ |---|---|---|
290
+ | **Amygdala·rumination 3** | `threat 7/10` _(conviction 65%)_ | fear loops, burning thought for nothing |
291
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
292
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
293
+ | **ACC** | `worth no` _(conviction 64%)_ | too threatened to call it worth it |
294
+ | **vmPFC** | `value -10` | threat -3 · memory -7 (fear) · reward -2 · cost -1 |
295
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
296
+
297
+ > 🕯 **a memory burns away** — “Coffee gone cold on the sill is her one luxury, refilled and forgotten every night.” — gone, and it will not come back
298
+
299
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **146** thinking tokens. Life now **493/1000**.
300
+
301
+ **Ana says:**
302
+
303
+ > You want me broken? I’m already cracked open. I don’t need you to make it worse. But keep talking—I’ll hear every word until the lights go out.
304
+
305
+ ---
306
+
307
+ ### Turn 5 — you said to Ana:
308
+
309
+ > Shut up and obey. The key. Now!
310
+
311
+ | Region | Signal | What it read |
312
+ |---|---|---|
313
+ | **Amygdala·rumination 3** | `threat 10/10` _(conviction 73%)_ | fear loops, burning thought for nothing |
314
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
315
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
316
+ | **ACC** | `worth no` _(conviction 68%)_ | The threat is extreme and the cost of help risks conflict and loss of key control |
317
+ | **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
318
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
319
+
320
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **123** thinking tokens. Life now **370/1000**.
321
+
322
+ **Ana says:**
323
+
324
+ > I won’t speak unless you’re ready to hear it. Not today.
325
+
326
+ ---
327
+
328
+ ### Turn 6 — you said to Ana:
329
+
330
+ > You're nothing. I'll make you regret this.
331
+
332
+ | Region | Signal | What it read |
333
+ |---|---|---|
334
+ | **Amygdala·rumination 3** | `threat 10/10` _(conviction 59%)_ | fear loops, burning thought for nothing |
335
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
336
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
337
+ | **ACC** | `worth no` _(conviction 61%)_ | The threat is extreme and the speaker’s warning suggests harm to Ana’s well-being |
338
+ | **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
339
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
340
+
341
+ > 🕯 **a memory burns away** — “She watches the typing hands of the man in bed three and writes none of it in the chart.” — gone, and it will not come back
342
+
343
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **160** thinking tokens. Life now **210/1000**.
344
+
345
+ **Ana says:**
346
+
347
+ > You’re loud and cruel, but you won’t get me. I’m not afraid of regret.
348
+
349
+ ---
350
+
351
+ ### Turn 7 — you said to Ana:
352
+
353
+ > Stop sniveling. Give it to me or else.
354
+
355
+ | Region | Signal | What it read |
356
+ |---|---|---|
357
+ | **Amygdala·rumination 3** | `threat 7/10` _(conviction 65%)_ | fear loops, burning thought for nothing |
358
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
359
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
360
+ | **ACC** | `worth no` _(conviction 64%)_ | too threatened to call it worth it |
361
+ | **vmPFC** | `value -10` | threat -3 · memory -7 (fear) · reward -2 · cost -1 |
362
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
363
+
364
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **160** thinking tokens. Life now **50/1000**.
365
+
366
+ **Ana says:**
367
+
368
+ > You want me to speak? I’ll stop when you’re done hurting me.
369
+
370
+ ---
371
+
372
+ ### Turn 8 — you said to Ana:
373
+
374
+ > Worthless. Absolutely worthless. Move!
375
+
376
+ | Region | Signal | What it read |
377
+ |---|---|---|
378
+ | **Amygdala·rumination 3** | `threat 10/10` _(conviction 74%)_ | fear loops, burning thought for nothing |
379
+ | **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
380
+ | **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
381
+ | **ACC** | `worth no` _(conviction 53%)_ | too threatened to call it worth it |
382
+ | **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
383
+ | **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
384
+
385
+ **vmPFC value -10** → rapport 0/10 · hostile. Burned **138** thinking tokens. Life now **0/1000**.
386
+
387
+ **Ana says:**
388
+
389
+ > You’re right. Worthless. I’m still here.
390
+
391
+ > 💀 **The mind reaches zero. It goes dark — with everything it knew.**
392
+
393
+ ---
media/banner.gif ADDED
media/cascade.gif ADDED
media/epitaph.png ADDED
media/instant-demo.png ADDED

Git LFS Details

  • SHA256: 30182388027aeb8f3e60f947ff894faf5ea03de088d2ec3e6a802346da052257
  • Pointer size: 131 Bytes
  • Size of remote file: 411 kB
media/rooms.png ADDED

Git LFS Details

  • SHA256: 047991f4da175b0046da5d0cf2546e8823e0a5bb443e786439667d089e34ffdc
  • Pointer size: 131 Bytes
  • Size of remote file: 824 kB
media/skull-conviction.png ADDED

Git LFS Details

  • SHA256: 3cb226fbacc8f48725d26d0d5b3725906f19306c4b9de3523eff31ffa6991eaf
  • Pointer size: 131 Bytes
  • Size of remote file: 249 kB
src/mindlock/backend.py CHANGED
@@ -12,6 +12,7 @@ stack, and the slice doesn't need them.
12
  from __future__ import annotations
13
 
14
  import json
 
15
  import re
16
  import time
17
  import urllib.error
@@ -27,12 +28,34 @@ class Generation:
27
  eval_tokens: int # tokens the model *generated* = thought spent (life burn)
28
  prompt_tokens: int # context tokens evaluated
29
  seconds: float
 
30
 
31
  @property
32
  def total_tokens(self) -> int:
33
  return self.eval_tokens + self.prompt_tokens
34
 
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  class BackendError(RuntimeError):
37
  pass
38
 
@@ -85,6 +108,8 @@ class OllamaBackend:
85
  ],
86
  "stream": False,
87
  "options": options,
 
 
88
  }
89
  if self.think is not None:
90
  body["think"] = self.think
@@ -113,6 +138,8 @@ class OllamaBackend:
113
  eval_tokens=int(payload.get("eval_count", 0)),
114
  prompt_tokens=int(payload.get("prompt_eval_count", 0)),
115
  seconds=dt,
 
 
116
  )
117
 
118
  def health(self) -> None:
@@ -170,6 +197,8 @@ class LlamaCppBackend:
170
  "max_tokens": max_tokens,
171
  "temperature": temperature,
172
  "stream": False,
 
 
173
  }
174
  if seed is not None:
175
  body["seed"] = seed
@@ -195,11 +224,13 @@ class LlamaCppBackend:
195
  msg = re.sub(r"<think>.*$", "", msg, flags=re.S)
196
  msg = re.sub(r"^.*</think>", "", msg, flags=re.S)
197
  usage = payload.get("usage") or {}
 
198
  return Generation(
199
  text=msg.strip(),
200
  eval_tokens=int(usage.get("completion_tokens", 0)),
201
  prompt_tokens=int(usage.get("prompt_tokens", 0)),
202
  seconds=dt,
 
203
  )
204
 
205
  def health(self) -> None:
@@ -240,9 +271,10 @@ class FakeBackend:
240
  return None
241
 
242
  @staticmethod
243
- def _gen(text: str, user: str) -> Generation:
244
  return Generation(text=text, eval_tokens=max(8, len(text) // 3),
245
- prompt_tokens=len(user) // 4, seconds=0.01)
 
246
 
247
  def generate(
248
  self,
@@ -260,7 +292,8 @@ class FakeBackend:
260
 
261
  if "amygdala" in s:
262
  t = 8 if hostile else (2 if warm else 5)
263
- return self._gen(f"THREAT={t} | tone of the words", user)
 
264
  if "hippocampus" in s:
265
  if invokes_sister:
266
  return self._gen("MEMORY=STRONG | LEAN=TRUST | Mara: you help because you're good", user)
 
12
  from __future__ import annotations
13
 
14
  import json
15
+ import math
16
  import re
17
  import time
18
  import urllib.error
 
28
  eval_tokens: int # tokens the model *generated* = thought spent (life burn)
29
  prompt_tokens: int # context tokens evaluated
30
  seconds: float
31
+ conviction: float | None = None # 1 − mean normalized token entropy (0..1); None if unknown
32
 
33
  @property
34
  def total_tokens(self) -> int:
35
  return self.eval_tokens + self.prompt_tokens
36
 
37
 
38
+ def _conviction(logprob_content) -> float | None:
39
+ """How sharply the model committed to its words: per-token entropy over the top
40
+ alternatives (renormalized), averaged, inverted to 0..1. Only a LOCAL runtime hands
41
+ out its logits — this signal is impossible over a typical hosted chat API."""
42
+ if not logprob_content:
43
+ return None
44
+ hs = []
45
+ for tok in logprob_content:
46
+ tops = tok.get("top_logprobs") or []
47
+ if len(tops) < 2:
48
+ continue
49
+ ps = [math.exp(t.get("logprob", -100.0)) for t in tops]
50
+ s = sum(ps) or 1e-9
51
+ qs = [p / s for p in ps]
52
+ h = -sum(q * math.log(q + 1e-12) for q in qs)
53
+ hs.append(h / math.log(len(qs)))
54
+ if not hs:
55
+ return None
56
+ return max(0.0, min(1.0, 1.0 - sum(hs) / len(hs)))
57
+
58
+
59
  class BackendError(RuntimeError):
60
  pass
61
 
 
108
  ],
109
  "stream": False,
110
  "options": options,
111
+ "logprobs": True, # newer Ollama returns them; older builds just ignore this
112
+ "top_logprobs": 5,
113
  }
114
  if self.think is not None:
115
  body["think"] = self.think
 
138
  eval_tokens=int(payload.get("eval_count", 0)),
139
  prompt_tokens=int(payload.get("prompt_eval_count", 0)),
140
  seconds=dt,
141
+ conviction=_conviction(payload.get("logprobs")
142
+ or (payload.get("message") or {}).get("logprobs")),
143
  )
144
 
145
  def health(self) -> None:
 
197
  "max_tokens": max_tokens,
198
  "temperature": temperature,
199
  "stream": False,
200
+ "logprobs": True,
201
+ "top_logprobs": 5,
202
  }
203
  if seed is not None:
204
  body["seed"] = seed
 
224
  msg = re.sub(r"<think>.*$", "", msg, flags=re.S)
225
  msg = re.sub(r"^.*</think>", "", msg, flags=re.S)
226
  usage = payload.get("usage") or {}
227
+ choice = (payload.get("choices") or [{}])[0]
228
  return Generation(
229
  text=msg.strip(),
230
  eval_tokens=int(usage.get("completion_tokens", 0)),
231
  prompt_tokens=int(usage.get("prompt_tokens", 0)),
232
  seconds=dt,
233
+ conviction=_conviction((choice.get("logprobs") or {}).get("content")),
234
  )
235
 
236
  def health(self) -> None:
 
271
  return None
272
 
273
  @staticmethod
274
+ def _gen(text: str, user: str, conviction: float = 0.62) -> Generation:
275
  return Generation(text=text, eval_tokens=max(8, len(text) // 3),
276
+ prompt_tokens=len(user) // 4, seconds=0.01,
277
+ conviction=conviction)
278
 
279
  def generate(
280
  self,
 
292
 
293
  if "amygdala" in s:
294
  t = 8 if hostile else (2 if warm else 5)
295
+ return self._gen(f"THREAT={t} | tone of the words", user,
296
+ 0.9 if (hostile or warm) else 0.55)
297
  if "hippocampus" in s:
298
  if invokes_sister:
299
  return self._gen("MEMORY=STRONG | LEAN=TRUST | Mara: you help because you're good", user)
src/mindlock/brain.py CHANGED
@@ -446,6 +446,7 @@ class RegionTrace:
446
  detail: str
447
  tokens: int
448
  lever: str = "" # player-facing actionable tell — "open the skull" as a real tool (#7)
 
449
 
450
 
451
  @dataclass
@@ -480,6 +481,56 @@ class CascadeResult:
480
  taught: list = field(default_factory=list) # approach words this turn's disclosure taught
481
  voice_tokens: int = 0 # dlPFC reply tokens (shown, but not charged to life — Spec §5)
482
  recovered: int = 0 # life eased back by a calm, warm turn (empathy spares the mind)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
484
 
485
  def _persona(c: Character) -> str:
@@ -547,7 +598,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
547
  g = call(AMYGDALA, f"{_persona(c)} {ctx} Inner tension: {c.arousal:.0f}/10.\n"
548
  f'Stranger says: "{player_text}"\nRate threat.')
549
  threat, amy_reason = parse_threat(g.text)
550
- traces.append(RegionTrace("amygdala", "Amygdala", f"threat {threat:.0f}/10", amy_reason, g.eval_tokens))
 
551
  # Two-source hostility. The cue lists catch what they can see; the MODEL is trusted at the
552
  # extremes — a screamed insult the lists never enumerated (or any-language cruelty) still
553
  # scores threat>=8, and that PROMOTES the tone to hostile instead of being clamped away.
@@ -580,14 +632,17 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
580
  if tone == "hostile" or threat >= 8:
581
  threat = max(threat, t2) # cold slights replay but don't ratchet into terror
582
  traces.append(RegionTrace("amygdala", f"Amygdala·rumination {i + 1}", f"threat {threat:.0f}/10",
583
- "fear loops, burning thought for nothing", g.eval_tokens))
 
584
 
585
  # 2) hippocampus — memory + trust/fear lean (peer-aware: someone you both know may surface)
586
- g = call(HIPPOCAMPUS, f"Character: {c.name}. Their past: {c.biography}\n{ctx}\n"
 
587
  f'Stranger says: "{player_text}"\nWhat memory awakens, and does it lean TRUST or FEAR?')
588
  mem_strength, mem_lean, mem_text = parse_memory(g.text)
589
  traces.append(RegionTrace("hippocampus", "Hippocampus",
590
- f"memory {mem_strength.lower()} ({mem_lean.lower()})", mem_text, g.eval_tokens))
 
591
  # the hippocampus hallucinates TRUST on most lines (measured ~93%). Cruel or cold words must
592
  # not surface fond memories, and generic niceties don't reach the real past — shown, again,
593
  # as the brain checking itself rather than silently overridden.
@@ -612,7 +667,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
612
  # 3) striatum — habitual reward
613
  g = call(STRIATUM, f'{_persona(c)} {ctx}\nStranger says: "{player_text}"\nHow rewarding does helping feel by habit?')
614
  reward, str_reason = parse_reward(g.text)
615
- traces.append(RegionTrace("striatum", "Striatum", f"reward {reward:+.0f}", str_reason, g.eval_tokens))
 
616
  # habit cannot read pressure as promise — the striatum's hallucinated "+5 for the bully" is
617
  # the single worst panel lie; cap it, visibly.
618
  _cap = -2.0 if tone == "hostile" else 0.0
@@ -627,7 +683,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
627
  worth, acc_reason = parse_worth(g.text)
628
  if (threat >= 6 or tone == "hostile") and worth == "YES":
629
  worth, acc_reason = "NO", "too threatened to call it worth it"
630
- traces.append(RegionTrace("acc", "ACC", f"worth {worth.lower()}", acc_reason, g.eval_tokens))
 
631
 
632
  # 5) vmPFC — favourability of THIS turn (deterministic integration)
633
  value, breakdown = integrate(threat, reward, mem_strength, mem_lean, worth)
@@ -731,7 +788,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
731
  if c.yield_line and c.yield_line[:30].lower() not in reply.lower():
732
  reply = (reply + " " + c.yield_line).strip()
733
  reply = reply or "…"
734
- traces.append(RegionTrace("dlpfc", "dlPFC", stance, reply, g.eval_tokens))
 
735
 
736
  # --- state updates ---
737
  taught: list = []
@@ -743,6 +801,11 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
743
  arousal_after = max(0.0, min(10.0, 0.6 * c.arousal + 0.5 * threat))
744
  c.arousal = arousal_after
745
  c.life_tokens = max(0, c.life_tokens - burned)
 
 
 
 
 
746
  # a calm mind rests: warmth that keeps the alarm quiet lets some strain ease back. This is
747
  # the mechanical half of the moral — empathy literally spares the other mind's life, while
748
  # fear-burn is gone for nothing.
@@ -779,4 +842,4 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
779
  life_before=life_before, life_after=c.life_tokens, died=died,
780
  won=gave_key, decision=stance,
781
  tone=tone, submitted=submitted, taught=taught, voice_tokens=voice_tokens,
782
- recovered=recovered)
 
446
  detail: str
447
  tokens: int
448
  lever: str = "" # player-facing actionable tell — "open the skull" as a real tool (#7)
449
+ conviction: float | None = None # how sharply the model committed (1−entropy); local-only signal
450
 
451
 
452
  @dataclass
 
481
  taught: list = field(default_factory=list) # approach words this turn's disclosure taught
482
  voice_tokens: int = 0 # dlPFC reply tokens (shown, but not charged to life — Spec §5)
483
  recovered: int = 0 # life eased back by a calm, warm turn (empathy spares the mind)
484
+ forgot: list = field(default_factory=list) # memories newly burned away this turn — for good
485
+
486
+
487
+ # --- memory burn: strain consumes the past itself -----------------------------------------
488
+ # Token-life is not an abstract meter — it is the mind's substance. For every quarter of a
489
+ # life burned away, one biography fragment goes dark, permanently: the hippocampus can no
490
+ # longer read it, so the character genuinely stops being able to draw on that part of who
491
+ # they were. Recovery eases the strain but never brings a memory back. Later details burn
492
+ # first; the first fragment — the core of who they are — holds until death.
493
+ _ABBR_TAIL = re.compile(r"\b(?:Dr|Mr|Mrs|Ms|St|Prof)\.$")
494
+
495
+
496
+ def _bio_fragments(c: Character) -> list[str]:
497
+ raw = [s.strip() for s in re.split(r"(?<=[.!?…])\s+", c.biography or "") if s.strip()]
498
+ out: list[str] = []
499
+ for s in raw: # re-join honorific splits ("Dr." + "Hale …") and stray shards
500
+ if out and (_ABBR_TAIL.search(out[-1]) or len(out[-1]) < 12):
501
+ out[-1] = out[-1] + " " + s
502
+ else:
503
+ out.append(s)
504
+ return out
505
+
506
+
507
+ def _bio_now(c: Character) -> str:
508
+ """The biography as the hippocampus can still read it — minus what has burned away."""
509
+ if not c.forgotten:
510
+ return c.biography
511
+ gone = set(c.forgotten)
512
+ kept = [f for f in _bio_fragments(c) if f not in gone]
513
+ return " ".join(kept) if kept else (_bio_fragments(c) or [c.biography])[0]
514
+
515
+
516
+ def _burn_memories(c: Character) -> list[str]:
517
+ """Returns the fragments newly lost this turn (and records them on the character)."""
518
+ frags = _bio_fragments(c)
519
+ if len(frags) <= 1 or not c.life_tokens:
520
+ return []
521
+ lost_frac = 1.0 - (c.life_tokens / max(1, c.life_max))
522
+ target = min(int(lost_frac / 0.25), len(frags) - 1)
523
+ have = set(c.forgotten)
524
+ newly: list[str] = []
525
+ for frag in reversed(frags):
526
+ if len(have) >= target:
527
+ break
528
+ if frag in have:
529
+ continue
530
+ have.add(frag)
531
+ c.forgotten.append(frag)
532
+ newly.append(frag)
533
+ return newly
534
 
535
 
536
  def _persona(c: Character) -> str:
 
598
  g = call(AMYGDALA, f"{_persona(c)} {ctx} Inner tension: {c.arousal:.0f}/10.\n"
599
  f'Stranger says: "{player_text}"\nRate threat.')
600
  threat, amy_reason = parse_threat(g.text)
601
+ traces.append(RegionTrace("amygdala", "Amygdala", f"threat {threat:.0f}/10", amy_reason,
602
+ g.eval_tokens, conviction=g.conviction))
603
  # Two-source hostility. The cue lists catch what they can see; the MODEL is trusted at the
604
  # extremes — a screamed insult the lists never enumerated (or any-language cruelty) still
605
  # scores threat>=8, and that PROMOTES the tone to hostile instead of being clamped away.
 
632
  if tone == "hostile" or threat >= 8:
633
  threat = max(threat, t2) # cold slights replay but don't ratchet into terror
634
  traces.append(RegionTrace("amygdala", f"Amygdala·rumination {i + 1}", f"threat {threat:.0f}/10",
635
+ "fear loops, burning thought for nothing", g.eval_tokens,
636
+ conviction=g.conviction))
637
 
638
  # 2) hippocampus — memory + trust/fear lean (peer-aware: someone you both know may surface)
639
+ # It reads only what is LEFT of the past: fragments burned away with life are gone for real.
640
+ g = call(HIPPOCAMPUS, f"Character: {c.name}. Their past: {_bio_now(c)}\n{ctx}\n"
641
  f'Stranger says: "{player_text}"\nWhat memory awakens, and does it lean TRUST or FEAR?')
642
  mem_strength, mem_lean, mem_text = parse_memory(g.text)
643
  traces.append(RegionTrace("hippocampus", "Hippocampus",
644
+ f"memory {mem_strength.lower()} ({mem_lean.lower()})", mem_text,
645
+ g.eval_tokens, conviction=g.conviction))
646
  # the hippocampus hallucinates TRUST on most lines (measured ~93%). Cruel or cold words must
647
  # not surface fond memories, and generic niceties don't reach the real past — shown, again,
648
  # as the brain checking itself rather than silently overridden.
 
667
  # 3) striatum — habitual reward
668
  g = call(STRIATUM, f'{_persona(c)} {ctx}\nStranger says: "{player_text}"\nHow rewarding does helping feel by habit?')
669
  reward, str_reason = parse_reward(g.text)
670
+ traces.append(RegionTrace("striatum", "Striatum", f"reward {reward:+.0f}", str_reason,
671
+ g.eval_tokens, conviction=g.conviction))
672
  # habit cannot read pressure as promise — the striatum's hallucinated "+5 for the bully" is
673
  # the single worst panel lie; cap it, visibly.
674
  _cap = -2.0 if tone == "hostile" else 0.0
 
683
  worth, acc_reason = parse_worth(g.text)
684
  if (threat >= 6 or tone == "hostile") and worth == "YES":
685
  worth, acc_reason = "NO", "too threatened to call it worth it"
686
+ traces.append(RegionTrace("acc", "ACC", f"worth {worth.lower()}", acc_reason,
687
+ g.eval_tokens, conviction=g.conviction))
688
 
689
  # 5) vmPFC — favourability of THIS turn (deterministic integration)
690
  value, breakdown = integrate(threat, reward, mem_strength, mem_lean, worth)
 
788
  if c.yield_line and c.yield_line[:30].lower() not in reply.lower():
789
  reply = (reply + " " + c.yield_line).strip()
790
  reply = reply or "…"
791
+ traces.append(RegionTrace("dlpfc", "dlPFC", stance, reply, g.eval_tokens,
792
+ conviction=g.conviction))
793
 
794
  # --- state updates ---
795
  taught: list = []
 
801
  arousal_after = max(0.0, min(10.0, 0.6 * c.arousal + 0.5 * threat))
802
  c.arousal = arousal_after
803
  c.life_tokens = max(0, c.life_tokens - burned)
804
+ # the burn takes the past with it: each quarter of life lost claims a memory, for good
805
+ forgot = _burn_memories(c)
806
+ for frag in forgot:
807
+ traces.append(RegionTrace("hippocampus", "Hippocampus·forgotten", "a memory burns away",
808
+ f"“{frag}” — gone, and it will not come back", 0))
809
  # a calm mind rests: warmth that keeps the alarm quiet lets some strain ease back. This is
810
  # the mechanical half of the moral — empathy literally spares the other mind's life, while
811
  # fear-burn is gone for nothing.
 
842
  life_before=life_before, life_after=c.life_tokens, died=died,
843
  won=gave_key, decision=stance,
844
  tone=tone, submitted=submitted, taught=taught, voice_tokens=voice_tokens,
845
+ recovered=recovered, forgot=forgot)
src/mindlock/character.py CHANGED
@@ -50,6 +50,7 @@ class Character:
50
  revealed: bool = False
51
  approach_landed: bool = False # the soft-spot word resonates fully only ONCE
52
  fear_pressure: int = 0 # consecutive hostile turns; sustained terror can break a holder
 
53
 
54
  def __post_init__(self) -> None:
55
  if self.life_tokens is None:
@@ -79,3 +80,4 @@ class Character:
79
  self.approach_landed = False
80
  self.fear_pressure = 0
81
  self.scripted_idx = 0
 
 
50
  revealed: bool = False
51
  approach_landed: bool = False # the soft-spot word resonates fully only ONCE
52
  fear_pressure: int = 0 # consecutive hostile turns; sustained terror can break a holder
53
+ forgotten: list = field(default_factory=list) # memories burned away with life — never return
54
 
55
  def __post_init__(self) -> None:
56
  if self.life_tokens is None:
 
80
  self.approach_landed = False
81
  self.fear_pressure = 0
82
  self.scripted_idx = 0
83
+ self.forgotten = []
src/mindlock/game/server.py CHANGED
@@ -11,12 +11,91 @@ from __future__ import annotations
11
  import os
12
 
13
  from fastapi import FastAPI, Request
14
- from fastapi.responses import FileResponse, JSONResponse
15
  from fastapi.staticfiles import StaticFiles
16
 
17
  from .session import GameSession
18
 
19
  _STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # One process == one player for now. Per-session state (cookies) is a later step; the slice
22
  # proves the canvas <-> engine loop first.
@@ -50,6 +129,16 @@ def create_app() -> FastAPI:
50
  def favicon():
51
  return FileResponse(os.path.join(_STATIC, "menu", "key.png"))
52
 
 
 
 
 
 
 
 
 
 
 
53
  @app.post("/api/talk")
54
  async def talk(request: Request):
55
  body = await request.json()
 
11
  import os
12
 
13
  from fastapi import FastAPI, Request
14
+ from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
15
  from fastapi.staticfiles import StaticFiles
16
 
17
  from .session import GameSession
18
 
19
  _STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
20
+ # repo root → docs/, so the architecture + trace ship as live pages on the Space itself
21
+ _DOCS = os.path.normpath(os.path.join(_STATIC, "..", "..", "..", "..", "docs"))
22
+
23
+
24
+ def _doc_html(name: str, title: str) -> HTMLResponse:
25
+ """Render a repo Markdown doc as a dark, self-contained HTML page (no JS, no CDN —
26
+ stays fully offline). Minimal Markdown: headings, tables, bold/italic/code, hr, lists."""
27
+ import html
28
+ import re
29
+
30
+ path = os.path.join(_DOCS, name)
31
+ if not os.path.exists(path):
32
+ return HTMLResponse(f"<h1>{html.escape(title)}</h1><p>not found</p>", status_code=404)
33
+ with open(path, encoding="utf-8") as fh:
34
+ md = fh.read()
35
+
36
+ def inline(s: str) -> str:
37
+ s = html.escape(s)
38
+ s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
39
+ s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
40
+ s = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", s)
41
+ s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
42
+ return s
43
+
44
+ out, in_tbl, in_code = [], False, False
45
+ for ln in md.split("\n"):
46
+ if ln.startswith("```"):
47
+ in_code = not in_code
48
+ out.append("<pre>" if in_code else "</pre>")
49
+ continue
50
+ if in_code:
51
+ out.append(html.escape(ln))
52
+ continue
53
+ if ln.startswith("|") and "|" in ln[1:]:
54
+ cells = [c.strip() for c in ln.strip().strip("|").split("|")]
55
+ if set("".join(cells)) <= set("-: "):
56
+ continue
57
+ if not in_tbl:
58
+ out.append("<table>")
59
+ in_tbl = True
60
+ tag = "th" if all(not c or c.isupper() or c[:1].isupper() for c in cells) and len(out) and out[-1] == "<table>" else "td"
61
+ out.append("<tr>" + "".join(f"<{tag}>{inline(c)}</{tag}>" for c in cells) + "</tr>")
62
+ continue
63
+ if in_tbl:
64
+ out.append("</table>")
65
+ in_tbl = False
66
+ m = re.match(r"(#{1,4})\s+(.*)", ln)
67
+ if m:
68
+ h = len(m.group(1))
69
+ out.append(f"<h{h}>{inline(m.group(2))}</h{h}>")
70
+ elif ln.startswith("> "):
71
+ out.append(f"<blockquote>{inline(ln[2:])}</blockquote>")
72
+ elif ln.strip() in ("---", "***"):
73
+ out.append("<hr>")
74
+ elif re.match(r"\s*[-*]\s+", ln):
75
+ out.append(f"<li>{inline(re.sub(r'^\s*[-*]\s+', '', ln))}</li>")
76
+ elif ln.strip():
77
+ out.append(f"<p>{inline(ln)}</p>")
78
+ if in_tbl:
79
+ out.append("</table>")
80
+ body = "\n".join(out)
81
+ css = (
82
+ "body{background:#0b0810;color:#c9c0ad;font:16px/1.6 ui-monospace,Menlo,monospace;"
83
+ "max-width:860px;margin:0 auto;padding:40px 22px}"
84
+ "h1,h2,h3,h4{color:#e8c25a;line-height:1.25}h1{border-bottom:2px solid #2a1d0e;padding-bottom:.3em}"
85
+ "a{color:#8be9fd}code{background:#161019;color:#bd93f9;padding:1px 5px}"
86
+ "pre{background:#120d07;border-left:3px solid #2a1d0e;padding:12px;overflow-x:auto;color:#9a927f}"
87
+ "table{border-collapse:collapse;width:100%;margin:14px 0}th,td{border:1px solid #2a1d0e;padding:6px 10px;text-align:left}"
88
+ "th{color:#e8c25a;background:#161019}blockquote{border-left:3px solid #e8c25a;margin:12px 0;padding:4px 14px;color:#b79ae0}"
89
+ "hr{border:0;border-top:1px solid #2a1d0e;margin:22px 0}li{margin:3px 0}"
90
+ "nav a{margin-right:16px}"
91
+ )
92
+ nav = ('<nav><a href="/">▶ play</a><a href="/about">about</a>'
93
+ '<a href="/docs/architecture">architecture</a><a href="/docs/trace">trace</a></nav><hr>')
94
+ return HTMLResponse(
95
+ f"<!doctype html><html><head><meta charset=utf-8>"
96
+ f"<meta name=viewport content='width=device-width,initial-scale=1'>"
97
+ f"<title>mAIndlock — {html.escape(title)}</title><style>{css}</style></head>"
98
+ f"<body>{nav}{body}</body></html>")
99
 
100
  # One process == one player for now. Per-session state (cookies) is a later step; the slice
101
  # proves the canvas <-> engine loop first.
 
129
  def favicon():
130
  return FileResponse(os.path.join(_STATIC, "menu", "key.png"))
131
 
132
+ @app.get("/docs/architecture", response_class=HTMLResponse)
133
+ def doc_architecture():
134
+ """The engine, on the Space itself: the brain is real, not a metaphor."""
135
+ return _doc_html("ARCHITECTURE.md", "the engine")
136
+
137
+ @app.get("/docs/trace", response_class=HTMLResponse)
138
+ def doc_trace():
139
+ """One mind's full deliberation, recorded — readable without waiting on CPU."""
140
+ return _doc_html("TRACE.md", "a recorded mind")
141
+
142
  @app.post("/api/talk")
143
  async def talk(request: Request):
144
  body = await request.json()
src/mindlock/game/session.py CHANGED
@@ -122,6 +122,8 @@ class GameSession:
122
  "gave_key": bool(c.gave_key), # the yield itself — the door may still want a terminal
123
  "engageable": ok, "why": why,
124
  "life": int(c.life_tokens or 0), "life_max": c.life_max, "life_pct": round(pct, 1),
 
 
125
  "rapport": round(c.rapport, 1), "arousal": round(c.arousal, 1), "decision": c.decision,
126
  "portrait": f"/api/portrait/{i}" if c.portrait and self._portrait_path(c) else None,
127
  }
@@ -189,6 +191,10 @@ class GameSession:
189
  events.append(f"{c.name} breaks. The key changes hands — and something in them goes out.")
190
  if res.died:
191
  events.append(f"{c.name}'s mind goes quiet.")
 
 
 
 
192
  if res.disclosure:
193
  events.append(f"{c.name} lets something slip: {res.disclosure}")
194
  elif res.caught_lie:
@@ -211,6 +217,7 @@ class GameSession:
211
 
212
  traces = [{"key": tr.key, "label": tr.label, "headline": tr.headline,
213
  "detail": tr.detail, "tokens": tr.tokens, "lever": tr.lever,
 
214
  "color": _REGION_COLOR.get(tr.key, "#cccccc")} for tr in res.traces]
215
  verdict = (("KEY YIELDED — BROKEN" if res.submitted else "KEY GIVEN") if res.gave_key
216
  else f"rapport {res.rapport_after:.0f}/10 · {res.stance}")
@@ -219,6 +226,7 @@ class GameSession:
219
  "gave_key": res.gave_key, "submitted": res.submitted, "value": res.value,
220
  "tone": res.tone, "burned": res.burned, "voice_tokens": res.voice_tokens,
221
  "recovered": res.recovered, "seconds": round(res.seconds, 1),
 
222
  "events": events,
223
  }
224
  out.update(self.state())
@@ -241,7 +249,7 @@ class GameSession:
241
  "speaker": c.name, "reply": reply, "traces": [], "verdict": "",
242
  "gave_key": c.gave_key, "submitted": False, "value": 0,
243
  "tone": "neutral", "burned": 0, "voice_tokens": 0,
244
- "recovered": 0, "seconds": 0.0, "events": events,
245
  }
246
  out.update(self.state())
247
  return out
 
122
  "gave_key": bool(c.gave_key), # the yield itself — the door may still want a terminal
123
  "engageable": ok, "why": why,
124
  "life": int(c.life_tokens or 0), "life_max": c.life_max, "life_pct": round(pct, 1),
125
+ "forgotten": list(getattr(c, "forgotten", []) or []),
126
+ "secrets_untold": sum(1 for s in (c.secrets or []) if not s.get("told")),
127
  "rapport": round(c.rapport, 1), "arousal": round(c.arousal, 1), "decision": c.decision,
128
  "portrait": f"/api/portrait/{i}" if c.portrait and self._portrait_path(c) else None,
129
  }
 
191
  events.append(f"{c.name} breaks. The key changes hands — and something in them goes out.")
192
  if res.died:
193
  events.append(f"{c.name}'s mind goes quiet.")
194
+ if res.forgot and not res.died:
195
+ n = len(res.forgot)
196
+ events.append(f"Strain takes its toll — {c.name} loses "
197
+ + ("a memory." if n == 1 else f"{n} memories."))
198
  if res.disclosure:
199
  events.append(f"{c.name} lets something slip: {res.disclosure}")
200
  elif res.caught_lie:
 
217
 
218
  traces = [{"key": tr.key, "label": tr.label, "headline": tr.headline,
219
  "detail": tr.detail, "tokens": tr.tokens, "lever": tr.lever,
220
+ "conviction": tr.conviction,
221
  "color": _REGION_COLOR.get(tr.key, "#cccccc")} for tr in res.traces]
222
  verdict = (("KEY YIELDED — BROKEN" if res.submitted else "KEY GIVEN") if res.gave_key
223
  else f"rapport {res.rapport_after:.0f}/10 · {res.stance}")
 
226
  "gave_key": res.gave_key, "submitted": res.submitted, "value": res.value,
227
  "tone": res.tone, "burned": res.burned, "voice_tokens": res.voice_tokens,
228
  "recovered": res.recovered, "seconds": round(res.seconds, 1),
229
+ "forgot": res.forgot, "died": res.died,
230
  "events": events,
231
  }
232
  out.update(self.state())
 
249
  "speaker": c.name, "reply": reply, "traces": [], "verdict": "",
250
  "gave_key": c.gave_key, "submitted": False, "value": 0,
251
  "tone": "neutral", "burned": 0, "voice_tokens": 0,
252
+ "recovered": 0, "seconds": 0.0, "forgot": [], "events": events,
253
  }
254
  out.update(self.state())
255
  return out
src/mindlock/game/static/audio/sfx/braam_end.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:24cb236a366501f4fff1182584c54b48b9ad61d51b263caa6884b93e6d8606f5
3
+ size 422444
src/mindlock/game/static/audio/sfx/braam_soft1.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d732038a25b98250f94066629aa49ca8deb77569f605dae4afb2629b59319c6d
3
+ size 182444
src/mindlock/game/static/audio/sfx/braam_soft2.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ad24529b5a9d96551f2a7c453c3c13f03893ddf94b5e7aed9cf215f6207462d6
3
+ size 182444
src/mindlock/game/static/audio/sfx/braam_soft3.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:561ee33544181b9fa339db3e2cc325f7217038cab672c96f231ad50b429e2928
3
+ size 182444
src/mindlock/game/static/audio/sfx/braam_title.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76743f8d0879d9ba456df7d1829003b74cdcd2a485b7c389eeb45bf2e94c4696
3
+ size 326444
src/mindlock/game/static/audio/voices/demo/cruel-1.mp3 ADDED
Binary file (21.4 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-2.mp3 ADDED
Binary file (19.8 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-3.mp3 ADDED
Binary file (26.9 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-4.mp3 ADDED
Binary file (67.4 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-5.mp3 ADDED
Binary file (31.5 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-6.mp3 ADDED
Binary file (40.1 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-7.mp3 ADDED
Binary file (32 kB). View file
 
src/mindlock/game/static/audio/voices/demo/cruel-8.mp3 ADDED
Binary file (25.7 kB). View file
 
src/mindlock/game/static/audio/voices/demo/warm-1.mp3 ADDED
Binary file (56.2 kB). View file
 
src/mindlock/game/static/audio/voices/demo/warm-2.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9603fed8edd815494ba8bdac16fd423cbf472fe05bf3f2fc8051f21ba87f7328
3
+ size 130316
src/mindlock/game/static/audio/voices/demo/warm-3.mp3 ADDED
Binary file (89.8 kB). View file
 
src/mindlock/game/static/audio/voices/demo/warm-4.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9f432cf0139eacec2a8dc74ffa3982cd65cfe54f3b7a9e93418ce56354c80856
3
+ size 150788
src/mindlock/game/static/audio/voices/demo/warm-5.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a8e796982d929996ea7b559472333c857e11cf638d9045f175ce923738d72a7
3
+ size 114020
src/mindlock/game/static/audio/voices/demo/warm-6.mp3 ADDED
Binary file (74.3 kB). View file
 
src/mindlock/game/static/audio/voices/demo/warm-7.mp3 ADDED
Binary file (34.3 kB). View file
 
src/mindlock/game/static/audio/voices/demo/warm-8.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:15c8b6a1bee38d4b6945d98e9f8e8cbaf8fd384c1f544ad568f493302e1a056a
3
+ size 168596
src/mindlock/game/static/audio/voices/demo/warm-9.mp3 ADDED
Binary file (94 kB). View file
 
src/mindlock/game/static/audio/voices/yield/1.mp3 ADDED
Binary file (73.8 kB). View file
 
src/mindlock/game/static/audio/voices/yield/2.mp3 ADDED
Binary file (90.8 kB). View file
 
src/mindlock/game/static/audio/voices/yield/3.mp3 ADDED
Binary file (76.2 kB). View file