TheNormsOfIntelligence commited on
Commit
4e02c3a
Β·
verified Β·
1 Parent(s): 85e4506

Restructure into nima_unified package + add model card

Browse files
Files changed (1) hide show
  1. nima_unified/ui/chemical_monitor.py +626 -0
nima_unified/ui/chemical_monitor.py ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Chemical Monitor Terminal UI
3
+ ================================================
4
+
5
+ 60fps ANSI terminal dashboard that observes the neurotransmitter shunt
6
+ via shared volatile memory. Does NOT carry data β€” only samples and renders.
7
+
8
+ Samples the NeurotransmitterShunt.get_state() at ~60fps and renders
9
+ a professional curses dashboard with:
10
+ - Horizontal bars for NE / Cortisol / Dopamine / Adenosine
11
+ - Color coding by threshold (GREEN / YELLOW / RED / CRITICAL)
12
+ - Sparkline history using unicode block characters
13
+ - Metabolic reserve, hijack status, power spike warnings
14
+ - Total firings and hijacks counters
15
+
16
+ Usage:
17
+ monitor = ChemicalMonitor(shunt)
18
+ monitor.start() # background curses thread
19
+ monitor.stop() # clean shutdown, restores terminal
20
+
21
+ # Or for non-curses contexts:
22
+ frame_str = monitor.render_single_frame()
23
+ """
24
+
25
+ import time
26
+ import re
27
+ import threading
28
+ from collections import deque
29
+ from typing import TYPE_CHECKING, Optional, Dict, Any, List
30
+
31
+ if TYPE_CHECKING:
32
+ from nima_unified.core.neurotransmitter_shunt import NeurotransmitterShunt
33
+
34
+
35
+ # ═══════════════════════════════════════════════════════════════════
36
+ # CONSTANTS
37
+ # ═══════════════════════════════════════════════════════════════════
38
+
39
+ # Sparkline block characters β€” 8 density levels, low to high
40
+ SPARKLINE_CHARS = '\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588'
41
+
42
+ # Layout dimensions
43
+ BOX_WIDTH = 70
44
+ INNER_W = BOX_WIDTH - 2 # 68 usable columns inside border
45
+ BAR_WIDTH = 24 # filled bar character count
46
+ SPARKLINE_LEN = 30 # number of history samples in sparkline
47
+ HISTORY_SIZE = 30 # ring buffer size per neurotransmitter
48
+ FRAME_MS = 16 # ~60fps
49
+
50
+ # Threshold boundaries
51
+ T_LOW = 0.4
52
+ T_HIGH = 0.75
53
+ T_CRIT = 0.95
54
+
55
+ # Curses color-pair IDs (initialized in _init_colors)
56
+ _CP_DIM = 1
57
+ _CP_CYAN = 2
58
+ _CP_RED = 3
59
+ _CP_GREEN = 4
60
+ _CP_MAGENTA = 5
61
+ _CP_YELLOW = 6
62
+ _CP_WHITE = 7
63
+ _CP_WHITE_ON_RED = 8
64
+
65
+
66
+ # ═══════════════════════════════════════════════════════════════════
67
+ # HELPERS
68
+ # ═══════════════════════════════════════════════════════════════════
69
+
70
+ def _level(value: float) -> int:
71
+ """Map a 0-1 value to a severity level: 0=green 1=yellow 2=red 3=critical."""
72
+ if value >= T_CRIT:
73
+ return 3
74
+ if value > T_HIGH:
75
+ return 2
76
+ if value >= T_LOW:
77
+ return 1
78
+ return 0
79
+
80
+
81
+ def _bar(value: float) -> tuple:
82
+ """Return (filled_count, empty_count) for a bar of BAR_WIDTH."""
83
+ f = max(0, min(BAR_WIDTH, int(value * BAR_WIDTH)))
84
+ return f, BAR_WIDTH - f
85
+
86
+
87
+ def _spark(values: List[float]) -> str:
88
+ """Convert a list of 0-1 floats into a sparkline string."""
89
+ return ''.join(SPARKLINE_CHARS[min(7, int(v * 8))] for v in values)
90
+
91
+
92
+ def _cpair(level: int, base: int) -> int:
93
+ """Curses color-pair ID for a severity level, falling back to *base*."""
94
+ if level == 3:
95
+ return _CP_WHITE_ON_RED
96
+ if level == 2:
97
+ return _CP_RED
98
+ if level == 1:
99
+ return _CP_YELLOW
100
+ return base
101
+
102
+
103
+ # ── ANSI escape sequences (for render_single_frame / non-curses paths) ──
104
+
105
+ class _A:
106
+ """Compact ANSI escape constants."""
107
+ RST = '\033[0m'
108
+ BLD = '\033[1m'
109
+ DIM = '\033[2m'
110
+ BLK = '\033[5m' # blink
111
+ BGK = '\033[40m' # black bg
112
+ BGR = '\033[41m' # red bg
113
+ W = '\033[37m' # white fg
114
+ C = '\033[36m' # cyan fg
115
+ R = '\033[31m' # red fg
116
+ G = '\033[32m' # green fg
117
+ Y = '\033[33m' # yellow fg
118
+ M = '\033[35m' # magenta fg
119
+ DW = '\033[90m' # dim white / gray fg
120
+
121
+
122
+ def _ansi_for(level: int, neon: str) -> str:
123
+ """Return ANSI color sequence for a severity level; *neon* used when green/0."""
124
+ if level == 3:
125
+ return _A.BLK + _A.W + _A.BGR + _A.BLD
126
+ if level == 2:
127
+ return _A.R + _A.BLD
128
+ if level == 1:
129
+ return _A.Y
130
+ return neon
131
+
132
+
133
+ _ANSI_RE = re.compile(r'\033\[[0-9;]*m')
134
+
135
+
136
+ def _vis_len(s: str) -> int:
137
+ """Visible character length of *s*, ignoring ANSI escape codes."""
138
+ return len(_ANSI_RE.sub('', s))
139
+
140
+
141
+ def _pad_vis(s: str, width: int) -> str:
142
+ """Right-pad an ANSI-colored string so its visible length equals *width*."""
143
+ gap = width - _vis_len(s)
144
+ if gap > 0:
145
+ return s + ' ' * gap
146
+ return s
147
+
148
+
149
+ # ═══════════════════════════════════════════════════════════════════
150
+ # CHEMICAL MONITOR
151
+ # ═══════════════════════════════════════════════════════════════════
152
+
153
+ class ChemicalMonitor:
154
+ """
155
+ 60fps terminal dashboard that observes the neurotransmitter shunt.
156
+
157
+ Purely observational β€” samples ``shunt.get_state()`` at high frequency
158
+ and renders the result as a curses or ANSI dashboard. Does **not**
159
+ write to or modify the shunt.
160
+
161
+ Parameters
162
+ ----------
163
+ shunt : NeurotransmitterShunt
164
+ Primary data source β€” the shared volatile memory vector.
165
+ model : optional
166
+ Reference to the NimaModel (used only to pull hijack count from
167
+ ``model.consciousness_metrics`` as a secondary source).
168
+ """
169
+
170
+ def __init__(self, shunt: 'NeurotransmitterShunt', model=None):
171
+ self._shunt = shunt
172
+ self._model = model
173
+ self._thread: Optional[threading.Thread] = None
174
+ self._running = False
175
+ self._frame = 0
176
+
177
+ # Sparkline ring buffers β€” one per neurotransmitter
178
+ self._hist_ne: deque = deque(maxlen=HISTORY_SIZE)
179
+ self._hist_co: deque = deque(maxlen=HISTORY_SIZE)
180
+ self._hist_do: deque = deque(maxlen=HISTORY_SIZE)
181
+ self._hist_ad: deque = deque(maxlen=HISTORY_SIZE)
182
+
183
+ # ──────────────────────────────────────────────────────────────
184
+ # PUBLIC API
185
+ # ──────────────────────────────────────────────────────────────
186
+
187
+ def start(self) -> None:
188
+ """Start the monitor in a background curses thread."""
189
+ if self._running:
190
+ return
191
+ self._running = True
192
+ self._thread = threading.Thread(target=self._thread_entry, daemon=True)
193
+ self._thread.start()
194
+
195
+ def stop(self) -> None:
196
+ """Stop the monitor and wait for the background thread to exit."""
197
+ self._running = False
198
+ if self._thread is not None:
199
+ self._thread.join(timeout=2.0)
200
+ self._thread = None
201
+
202
+ def render_single_frame(self) -> str:
203
+ """Sample the shunt and return one frame as an ANSI string."""
204
+ d = self._sample()
205
+ return '\n'.join(self._build_ansi_lines(d))
206
+
207
+ def get_snapshot(self) -> dict:
208
+ """Return the current shunt state as a plain dict."""
209
+ return self._shunt.get_state().to_dict()
210
+
211
+ # ──────────────────────────────────────────────────────────────
212
+ # SAMPLING
213
+ # ──────────────────────────────────────────────────────────────
214
+
215
+ def _sample(self) -> Dict[str, Any]:
216
+ """Sample the shunt, append to history buffers, bump frame counter."""
217
+ state = self._shunt.get_state()
218
+ self._hist_ne.append(state.norepinephrine)
219
+ self._hist_co.append(state.cortisol)
220
+ self._hist_do.append(state.dopamine)
221
+ self._hist_ad.append(state.adenosine)
222
+ self._frame += 1
223
+ return state.to_dict()
224
+
225
+ # ──────────────────────────────────────────────────────────────
226
+ # ANSI STRING RENDERING (non-curses path)
227
+ # ──────────────────────────────────────────────────────────────
228
+
229
+ def _build_ansi_lines(self, d: Dict[str, Any]) -> List[str]:
230
+ """Build a list of ANSI-escaped strings for one complete frame."""
231
+ ne = d['norepinephrine']
232
+ co = d['cortisol']
233
+ do = d['dopamine']
234
+ ad = d['adenosine']
235
+ meta = d['metabolic_reserve']
236
+ hijack = d['hijack_active']
237
+ reason = d['hijack_reason']
238
+ spike = d['power_spike_predicted']
239
+ firings = d['total_firings']
240
+ hijacks = d['total_hijacks']
241
+
242
+ W = INNER_W
243
+ A = _A
244
+ lines: List[str] = []
245
+
246
+ # ── helpers local to this frame ──
247
+ def _border(left: str, mid: str, right: str) -> str:
248
+ return f"{A.DIM}{left}{mid * W}{right}{A.RST}"
249
+
250
+ def _row(content: str, attr: int = 0) -> None:
251
+ lines.append(f"{A.DIM}\u2551{A.RST}"
252
+ f"{_pad_vis(content, W)}"
253
+ f"{A.DIM}\u2551{A.RST}")
254
+
255
+ def _center(text: str, color: str) -> None:
256
+ gap = max(0, W - len(text))
257
+ left = gap // 2
258
+ right = gap - left
259
+ _row(f"{' ' * left}{color}{text}{A.RST}{' ' * right}")
260
+
261
+ def _nt_row(name: str, val: float, hist: List[float], neon: str) -> None:
262
+ lv = _level(val)
263
+ col = _ansi_for(lv, neon)
264
+ f, e = _bar(val)
265
+ pct = f"{val * 100:5.1f}%"
266
+ filled = '\u2588' * f
267
+ empty = '\u2591' * e
268
+ _row(f" {A.BLD}{name:<14s}{A.RST} "
269
+ f"{col}[{filled}{empty}]{A.RST} "
270
+ f"{col}{pct}{A.RST}")
271
+ spark = _spark(hist) if hist else ' ' * SPARKLINE_LEN
272
+ _row(f"{' ' * 16}{col}{spark}{A.RST}")
273
+
274
+ # ════ LAYOUT ════
275
+
276
+ # Row 0 β€” top border
277
+ lines.append(_border('\u2554', '\u2550', '\u2557'))
278
+
279
+ # Row 1 β€” title
280
+ _center("NIMA CHEMICAL CORTEX", A.BLD + A.C)
281
+
282
+ # Row 2 β€” subtitle
283
+ _center("Neurotransmitter Shunt Monitor 60Hz", A.DW)
284
+
285
+ # Row 3 β€” separator
286
+ lines.append(_border('\u2560', '\u2550', '\u2563'))
287
+
288
+ # Rows 4-11 β€” four neurotransmitters (bar + sparkline each)
289
+ _nt_row("NOREPINEPHRINE", ne, list(self._hist_ne), A.C)
290
+ _nt_row("CORTISOL", co, list(self._hist_co), A.R)
291
+ _nt_row("DOPAMINE", do, list(self._hist_do), A.G)
292
+ _nt_row("ADENOSINE", ad, list(self._hist_ad), A.M)
293
+
294
+ # Row 12 β€” separator
295
+ lines.append(_border('\u2560', '\u2550', '\u2563'))
296
+
297
+ # Row 13 β€” metabolic reserve
298
+ ml = _level(meta) if meta < 0.5 else 0
299
+ if meta > 0.5:
300
+ mc = A.G
301
+ elif meta > 0.25:
302
+ mc = A.Y
303
+ elif meta > 0.1:
304
+ mc = A.R + A.BLD
305
+ else:
306
+ mc = A.BLK + A.W + A.BGR + A.BLD
307
+ mf, me = _bar(meta)
308
+ mpct = f"{meta * 100:5.1f}%"
309
+ _row(f" {A.BLD}METABOLIC RESERVE{A.RST} "
310
+ f"{mc}[\u2588{'\u2588' * (mf - 1) if mf else ''}"
311
+ f"{'\u2591' * me}]{A.RST} {mc}{mpct}{A.RST}")
312
+
313
+ # Row 14 β€” blank
314
+ _row("")
315
+
316
+ # Row 15 β€” hijack status
317
+ if hijack:
318
+ _row(f" HIJACK ACTIVE: {A.R}{A.BLD}{reason}{A.RST}")
319
+ elif co > 0.75 or ad > 0.75:
320
+ _row(f" {A.Y}{A.BLD}WARNING: APPROACHING METABOLIC CRISIS{A.RST}")
321
+ else:
322
+ _row(f" HIJACK STATUS: {A.G}NOMINAL{A.RST}")
323
+
324
+ # Row 16 β€” power spike
325
+ if spike:
326
+ _row(f" POWER SPIKE: {A.R}{A.BLD}PREDICTED - CORTISOL RISING{A.RST}")
327
+ else:
328
+ _row(f" POWER SPIKE: {A.DW}---{A.RST}")
329
+
330
+ # Row 17 β€” system state
331
+ if hijack:
332
+ _row(f" SYSTEM: {A.R}{A.BLD}AMYGDALA HIJACK INJECTED{A.RST}")
333
+ elif co > 0.75 or ad > 0.75:
334
+ _row(f" SYSTEM: {A.Y}ELEVATED STRESS{A.RST}")
335
+ else:
336
+ _row(f" SYSTEM: {A.DW}conscious processing active{A.RST}")
337
+
338
+ # Row 18 β€” separator
339
+ lines.append(_border('\u2560', '\u2550', '\u2563'))
340
+
341
+ # Row 19 β€” counters
342
+ _row(f" TOTAL FIRINGS: {A.C}{firings:>6}{A.RST}"
343
+ f" TOTAL HIJACKS: {A.R}{hijacks:>4}{A.RST}")
344
+
345
+ # Row 20 β€” frame / timestamp
346
+ ts = time.strftime("%H:%M:%S") + f".{int(time.time() * 1000) % 1000:03d}"
347
+ _row(f" FRAME: {A.DW}{self._frame:>8}{A.RST}"
348
+ f" SAMPLE: {A.DW}{ts}{A.RST}")
349
+
350
+ # Row 21 β€” bottom border
351
+ lines.append(_border('\u255a', '\u2550', '\u255d'))
352
+
353
+ return lines
354
+
355
+ # ──────────────────────────────────────────────────────────────
356
+ # CURSES RENDERING
357
+ # ──────────────────────────────────────────────────────────────
358
+
359
+ def _init_colors(self, stdscr) -> None:
360
+ """Initialise all curses colour pairs used by the dashboard."""
361
+ import curses
362
+ curses.start_color()
363
+ curses.use_default_colors()
364
+ curses.init_pair(_CP_DIM, 8, -1) # dark gray
365
+ curses.init_pair(_CP_CYAN, curses.COLOR_CYAN, curses.COLOR_BLACK)
366
+ curses.init_pair(_CP_RED, curses.COLOR_RED, curses.COLOR_BLACK)
367
+ curses.init_pair(_CP_GREEN, curses.COLOR_GREEN, curses.COLOR_BLACK)
368
+ curses.init_pair(_CP_MAGENTA,curses.COLOR_MAGENTA, curses.COLOR_BLACK)
369
+ curses.init_pair(_CP_YELLOW, curses.COLOR_YELLOW, curses.COLOR_BLACK)
370
+ curses.init_pair(_CP_WHITE, curses.COLOR_WHITE, curses.COLOR_BLACK)
371
+ curses.init_pair(_CP_WHITE_ON_RED,
372
+ curses.COLOR_WHITE, curses.COLOR_RED)
373
+
374
+ def render_curses_frame(self, stdscr) -> None:
375
+ """Sample the shunt and render one frame into *stdscr*."""
376
+ import curses
377
+
378
+ d = self._sample()
379
+ stdscr.erase()
380
+
381
+ ne = d['norepinephrine']
382
+ co = d['cortisol']
383
+ do = d['dopamine']
384
+ ad = d['adenosine']
385
+ meta = d['metabolic_reserve']
386
+ hijack = d['hijack_active']
387
+ reason = d['hijack_reason']
388
+ spike = d['power_spike_predicted']
389
+ firings = d['total_firings']
390
+ hijacks = d['total_hijacks']
391
+
392
+ max_y, max_x = stdscr.getmaxyx()
393
+ W = min(INNER_W, max_x - 2)
394
+ bw = W + 2 # total box width including borders
395
+
396
+ dim_a = curses.color_pair(_CP_DIM)
397
+ brd = dim_a # border attribute
398
+
399
+ # ── micro-helpers ──
400
+
401
+ def put(r: int, c: int, txt: str, attr: int = 0) -> None:
402
+ if 0 <= r < max_y and 0 <= c < max_x:
403
+ try:
404
+ stdscr.addstr(r, c, txt, attr)
405
+ except curses.error:
406
+ pass
407
+
408
+ def full_row(r: int, txt: str, attr: int = 0) -> None:
409
+ put(r, 0, '\u2551', brd)
410
+ put(r, 1, txt, attr)
411
+ tail = W - len(txt)
412
+ if tail > 0:
413
+ put(r, 1 + len(txt), ' ' * tail)
414
+ put(r, bw - 1, '\u2551', brd)
415
+
416
+ def center_row(r: int, txt: str, attr: int) -> None:
417
+ gap = max(0, W - len(txt))
418
+ left = gap // 2
419
+ full_row(r, ' ' * left + txt + ' ' * (gap - left), attr)
420
+
421
+ r = 0 # current row
422
+
423
+ # ── top border ──
424
+ put(r, 0, '\u2554' + '\u2550' * W + '\u2557', brd)
425
+ r += 1
426
+
427
+ # ── title ──
428
+ center_row(r, "NIMA CHEMICAL CORTEX",
429
+ curses.color_pair(_CP_CYAN) | curses.A_BOLD)
430
+ r += 1
431
+
432
+ # ── subtitle ──
433
+ center_row(r, "Neurotransmitter Shunt Monitor 60Hz", dim_a)
434
+ r += 1
435
+
436
+ # ── separator ──
437
+ put(r, 0, '\u2560' + '\u2550' * W + '\u2563', brd)
438
+ r += 1
439
+
440
+ # ── four neurotransmitter bar + sparkline rows ──
441
+ nt_items = [
442
+ ("NOREPINEPHRINE", ne, list(self._hist_ne), _CP_CYAN),
443
+ ("CORTISOL", co, list(self._hist_co), _CP_RED),
444
+ ("DOPAMINE", do, list(self._hist_do), _CP_GREEN),
445
+ ("ADENOSINE", ad, list(self._hist_ad), _CP_MAGENTA),
446
+ ]
447
+
448
+ for name, val, hist, base_cp in nt_items:
449
+ lv = _level(val)
450
+ cp = _cpair(lv, base_cp)
451
+ attr = curses.color_pair(cp)
452
+ if lv >= 2:
453
+ attr |= curses.A_BOLD
454
+ if lv == 3:
455
+ attr |= curses.A_BLINK
456
+
457
+ f, e = _bar(val)
458
+ pct = f"{val * 100:5.1f}%"
459
+ fill = '\u2588' * f
460
+ empty = '\u2591' * e
461
+
462
+ # bar line
463
+ label = f"{name:<14s}"
464
+ put(r, 0, '\u2551', brd)
465
+ put(r, 1, ' ', 0)
466
+ put(r, 2, label, curses.color_pair(base_cp) | curses.A_BOLD)
467
+ put(r, 16, ' [' + fill, attr)
468
+ put(r, 18 + f, empty + ']', dim_a)
469
+ put(r, 18 + BAR_WIDTH, ' ' + pct, attr)
470
+ end = 18 + BAR_WIDTH + 2 + 5
471
+ if end < 1 + W:
472
+ put(r, end, ' ' * (1 + W - end))
473
+ put(r, bw - 1, '\u2551', brd)
474
+ r += 1
475
+
476
+ # sparkline line
477
+ spark = _spark(hist) if hist else ' ' * SPARKLINE_LEN
478
+ put(r, 0, '\u2551', brd)
479
+ put(r, 16, spark, attr)
480
+ sp_end = 16 + len(spark)
481
+ if sp_end < 1 + W:
482
+ put(r, sp_end, ' ' * (1 + W - sp_end))
483
+ put(r, bw - 1, '\u2551', brd)
484
+ r += 1
485
+
486
+ # ── separator ──
487
+ put(r, 0, '\u2560' + '\u2550' * W + '\u2563', brd)
488
+ r += 1
489
+
490
+ # ── metabolic reserve ──
491
+ if meta > 0.5:
492
+ mcp = _CP_GREEN
493
+ elif meta > 0.25:
494
+ mcp = _CP_YELLOW
495
+ elif meta > 0.1:
496
+ mcp = _CP_RED
497
+ else:
498
+ mcp = _CP_WHITE_ON_RED
499
+ ma = curses.color_pair(mcp)
500
+ if meta <= 0.25:
501
+ ma |= curses.A_BOLD
502
+ if meta < 0.1:
503
+ ma |= curses.A_BLINK
504
+
505
+ mf, me = _bar(meta)
506
+ mpct = f"{meta * 100:5.1f}%"
507
+ put(r, 0, '\u2551', brd)
508
+ put(r, 1, ' METABOLIC RESERVE ', curses.color_pair(_CP_WHITE) | curses.A_BOLD)
509
+ put(r, 19, '[' + '\u2588' * mf, ma)
510
+ put(r, 21 + mf, '\u2591' * me + ']', dim_a)
511
+ put(r, 21 + BAR_WIDTH, ' ' + mpct, ma)
512
+ mend = 21 + BAR_WIDTH + 2 + 5
513
+ if mend < 1 + W:
514
+ put(r, mend, ' ' * (1 + W - mend))
515
+ put(r, bw - 1, '\u2551', brd)
516
+ r += 1
517
+
518
+ # ── blank ──
519
+ put(r, 0, '\u2551' + ' ' * W + '\u2551', brd)
520
+ r += 1
521
+
522
+ # ── hijack status ──
523
+ if hijack:
524
+ htxt = f" HIJACK ACTIVE: {reason}"
525
+ ha = curses.color_pair(_CP_RED) | curses.A_BOLD
526
+ elif co > 0.75 or ad > 0.75:
527
+ htxt = " WARNING: APPROACHING METABOLIC CRISIS"
528
+ ha = curses.color_pair(_CP_YELLOW) | curses.A_BOLD
529
+ else:
530
+ htxt = " HIJACK STATUS: NOMINAL"
531
+ ha = curses.color_pair(_CP_GREEN)
532
+ full_row(r, htxt, ha)
533
+ r += 1
534
+
535
+ # ── power spike ──
536
+ if spike:
537
+ stxt = " POWER SPIKE: PREDICTED - CORTISOL RISING"
538
+ sa = curses.color_pair(_CP_RED) | curses.A_BOLD
539
+ else:
540
+ stxt = " POWER SPIKE: ---"
541
+ sa = dim_a
542
+ full_row(r, stxt, sa)
543
+ r += 1
544
+
545
+ # ── system state ──
546
+ if hijack:
547
+ stxt2 = " SYSTEM: AMYGDALA HIJACK INJECTED"
548
+ sa2 = curses.color_pair(_CP_RED) | curses.A_BOLD
549
+ elif co > 0.75 or ad > 0.75:
550
+ stxt2 = " SYSTEM: ELEVATED STRESS"
551
+ sa2 = curses.color_pair(_CP_YELLOW)
552
+ else:
553
+ stxt2 = " SYSTEM: conscious processing active"
554
+ sa2 = dim_a
555
+ full_row(r, stxt2, sa2)
556
+ r += 1
557
+
558
+ # ── separator ──
559
+ put(r, 0, '\u2560' + '\u2550' * W + '\u2563', brd)
560
+ r += 1
561
+
562
+ # ── counters (two-tone) ──
563
+ fl = " TOTAL FIRINGS: "
564
+ fv = f"{firings:>6}"
565
+ hl = " TOTAL HIJACKS: "
566
+ hv = f"{hijacks:>4}"
567
+ put(r, 0, '\u2551', brd)
568
+ put(r, 1, fl, curses.color_pair(_CP_CYAN))
569
+ put(r, 1 + len(fl), fv, curses.color_pair(_CP_CYAN))
570
+ c1 = 1 + len(fl) + len(fv)
571
+ put(r, c1, hl, curses.color_pair(_CP_RED))
572
+ put(r, c1 + len(hl), hv, curses.color_pair(_CP_RED))
573
+ c2 = c1 + len(hl) + len(hv)
574
+ if c2 < 1 + W:
575
+ put(r, c2, ' ' * (1 + W - c2))
576
+ put(r, bw - 1, '\u2551', brd)
577
+ r += 1
578
+
579
+ # ── frame / timestamp ──
580
+ ts = time.strftime("%H:%M:%S") + f".{int(time.time() * 1000) % 1000:03d}"
581
+ full_row(r, f" FRAME: {self._frame:>8} SAMPLE: {ts}", dim_a)
582
+ r += 1
583
+
584
+ # ── bottom border ──
585
+ put(r, 0, '\u255a' + '\u2550' * W + '\u255d', brd)
586
+
587
+ stdscr.refresh()
588
+
589
+ # ──────────────────────────────────────────────────────────────
590
+ # THREAD MANAGEMENT
591
+ # ──────────────────────────────────────────────────────────────
592
+
593
+ def _thread_entry(self) -> None:
594
+ """Background thread entry point β€” wraps the curses loop."""
595
+ import curses
596
+ try:
597
+ curses.wrapper(self._curses_loop)
598
+ except Exception:
599
+ self._running = False
600
+
601
+ def _curses_loop(self, stdscr) -> None:
602
+ """
603
+ Main curses rendering loop. Samples at ~60 fps.
604
+
605
+ Press **q** or **Esc** to quit (also handled by ``stop()``).
606
+ ``curses.wrapper`` guarantees terminal state is restored on exit.
607
+ """
608
+ import curses
609
+
610
+ curses.curs_set(0) # hide cursor
611
+ stdscr.nodelay(True) # non-blocking getch
612
+ self._init_colors(stdscr)
613
+
614
+ while self._running:
615
+ self.render_curses_frame(stdscr)
616
+
617
+ # allow quit via keyboard
618
+ try:
619
+ key = stdscr.getch()
620
+ if key in (ord('q'), 27): # 'q' or Escape
621
+ self._running = False
622
+ break
623
+ except Exception:
624
+ pass
625
+
626
+ time.sleep(FRAME_MS / 1000.0)