peninsula123 commited on
Commit
cb55577
·
1 Parent(s): bce1d4b

Prepare OpenCortex hackathon submission

Browse files
README.md CHANGED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: OpenCortex
3
+ emoji: 🧠
4
+ colorFrom: cyan
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.18.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ tags:
12
+ - build-small-hackathon
13
+ - backyard-ai
14
+ - off-brand
15
+ - tiny-titan
16
+ - best-demo
17
+ - codex
18
+ - llama-cpp
19
+ - local-ai
20
+ - ai-infra
21
+ - observability
22
+ ---
23
+
24
+ # OpenCortex
25
+
26
+ **Making LLM inference visible.**
27
+
28
+ OpenCortex is a real-time observatory for local LLM inference. It pairs a chat
29
+ interface with a living view of the runtime behind the answer: working memory,
30
+ context pressure, token flow, and engine health.
31
+
32
+ Most chat interfaces show only two things:
33
+
34
+ ```text
35
+ user input -> assistant output
36
+ ```
37
+
38
+ OpenCortex shows the machine in the middle.
39
+
40
+ ```text
41
+ prompt prefill -> KV/cache pressure -> decode rhythm -> context boundary -> answer behavior
42
+ ```
43
+
44
+ It is built for learners, local AI users, and AI infrastructure engineers who
45
+ want to understand why an LLM slows down, loops, forgets, or runs out of
46
+ context.
47
+
48
+ ![OpenCortex runtime observatory](docs/assets/opencortex-hero.png)
49
+
50
+ > Screenshot TODO: replace `docs/assets/opencortex-hero.png` with the main
51
+ > 16:9 demo screenshot before submission.
52
+
53
+ ## Why this exists
54
+
55
+ Small local models make AI personal again. But local inference is still a black
56
+ box for most users. When a response becomes slow or repetitive, the user rarely
57
+ knows whether the model is thinking, overloaded, stuck in a loop, or simply
58
+ running out of context.
59
+
60
+ OpenCortex turns runtime signals into a product experience:
61
+
62
+ | Runtime signal | Human-readable concept | Visual feedback |
63
+ | --- | --- | --- |
64
+ | KV/cache usage proxy | Working Memory | block pressure and fragmentation |
65
+ | Context tokens | Context Window | filling chamber and active boundary |
66
+ | Decode throughput | Token Stream | flowing, broken, or stalled token river |
67
+ | TTFT and queue evidence | Engine State | pulse, charging, recovery, hazard state |
68
+ | Repeated generated text | Thought Loop | red loop warning and irregular core pulse |
69
+
70
+ The goal is not to build another metrics dashboard. The goal is to let people
71
+ feel the hidden mechanics of inference while they chat.
72
+
73
+ ## Demo links
74
+
75
+ - **Hugging Face Space:** TODO: add Space URL
76
+ - **Demo video:** TODO: add YouTube / Space video URL
77
+ - **Social post:** TODO: add X / LinkedIn / HF post URL
78
+
79
+ The Build Small Hackathon requires a deployed Gradio Space, a demo video, a
80
+ social post, and README tags for tracks and badges. This README is structured
81
+ for that submission flow.
82
+
83
+ The hosted Space runs in **simulated runtime mode** so judges can open the demo
84
+ without a private llama.cpp server. The local path below connects to live
85
+ llama.cpp metrics.
86
+
87
+ ## What you can try
88
+
89
+ OpenCortex has one live mode and four built-in runtime experiments.
90
+
91
+ ### 1. Live local chat
92
+
93
+ Ask the local model a question and watch the observatory react while the answer
94
+ streams. The UI listens to llama.cpp OpenAI-compatible streaming events,
95
+ timings, `/metrics`, and `/slots`.
96
+
97
+ ### 2. Long context stress
98
+
99
+ Shows how prompt growth increases prefill work before generation begins.
100
+
101
+ ### 3. Memory pressure
102
+
103
+ Shows working memory blocks fragmenting and reallocating as the runtime becomes
104
+ strained.
105
+
106
+ ### 4. Slow decode
107
+
108
+ Shows token flow breaking into a stop-burst-stop rhythm when generation slows.
109
+
110
+ ### 5. Context collapse
111
+
112
+ Shows the moment earlier turns leave the active context window. The chat history
113
+ stays visible, but OpenCortex marks the active context boundary so the user can
114
+ see what the model can no longer reliably use.
115
+
116
+ ![Context boundary event](docs/assets/opencortex-context-collapse.png)
117
+
118
+ > Screenshot TODO: capture the active context boundary and context window full
119
+ > state.
120
+
121
+ ### 6. Thought loop detection
122
+
123
+ If generation begins repeating the same pattern, OpenCortex marks it as a
124
+ thought loop. The Token Stream and Cortex Core switch into a red hazard state.
125
+
126
+ ![Thought loop detected](docs/assets/opencortex-thought-loop.png)
127
+
128
+ > Screenshot TODO: capture a repeated story response with `THOUGHT LOOP
129
+ > DETECTED` visible.
130
+
131
+ ## How it works
132
+
133
+ OpenCortex is intentionally lightweight:
134
+
135
+ ```text
136
+ Browser UI
137
+ |
138
+ | POST /api/chat
139
+ v
140
+ FastAPI / Space app
141
+ |
142
+ | OpenAI-compatible streaming request
143
+ v
144
+ llama.cpp llama-server
145
+ |
146
+ | /v1/chat/completions
147
+ | /metrics
148
+ | /slots
149
+ v
150
+ Runtime events -> semantic state -> visual organs
151
+ ```
152
+
153
+ The backend converts low-level runtime evidence into normalized events:
154
+
155
+ - `request_started`
156
+ - `first_token`
157
+ - `token`
158
+ - `context_collapse`
159
+ - `request_completed`
160
+ - `error`
161
+
162
+ The frontend consumes those events and updates the chat and observatory in the
163
+ same stream, so visual state and language output stay aligned.
164
+
165
+ ## Runtime evidence
166
+
167
+ The MVP uses real llama.cpp evidence where available:
168
+
169
+ | Evidence | Source | Used for |
170
+ | --- | --- | --- |
171
+ | Time to first token | measured in Python client | Engine State |
172
+ | Prompt tokens | llama.cpp usage/timings | Context Window |
173
+ | Completion tokens | llama.cpp usage/timings | Token Stream |
174
+ | Prompt throughput | llama.cpp timings | Prefill evidence |
175
+ | Decode throughput | llama.cpp timings and metrics | Token Stream |
176
+ | Active slot context | llama.cpp `/slots` | Context and memory proxy |
177
+ | Processing/deferred requests | llama.cpp `/metrics` | Engine State |
178
+ | Repetition pattern | generated text heuristic | Thought loop hazard |
179
+
180
+ Working Memory is labeled as a **context-derived proxy** in this MVP. llama.cpp
181
+ does not expose a simple per-request KV cache percentage through the HTTP API we
182
+ use, so OpenCortex derives a truthful pressure estimate from active slot context
183
+ tokens and context size.
184
+
185
+ ## Model
186
+
187
+ The current local demo uses:
188
+
189
+ ```text
190
+ Qwen/Qwen2.5-1.5B-Instruct-GGUF
191
+ quantization: Q4_K_M
192
+ context: 2048 tokens in the local demo
193
+ runtime: llama.cpp llama-server
194
+ ```
195
+
196
+ This keeps the submission inside the Build Small model limit and qualifies the
197
+ project for tiny-model storytelling. The interface is backend-neutral enough to
198
+ evolve toward Ollama, vLLM, or Modal-hosted llama.cpp later.
199
+
200
+ On Hugging Face Spaces, OpenCortex defaults to `OPEN_CORTEX_BACKEND=simulated`
201
+ when `SPACE_ID` is present. Set `OPEN_CORTEX_BACKEND=llama_cpp` only when the
202
+ Space can reach a running llama.cpp backend.
203
+
204
+ ## Run locally
205
+
206
+ ### 1. Install project dependencies
207
+
208
+ ```bash
209
+ uv sync
210
+ ```
211
+
212
+ ### 2. Start llama.cpp
213
+
214
+ From your llama.cpp checkout:
215
+
216
+ ```bash
217
+ llama-server \
218
+ -hf Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M \
219
+ -c 2048 -t 8 -tb 8 \
220
+ --metrics \
221
+ --host 127.0.0.1 --port 8080
222
+ ```
223
+
224
+ Check that the server is alive:
225
+
226
+ ```bash
227
+ curl --noproxy '*' http://127.0.0.1:8080/health
228
+ ```
229
+
230
+ Expected:
231
+
232
+ ```json
233
+ {"status":"ok"}
234
+ ```
235
+
236
+ ### 3. Start OpenCortex
237
+
238
+ ```bash
239
+ uv run open-cortex
240
+ ```
241
+
242
+ Then open:
243
+
244
+ ```text
245
+ http://127.0.0.1:7860
246
+ ```
247
+
248
+ ## Suggested demo script
249
+
250
+ Use this for the video:
251
+
252
+ 1. Open the app and send a normal question.
253
+ 2. Point out prefill, first-token latency, context usage, and token flow.
254
+ 3. Run **Slow decode** to show token flow breaking.
255
+ 4. Ask for a long story and show **Thought Loop Detected** when repetition
256
+ appears.
257
+ 5. Fill the context window and show **Context Window Full**.
258
+ 6. Send one more message and show the earliest message leaving the active
259
+ context boundary.
260
+ 7. Close with the core claim: OpenCortex turns runtime internals into something
261
+ users can see and reason about.
262
+
263
+ ## Screenshots needed before final submission
264
+
265
+ Place these files under `docs/assets/`:
266
+
267
+ ```text
268
+ docs/assets/opencortex-hero.png
269
+ docs/assets/opencortex-thought-loop.png
270
+ docs/assets/opencortex-context-collapse.png
271
+ docs/assets/opencortex-slow-decode.png
272
+ ```
273
+
274
+ Capture recommendations:
275
+
276
+ - `opencortex-hero.png`: full UI during live decode, 16:9, both chat and
277
+ observatory visible.
278
+ - `opencortex-thought-loop.png`: repeated generation with `THOUGHT LOOP
279
+ DETECTED` visible.
280
+ - `opencortex-context-collapse.png`: active context boundary visible on the
281
+ left, context organ visible on the right.
282
+ - `opencortex-slow-decode.png`: Slow decode experiment with broken token flow.
283
+
284
+ ## Hackathon fit
285
+
286
+ OpenCortex targets the **Backyard AI** track: it is a practical tool for
287
+ learning and debugging local AI behavior on hardware you own.
288
+
289
+ It also targets these badges:
290
+
291
+ - **Off Brand**: custom runtime cockpit UI, not default Gradio components.
292
+ - **Tiny Titan**: built around a 1.5B local model.
293
+ - **Best Demo**: the experience is designed around a clear visual story.
294
+ - **Best Use of Codex**: the project was developed with Codex assistance and
295
+ should be submitted with Codex-attributed commits in the connected repository.
296
+
297
+ Modal credits were used/planned for development exploration, but the current MVP
298
+ does not require Modal at runtime.
299
+
300
+ ## Truthfulness and limitations
301
+
302
+ OpenCortex is a visualization layer over real runtime evidence, but it does not
303
+ claim to expose every internal tensor or true biological cognition.
304
+
305
+ Current limitations:
306
+
307
+ - KV cache usage is approximated from active context pressure.
308
+ - Thought loop detection is heuristic pattern detection over generated text.
309
+ - Context collapse is handled at the application layer by trimming oldest turns.
310
+ - The current demo is optimized for a single local runtime, not multi-user
311
+ production serving.
312
+ - vLLM integration is a planned evolution, not part of v0.1.
313
+
314
+ These constraints are visible by design. The product shows both semantic states
315
+ and metric evidence so users can tell which parts are measured and which parts
316
+ are interpreted.
317
+
318
+ ## Project structure
319
+
320
+ ```text
321
+ app.py Space/local entry point
322
+ open_cortex/ui/app.py HTTP app, event streaming, HTML shell
323
+ open_cortex/ui/assets/ Product UI CSS and browser controller
324
+ open_cortex/runtime/client.py llama.cpp streaming client
325
+ open_cortex/runtime/metrics.py llama.cpp metrics and slots parser
326
+ open_cortex/runtime/events.py Runtime event model
327
+ tests/ Runtime and UI regression tests
328
+ ```
329
+
330
+ ## Development checks
331
+
332
+ ```bash
333
+ uv run pytest -q
334
+ mise exec -- node --check open_cortex/ui/assets/open_cortex.js
335
+ ```
336
+
337
+ ## Submission checklist
338
+
339
+ - [ ] Deploy Space inside `build-small-hackathon/`.
340
+ - [ ] Confirm the Space launches as a Gradio-compatible submission.
341
+ - [ ] Add final Space URL to this README.
342
+ - [ ] Add demo video URL to this README.
343
+ - [ ] Add social post URL to this README.
344
+ - [ ] Replace screenshot placeholders in `docs/assets/`.
345
+ - [ ] Run the Build Small README validator.
346
+ - [ ] Confirm the model is under 32B parameters.
347
+
348
+ ## License
349
+
350
+ MIT
app.py CHANGED
@@ -1,7 +1,5 @@
1
- from open_cortex.ui.app import build_app
2
 
3
- demo = build_app()
4
 
5
  if __name__ == "__main__":
6
- demo.queue()
7
- demo.launch()
 
1
+ from open_cortex.ui.app import app, main
2
 
 
3
 
4
  if __name__ == "__main__":
5
+ main()
 
docs/assets/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
docs/superpowers/plans/2026-06-15-context-collapse-retry.md ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Context Collapse Retry Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Turn llama.cpp context overflow into a visible Context Collapse event, then trim oldest chat history and retry once.
6
+
7
+ **Architecture:** Keep llama.cpp client behavior simple: it raises the HTTP 400 from llama.cpp. The UI service layer catches the overflow, emits a `context_collapse` NDJSON event, trims oldest non-system messages, and retries once. The browser renders the collapse boundary and continues streaming the retried response.
8
+
9
+ **Tech Stack:** Python FastAPI streaming NDJSON, httpx, vanilla JS, CSS.
10
+
11
+ ---
12
+
13
+ ### Task 1: Backend Collapse Event And Retry
14
+
15
+ **Files:**
16
+ - Modify: `open_cortex/ui/app.py`
17
+ - Test: `tests/ui/test_app.py`
18
+
19
+ - [ ] Write a failing test that monkeypatches `stream_chat_events` to raise an HTTP 400 overflow on the first call and stream a normal response on the second call.
20
+ - [ ] Implement `_is_context_overflow`, `_trim_for_context_collapse`, and retry-once logic in `_stream_events`.
21
+ - [ ] Verify the stream contains `request_started`, `context_collapse`, then retried response events.
22
+
23
+ ### Task 2: Frontend Collapse Boundary
24
+
25
+ **Files:**
26
+ - Modify: `open_cortex/ui/assets/open_cortex.js`
27
+ - Modify: `open_cortex/ui/assets/open_cortex.css`
28
+
29
+ - [ ] Add `applyContextCollapse(event)` to mark old messages as `historical-context`, insert `active-context-boundary`, and switch the right panel to collapse visuals.
30
+ - [ ] Ensure the next retried stream reuses the same assistant bubble.
31
+ - [ ] Run `mise exec -- node --check open_cortex/ui/assets/open_cortex.js`.
32
+
33
+ ### Task 3: Verification
34
+
35
+ **Files:**
36
+ - Test: all tests
37
+
38
+ - [ ] Run `uv run pytest -q`.
39
+ - [ ] Run `uv run python -m py_compile open_cortex/ui/app.py open_cortex/runtime/client.py`.
40
+ - [ ] Restart `uv run python app.py` and manually trigger a long-context continuation.
open_cortex/runtime/client.py CHANGED
@@ -13,6 +13,39 @@ METRICS_URL = "http://127.0.0.1:8080/metrics"
13
  SLOTS_URL = "http://127.0.0.1:8080/slots"
14
 
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
17
  request_body = {
18
  "messages": to_llama_messages(message),
@@ -25,7 +58,12 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
25
 
26
  request_started = time.perf_counter()
27
  first_token_seen = False
 
 
 
28
  final_stats = None
 
 
29
 
30
  yield RuntimeEvent(
31
  kind="request_started",
@@ -36,7 +74,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
36
 
37
  with httpx.Client(timeout=120.0, trust_env=False) as client:
38
  with client.stream("POST", CHAT_URL, json=request_body) as response:
39
- response.raise_for_status()
40
 
41
  for line in response.iter_lines():
42
  if not line.startswith("data: "):
@@ -54,27 +92,78 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
54
  content = choices[0].get("delta", {}).get("content")
55
 
56
  if content:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  if not first_token_seen:
58
  first_token_seen = True
59
- first_token_at = time.perf_counter()
60
  ttft_ms = (first_token_at - request_started) * 1000
61
  snapshot = fetch_runtime_snapshot(
62
  client,
63
  METRICS_URL,
64
  SLOTS_URL,
65
  )
 
 
 
 
 
 
 
 
 
 
 
66
  yield RuntimeEvent(
67
  kind="first_token",
68
  text_delta=content,
69
  ttft_ms=ttft_ms,
70
  snapshot=snapshot,
 
 
 
 
 
 
 
 
 
 
71
  )
72
  else:
 
 
 
 
 
73
  yield RuntimeEvent(
74
  kind="token",
75
  text_delta=content,
76
  ttft_ms=None,
77
  snapshot=None,
 
 
 
 
 
 
 
 
 
 
78
  )
79
 
80
  if event.get("usage"):
@@ -83,6 +172,7 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
83
  if final_stats is not None:
84
  usage = final_stats["usage"]
85
  timings = final_stats["timings"]
 
86
 
87
  yield RuntimeEvent(
88
  kind="request_completed",
@@ -93,4 +183,12 @@ def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
93
  completion_tokens=usage["completion_tokens"],
94
  prompt_tps=timings["prompt_per_second"],
95
  decode_tps=timings["predicted_per_second"],
96
- )
 
 
 
 
 
 
 
 
 
13
  SLOTS_URL = "http://127.0.0.1:8080/slots"
14
 
15
 
16
+ def _raise_for_status_with_body(response: httpx.Response) -> None:
17
+ if response.is_error:
18
+ try:
19
+ response.read()
20
+ except httpx.HTTPError:
21
+ pass
22
+ response.raise_for_status()
23
+
24
+
25
+ def _working_memory_percent(
26
+ context_tokens: int | None,
27
+ context_size: int | None,
28
+ ) -> float | None:
29
+ if context_tokens is None or not context_size:
30
+ return None
31
+ return round(min(100.0, context_tokens / context_size * 100), 1)
32
+
33
+
34
+ def _detect_repetition(text: str) -> bool:
35
+ normalized = " ".join(text.split())
36
+ if len(normalized) < 120:
37
+ return False
38
+
39
+ for window in (96, 72, 48, 32):
40
+ if len(normalized) <= window * 2:
41
+ continue
42
+ tail = normalized[-window:]
43
+ if normalized[:-window].count(tail) >= 1:
44
+ return True
45
+
46
+ return False
47
+
48
+
49
  def stream_chat_events(message: list[ChatMessage]) -> Iterator[RuntimeEvent]:
50
  request_body = {
51
  "messages": to_llama_messages(message),
 
58
 
59
  request_started = time.perf_counter()
60
  first_token_seen = False
61
+ first_token_at = None
62
+ generated_tokens = 0
63
+ generated_text = ""
64
  final_stats = None
65
+ base_context_tokens = None
66
+ context_size = None
67
 
68
  yield RuntimeEvent(
69
  kind="request_started",
 
74
 
75
  with httpx.Client(timeout=120.0, trust_env=False) as client:
76
  with client.stream("POST", CHAT_URL, json=request_body) as response:
77
+ _raise_for_status_with_body(response)
78
 
79
  for line in response.iter_lines():
80
  if not line.startswith("data: "):
 
92
  content = choices[0].get("delta", {}).get("content")
93
 
94
  if content:
95
+ now = time.perf_counter()
96
+ generated_tokens += 1
97
+ generated_text += content
98
+ elapsed_ms = (
99
+ (now - first_token_at) * 1000
100
+ if first_token_at is not None
101
+ else 0.0
102
+ )
103
+ live_tps = (
104
+ generated_tokens / (elapsed_ms / 1000)
105
+ if elapsed_ms > 0
106
+ else None
107
+ )
108
+ repetition_detected = _detect_repetition(generated_text)
109
+
110
  if not first_token_seen:
111
  first_token_seen = True
112
+ first_token_at = now
113
  ttft_ms = (first_token_at - request_started) * 1000
114
  snapshot = fetch_runtime_snapshot(
115
  client,
116
  METRICS_URL,
117
  SLOTS_URL,
118
  )
119
+ base_context_tokens = (
120
+ snapshot.slot_context_tokens[0]
121
+ if snapshot.slot_context_tokens
122
+ else None
123
+ )
124
+ context_size = snapshot.slot_context_size
125
+ context_tokens = (
126
+ base_context_tokens + generated_tokens
127
+ if base_context_tokens is not None
128
+ else None
129
+ )
130
  yield RuntimeEvent(
131
  kind="first_token",
132
  text_delta=content,
133
  ttft_ms=ttft_ms,
134
  snapshot=snapshot,
135
+ generated_tokens=generated_tokens,
136
+ elapsed_ms=0.0,
137
+ live_tps=None,
138
+ repetition_detected=repetition_detected,
139
+ context_tokens=context_tokens,
140
+ context_size=context_size,
141
+ working_memory_percent=_working_memory_percent(
142
+ context_tokens,
143
+ context_size,
144
+ ),
145
  )
146
  else:
147
+ context_tokens = (
148
+ base_context_tokens + generated_tokens
149
+ if base_context_tokens is not None
150
+ else None
151
+ )
152
  yield RuntimeEvent(
153
  kind="token",
154
  text_delta=content,
155
  ttft_ms=None,
156
  snapshot=None,
157
+ generated_tokens=generated_tokens,
158
+ elapsed_ms=elapsed_ms,
159
+ live_tps=live_tps,
160
+ repetition_detected=repetition_detected,
161
+ context_tokens=context_tokens,
162
+ context_size=context_size,
163
+ working_memory_percent=_working_memory_percent(
164
+ context_tokens,
165
+ context_size,
166
+ ),
167
  )
168
 
169
  if event.get("usage"):
 
172
  if final_stats is not None:
173
  usage = final_stats["usage"]
174
  timings = final_stats["timings"]
175
+ context_tokens = usage["prompt_tokens"] + usage["completion_tokens"]
176
 
177
  yield RuntimeEvent(
178
  kind="request_completed",
 
183
  completion_tokens=usage["completion_tokens"],
184
  prompt_tps=timings["prompt_per_second"],
185
  decode_tps=timings["predicted_per_second"],
186
+ generated_tokens=usage["completion_tokens"],
187
+ repetition_detected=_detect_repetition(generated_text),
188
+ context_tokens=context_tokens,
189
+ context_size=context_size,
190
+ working_memory_percent=_working_memory_percent(
191
+ context_tokens,
192
+ context_size,
193
+ ),
194
+ )
open_cortex/runtime/events.py CHANGED
@@ -18,7 +18,14 @@ class RuntimeEvent:
18
  text_delta: str
19
  ttft_ms: float | None
20
  snapshot: RuntimeSnapshot | None
 
 
 
 
 
 
 
21
  prompt_tokens: int | None = None
22
  completion_tokens: int | None = None
23
  prompt_tps: float | None = None
24
- decode_tps: float | None = None
 
18
  text_delta: str
19
  ttft_ms: float | None
20
  snapshot: RuntimeSnapshot | None
21
+ generated_tokens: int = 0
22
+ elapsed_ms: float | None = None
23
+ live_tps: float | None = None
24
+ repetition_detected: bool = False
25
+ context_tokens: int | None = None
26
+ context_size: int | None = None
27
+ working_memory_percent: float | None = None
28
  prompt_tokens: int | None = None
29
  completion_tokens: int | None = None
30
  prompt_tps: float | None = None
31
+ decode_tps: float | None = None
open_cortex/ui/app.py CHANGED
@@ -1,138 +1,554 @@
1
- import gradio as gr
2
- from open_cortex.ui.gradio_history import history_to_chat_messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  from open_cortex.runtime.client import stream_chat_events
 
 
 
4
 
5
 
6
- def format_runtime(event) -> str:
7
- if event.kind == "request_started":
8
- return "Phase: request started\nWaiting for first token..."
 
 
 
9
 
10
- if event.kind == "first_token" and event.snapshot is not None:
11
- snapshot = event.snapshot
12
- context_tokens = (
13
- snapshot.slot_context_tokens[0]
14
- if snapshot.slot_context_tokens
15
- else None
16
- )
17
 
18
- return "\n".join(
19
- [
20
- "Phase: first token",
21
- f"TTFT: {event.ttft_ms:.1f} ms",
22
- f"Context: {context_tokens} / {snapshot.slot_context_size}",
23
- f"Token Stream: {snapshot.decode_tps} tok/s",
24
- (
25
- "Engine: "
26
- f"processing={snapshot.requests_processing} "
27
- f"deferred={snapshot.requests_deferred}"
28
- ),
29
- ]
30
- )
31
 
32
- if event.kind == "request_completed":
33
- return "\n".join(
34
- [
35
- "Phase: completed",
36
- f"Prompt tokens: {event.prompt_tokens}",
37
- f"Output tokens: {event.completion_tokens}",
38
- f"Prefill: {event.prompt_tps:.1f} tok/s",
39
- f"Decode: {event.decode_tps:.1f} tok/s",
40
- ]
41
- )
42
 
43
- return "Phase: decoding"
 
44
 
45
 
 
 
46
 
47
- def user(user_message: str, history: list[dict]) -> tuple[str, list[dict]]:
48
- if not user_message.strip():
49
- return "", history
50
 
51
- return "", history + [
52
- {
53
- "role": "user",
54
- "content": user_message,
55
- }
56
- ]
57
 
58
- def bot(history: list):
59
- messages = history_to_chat_messages(history)
60
 
61
- history.append(
62
- {
63
- "role": "assistant",
64
- "content": "",
65
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  )
67
 
68
- runtime_text = "Phase: idle"
69
 
70
- for event in stream_chat_events(messages):
71
- runtime_text = format_runtime(event)
 
 
 
72
 
73
- if event.text_delta:
74
- history[-1]["content"] += event.text_delta
75
 
76
- yield history, runtime_text
 
 
 
 
77
 
 
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- def build_app() -> gr.Blocks:
81
- with gr.Blocks() as demo:
82
- gr.Markdown("# OpenCortex Minimal Chat")
83
 
84
- with gr.Row():
85
- with gr.Column(scale=2):
86
- chatbot = gr.Chatbot(
87
- height=480,
88
- )
89
- msg = gr.Textbox(
90
- placeholder="Ask the local model...",
91
- show_label=False,
92
- )
93
- clear = gr.Button("Clear")
94
-
95
- with gr.Column(scale=1):
96
- runtime = gr.Textbox(
97
- label="Runtime",
98
- value="Phase: idle",
99
- lines=10,
100
- interactive=False,
101
- )
102
 
103
 
104
- msg.submit(
105
- user,
106
- [msg, chatbot],
107
- [msg, chatbot],
108
- queue=False,
109
- ).then(
110
- bot,
111
- chatbot,
112
- [chatbot, runtime],
113
- )
114
 
115
- clear.click(
116
- lambda: ([], "Phase: idle"),
117
- None,
118
- [chatbot, runtime],
119
- queue=False,
120
- )
121
- return demo
122
 
 
 
 
 
 
 
 
 
 
123
 
124
- def main() -> None:
125
- demo = build_app()
126
- demo.queue()
127
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
 
131
- if __name__ == "__main__":
132
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
 
134
 
135
 
 
136
 
137
 
 
 
 
 
 
 
 
 
138
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ import time
7
+ from collections.abc import Iterator
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import httpx
12
+ import uvicorn
13
+ from fastapi import FastAPI, Request
14
+ from fastapi.responses import HTMLResponse, StreamingResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
  from open_cortex.runtime.client import stream_chat_events
18
+ from open_cortex.runtime.events import RuntimeEvent
19
+ from open_cortex.runtime.messages import ChatMessage
20
+ from open_cortex.runtime.metrics import RuntimeSnapshot
21
 
22
 
23
+ ASSET_DIR = Path(__file__).with_name("assets")
24
+ CSS_FILE = ASSET_DIR / "open_cortex.css"
25
+ JS_FILE = ASSET_DIR / "open_cortex.js"
26
+ MAX_COLLAPSE_RETAINED_CHARS = 3000
27
+ COLLAPSED_MESSAGE_CHARS = 1200
28
+ DEFAULT_CONTEXT_SIZE = 2048
29
 
 
 
 
 
 
 
 
30
 
31
+ def _backend_mode() -> str:
32
+ explicit = os.getenv("OPEN_CORTEX_BACKEND")
33
+ if explicit:
34
+ return explicit
35
+ if os.getenv("SPACE_ID"):
36
+ return "simulated"
37
+ return "llama_cpp"
 
 
 
 
 
 
38
 
 
 
 
 
 
 
 
 
 
 
39
 
40
+ def _simulator_delay_seconds() -> float:
41
+ return float(os.getenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0.025"))
42
 
43
 
44
+ def _asset_version() -> int:
45
+ return int(max(CSS_FILE.stat().st_mtime, JS_FILE.stat().st_mtime))
46
 
 
 
 
47
 
48
+ def _memory_blocks() -> str:
49
+ return "\n".join(
50
+ '<i class="filled"></i>' if index < 1 else "<i></i>"
51
+ for index in range(12)
52
+ )
 
53
 
 
 
54
 
55
+ def _token_particles() -> str:
56
+ return "\n".join('<i class="particle"></i>' for _ in range(9))
57
+
58
+
59
+ def _latency_trace() -> str:
60
+ return "\n".join("<i></i>" for _ in range(8))
61
+
62
+
63
+ def render_index() -> str:
64
+ asset_version = _asset_version()
65
+ return f"""<!doctype html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
70
+ <title>OpenCortex Runtime Observatory</title>
71
+ <link rel="stylesheet" href="/assets/open_cortex.css?v={asset_version}">
72
+ </head>
73
+ <body>
74
+ <main class="app phase-idle" id="app">
75
+ <header class="topbar">
76
+ <div class="brand">
77
+ <div class="mark"></div>
78
+ <div class="brand-name">OpenCortex</div>
79
+ </div>
80
+ <div class="top-actions">
81
+ <div class="runtime-connection"><span class="live-dot"></span><span>llama.cpp · local</span></div>
82
+ </div>
83
+ </header>
84
+
85
+ <div class="workspace">
86
+ <section class="surface conversation">
87
+ <div class="section-head">
88
+ <div class="panel-title">Conversation</div>
89
+ <div class="scenario" id="scenario-label">Live local chat</div>
90
+ </div>
91
+
92
+ <div class="messages" id="messages"></div>
93
+
94
+ <div class="composer-wrap">
95
+ <div class="composer">
96
+ <textarea id="prompt" placeholder="Ask the local model, then watch inference state change..."></textarea>
97
+ <div class="composer-actions">
98
+ <span class="hint">↵ SEND · SHIFT + ↵ NEW LINE</span>
99
+ <button class="send" id="send" type="button"><span>Send</span><span>↗</span></button>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </section>
104
+
105
+ <div class="resizer" id="resizer" role="separator" aria-label="Resize conversation panel" aria-orientation="vertical">
106
+ <span class="resize-grip"></span>
107
+ <button class="drawer-toggle" id="drawer-toggle" type="button" aria-label="Show conversation">›</button>
108
+ </div>
109
+
110
+ <section class="surface observatory">
111
+ <div class="observatory-head">
112
+ <div class="panel-title">Runtime Observatory</div>
113
+ <div class="model-state">
114
+ <div class="model-name">Qwen2.5 1.5B · Q4_K_M</div>
115
+ <div class="phase-pill" id="phase">IDLE</div>
116
+ <div class="controls">
117
+ <select id="experiment" aria-label="Runtime experiment">
118
+ <option value="live">Live detected</option>
119
+ <option value="long-context">Sim · Long context stress</option>
120
+ <option value="memory-pressure">Sim · Memory pressure</option>
121
+ <option value="slow-decode">Sim · Slow decode</option>
122
+ <option value="context-collapse">Sim · Context collapse</option>
123
+ </select>
124
+ <button class="run-experiment" id="run-experiment" type="button">Run experiment</button>
125
+ </div>
126
+ </div>
127
+ </div>
128
+
129
+ <div class="observatory-stage">
130
+ <div class="runtime-event" id="runtime-event">READY</div>
131
+ <span class="conduit c1"></span>
132
+ <span class="conduit c2"></span>
133
+ <span class="conduit c3"></span>
134
+ <span class="conduit c4"></span>
135
+
136
+ <article class="organ memory active" id="memory-organ">
137
+ <div class="organ-head">
138
+ <div class="organ-name">Working Memory</div>
139
+ <span class="organ-state" id="memory-state">Memory quiet</span>
140
+ </div>
141
+ <div class="metric-row">
142
+ <div><span class="metric-value" id="kv">—</span><span class="metric-unit">%</span></div>
143
+ </div>
144
+ <div class="memory-blocks" id="memory-blocks">
145
+ {_memory_blocks()}
146
+ </div>
147
+ </article>
148
+
149
+ <article class="organ context active" id="context-organ">
150
+ <div class="organ-head">
151
+ <div class="organ-name">Context Window</div>
152
+ <span class="organ-state" id="context-state">Context resting</span>
153
+ </div>
154
+ <div class="metric-row">
155
+ <div><span class="metric-value" id="context-used">0</span><span class="metric-unit" id="context-unit">/ —</span></div>
156
+ </div>
157
+ <div class="context-vessel">
158
+ <div class="context-fill" id="context-fill"></div>
159
+ <div class="memory-tape" aria-hidden="true">
160
+ <i class="memory-segment old"></i><i class="memory-segment old"></i>
161
+ <i class="memory-segment"></i><i class="memory-segment"></i>
162
+ <i class="memory-segment"></i><i class="memory-segment"></i>
163
+ <i class="memory-segment recent"></i><i class="memory-segment recent"></i>
164
+ </div>
165
+ <div class="context-ticks"></div>
166
+ <span class="tape-direction">→</span>
167
+ </div>
168
+ </article>
169
+
170
+ <article class="organ tokens active" id="tokens-organ">
171
+ <div class="organ-head">
172
+ <div class="organ-name">Token Stream</div>
173
+ <span class="organ-state" id="token-state">Stream dormant</span>
174
+ </div>
175
+ <div class="metric-row">
176
+ <div><span class="metric-value" id="tps">0.0</span><span class="metric-unit">tok/s</span></div>
177
+ </div>
178
+ <div class="token-river">
179
+ {_token_particles()}
180
+ </div>
181
+ </article>
182
+
183
+ <article class="organ health active" id="engine-organ">
184
+ <div class="organ-head">
185
+ <div class="organ-name">Engine State</div>
186
+ <span class="organ-state" id="health-state">Engine healthy</span>
187
+ </div>
188
+ <div class="metric-row">
189
+ <div><span class="metric-value" id="ttft">—</span><span class="metric-unit">ms</span></div>
190
+ </div>
191
+ <div class="latency-trace">{_latency_trace()}</div>
192
+ </article>
193
+
194
+ <div class="core-wrap">
195
+ <div class="core-rings"></div>
196
+ <div class="core"></div>
197
+ </div>
198
+ <div class="core-copy">
199
+ <div class="core-label">Cortex Core</div>
200
+ <div class="core-state" id="core-state">Ready for input</div>
201
+ <div class="core-detail" id="core-detail">ENGINE IDLE · SLOT AVAILABLE</div>
202
+ </div>
203
+ </div>
204
+
205
+ <div class="evidence">
206
+ <div class="evidence-cell">
207
+ <div class="evidence-head"><span class="measured-dot"></span><span>Measured live by llama.cpp</span></div>
208
+ </div>
209
+ <div class="evidence-cell">
210
+ <div class="evidence-label">Prompt eval</div>
211
+ <div class="evidence-value" id="prompt-eval">measuring</div>
212
+ </div>
213
+ <div class="evidence-cell">
214
+ <div class="evidence-label" id="evidence-3-label">KV evidence</div>
215
+ <div class="evidence-value" id="kv-retained">unavailable</div>
216
+ </div>
217
+ </div>
218
+ </section>
219
+ </div>
220
+ </main>
221
+ <script type="module" src="/assets/open_cortex.js?v={asset_version}"></script>
222
+ </body>
223
+ </html>
224
+ """
225
+
226
+
227
+ def _snapshot_to_payload(snapshot: RuntimeSnapshot | None) -> dict[str, Any] | None:
228
+ if snapshot is None:
229
+ return None
230
+ return {
231
+ "prompt_tps": snapshot.prompt_tps,
232
+ "decode_tps": snapshot.decode_tps,
233
+ "requests_processing": snapshot.requests_processing,
234
+ "requests_deferred": snapshot.requests_deferred,
235
+ "active_slots": snapshot.active_slots,
236
+ "slot_context_tokens": list(snapshot.slot_context_tokens),
237
+ "slot_context_size": snapshot.slot_context_size,
238
+ }
239
+
240
+
241
+ def event_to_payload(event: RuntimeEvent) -> dict[str, Any]:
242
+ return {
243
+ "kind": event.kind,
244
+ "text_delta": event.text_delta,
245
+ "ttft_ms": event.ttft_ms,
246
+ "snapshot": _snapshot_to_payload(event.snapshot),
247
+ "generated_tokens": event.generated_tokens,
248
+ "elapsed_ms": event.elapsed_ms,
249
+ "live_tps": event.live_tps,
250
+ "repetition_detected": event.repetition_detected,
251
+ "context_tokens": event.context_tokens,
252
+ "context_size": event.context_size,
253
+ "working_memory_percent": event.working_memory_percent,
254
+ "prompt_tokens": event.prompt_tokens,
255
+ "completion_tokens": event.completion_tokens,
256
+ "prompt_tps": event.prompt_tps,
257
+ "decode_tps": event.decode_tps,
258
+ }
259
+
260
+
261
+ def _http_error_text(exc: httpx.HTTPStatusError) -> str:
262
+ try:
263
+ return exc.response.text
264
+ except httpx.ResponseNotRead:
265
+ try:
266
+ exc.response.read()
267
+ except (httpx.HTTPError, httpx.StreamError):
268
+ return ""
269
+ return exc.response.text
270
+
271
+
272
+ def _is_context_overflow(exc: httpx.HTTPStatusError) -> bool:
273
+ return (
274
+ exc.response.status_code == 400
275
+ and "exceeds the available context size" in _http_error_text(exc)
276
  )
277
 
 
278
 
279
+ def _context_size_from_error(exc: httpx.HTTPStatusError) -> int | None:
280
+ match = re.search(r"context size \((\d+) tokens\)", _http_error_text(exc))
281
+ if match is None:
282
+ return None
283
+ return int(match.group(1))
284
 
 
 
285
 
286
+ def _trim_for_context_collapse(
287
+ messages: list[ChatMessage],
288
+ ) -> tuple[list[ChatMessage], int]:
289
+ system_messages = [message for message in messages if message.role == "system"]
290
+ conversation = [message for message in messages if message.role != "system"]
291
 
292
+ if not conversation:
293
+ return messages, 0
294
 
295
+ latest_user_index = next(
296
+ (
297
+ index
298
+ for index in range(len(conversation) - 1, -1, -1)
299
+ if conversation[index].role == "user"
300
+ ),
301
+ len(conversation) - 1,
302
+ )
303
+ latest_user = conversation[latest_user_index]
304
+ recent_start = max(1, len(conversation) - 3)
305
+ recent = conversation[recent_start:]
306
+
307
+ retained: list[ChatMessage] = []
308
+ for message in recent:
309
+ if message is latest_user:
310
+ retained.append(message)
311
+ continue
312
+ if len(message.content) > MAX_COLLAPSE_RETAINED_CHARS:
313
+ retained.append(
314
+ ChatMessage(
315
+ role=message.role,
316
+ content=(
317
+ "[Earlier content collapsed; recent tail retained.]\n"
318
+ + message.content[-COLLAPSED_MESSAGE_CHARS:]
319
+ ),
320
+ )
321
+ )
322
+ else:
323
+ retained.append(message)
324
+
325
+ if latest_user not in retained:
326
+ retained.append(latest_user)
327
+
328
+ retained = list(dict.fromkeys(retained))
329
+ dropped = len(conversation) - len(retained)
330
+ return [*system_messages, *retained], max(0, dropped)
331
+
332
+
333
+ def _parse_messages(raw_messages: Any) -> list[ChatMessage]:
334
+ if not isinstance(raw_messages, list):
335
+ return []
336
+
337
+ messages: list[ChatMessage] = []
338
+ for raw_message in raw_messages:
339
+ if not isinstance(raw_message, dict):
340
+ continue
341
+ role = raw_message.get("role")
342
+ content = raw_message.get("content")
343
+ if role not in {"system", "user", "assistant"}:
344
+ continue
345
+ if not isinstance(content, str) or not content.strip():
346
+ continue
347
+ messages.append(ChatMessage(role=role, content=content.strip()))
348
+
349
+ return messages
350
+
351
+
352
+ def _latest_user_text(messages: list[ChatMessage]) -> str:
353
+ for message in reversed(messages):
354
+ if message.role == "user":
355
+ return message.content
356
+ return ""
357
+
358
+
359
+ def _simulated_answer(messages: list[ChatMessage]) -> str:
360
+ latest = _latest_user_text(messages).lower()
361
+ if any(marker in latest for marker in ("story", "故事", "repeat", "loop")):
362
+ phrase = (
363
+ "A tiny local model begins exploring a hidden runtime chamber. "
364
+ "It finds the same sentence again, and the same sentence again. "
365
+ )
366
+ return (
367
+ "I can feel the decode rhythm becoming unstable.\n\n"
368
+ + phrase * 8
369
+ + "\n\nOpenCortex marks this as a thought loop because recent generated "
370
+ "text is repeating instead of moving forward."
371
+ )
372
+ if any(marker in latest for marker in ("context", "window", "memory", "上下文", "记忆")):
373
+ return (
374
+ "OpenCortex treats the context window as the model's active workspace. "
375
+ "As the conversation grows, more tokens must be prefetched before decode "
376
+ "can begin. When the active window fills, older turns are still visible in "
377
+ "the chat log, but they fall outside what the model can reliably use."
378
+ )
379
+ return (
380
+ "OpenCortex is running in Space demo mode. It simulates the same runtime "
381
+ "event stream used by the local llama.cpp integration: prefill, first token, "
382
+ "decode throughput, context pressure, and completion. Run it locally with "
383
+ "llama.cpp to replace this simulated stream with live engine evidence."
384
+ )
385
 
 
 
 
386
 
387
+ def _token_chunks(text: str) -> list[str]:
388
+ chunks = re.findall(r"\S+\s*", text)
389
+ return chunks or [text]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
 
391
 
392
+ def _stream_simulated_events(messages: list[ChatMessage]) -> Iterator[RuntimeEvent]:
393
+ answer = _simulated_answer(messages)
394
+ chunks = _token_chunks(answer)
395
+ context_size = DEFAULT_CONTEXT_SIZE
396
+ prompt_tokens = max(24, sum(len(message.content) for message in messages) // 3)
397
+ base_context_tokens = min(context_size, prompt_tokens + 96)
398
+ delay_seconds = _simulator_delay_seconds()
 
 
 
399
 
400
+ yield RuntimeEvent(kind="request_started", text_delta="", ttft_ms=None, snapshot=None)
 
 
 
 
 
 
401
 
402
+ snapshot = RuntimeSnapshot(
403
+ prompt_tps=72.4,
404
+ decode_tps=24.6,
405
+ requests_processing=1,
406
+ requests_deferred=0,
407
+ active_slots=1,
408
+ slot_context_tokens=(base_context_tokens,),
409
+ slot_context_size=context_size,
410
+ )
411
 
412
+ generated_text = ""
413
+ started_at = time.perf_counter()
414
+ for index, chunk in enumerate(chunks, start=1):
415
+ if delay_seconds:
416
+ time.sleep(delay_seconds)
417
+ generated_text += chunk
418
+ context_tokens = min(context_size, base_context_tokens + index)
419
+ event_kind = "first_token" if index == 1 else "token"
420
+ elapsed_ms = max(0.0, (time.perf_counter() - started_at) * 1000)
421
+ live_tps = None if index == 1 or elapsed_ms == 0 else index / (elapsed_ms / 1000)
422
+ repetition_detected = "same sentence again" in generated_text and index > 18
423
+
424
+ yield RuntimeEvent(
425
+ kind=event_kind,
426
+ text_delta=chunk,
427
+ ttft_ms=420.0 if index == 1 else None,
428
+ snapshot=snapshot if index == 1 else None,
429
+ generated_tokens=index,
430
+ elapsed_ms=0.0 if index == 1 else elapsed_ms,
431
+ live_tps=live_tps,
432
+ repetition_detected=repetition_detected,
433
+ context_tokens=context_tokens,
434
+ context_size=context_size,
435
+ working_memory_percent=round(min(100.0, context_tokens / context_size * 100), 1),
436
+ )
437
 
438
+ final_context_tokens = min(context_size, base_context_tokens + len(chunks))
439
+ yield RuntimeEvent(
440
+ kind="request_completed",
441
+ text_delta="",
442
+ ttft_ms=None,
443
+ snapshot=None,
444
+ generated_tokens=len(chunks),
445
+ repetition_detected="same sentence again" in generated_text,
446
+ context_tokens=final_context_tokens,
447
+ context_size=context_size,
448
+ working_memory_percent=round(min(100.0, final_context_tokens / context_size * 100), 1),
449
+ prompt_tokens=prompt_tokens,
450
+ completion_tokens=len(chunks),
451
+ prompt_tps=72.4,
452
+ decode_tps=24.6,
453
+ )
454
 
455
 
456
+ def _stream_events(messages: list[ChatMessage]) -> Iterator[str]:
457
+ if _backend_mode() == "simulated":
458
+ for event in _stream_simulated_events(messages):
459
+ yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
460
+ return
461
+
462
+ try:
463
+ for event in stream_chat_events(messages):
464
+ yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
465
+ except httpx.HTTPStatusError as exc:
466
+ if _is_context_overflow(exc):
467
+ trimmed_messages, dropped_messages = _trim_for_context_collapse(messages)
468
+ yield json.dumps(
469
+ {
470
+ "kind": "context_collapse",
471
+ "message": (
472
+ "The earliest turns fell outside the active context. "
473
+ "OpenCortex trimmed old history and retried with recent memory."
474
+ ),
475
+ "dropped_messages": dropped_messages,
476
+ "retained_messages": len(trimmed_messages),
477
+ "context_size": _context_size_from_error(exc),
478
+ },
479
+ ensure_ascii=False,
480
+ ) + "\n"
481
+ try:
482
+ for event in stream_chat_events(trimmed_messages):
483
+ yield json.dumps(event_to_payload(event), ensure_ascii=False) + "\n"
484
+ return
485
+ except httpx.HTTPStatusError as retry_exc:
486
+ if _is_context_overflow(retry_exc):
487
+ payload = {
488
+ "kind": "error",
489
+ "code": "context_overflow",
490
+ "message": (
491
+ "The conversation still exceeds llama.cpp's context window "
492
+ "after collapse. Start a new run or restart llama-server with "
493
+ "a larger -c value."
494
+ ),
495
+ }
496
+ else:
497
+ payload = {
498
+ "kind": "error",
499
+ "code": "backend_http_error",
500
+ "message": f"{type(retry_exc).__name__}: {retry_exc}",
501
+ }
502
+ else:
503
+ payload = {
504
+ "kind": "error",
505
+ "code": "backend_http_error",
506
+ "message": f"{type(exc).__name__}: {exc}",
507
+ }
508
+ yield json.dumps(payload, ensure_ascii=False) + "\n"
509
+ except Exception as exc:
510
+ yield json.dumps(
511
+ {
512
+ "kind": "error",
513
+ "code": "backend_error",
514
+ "message": f"{type(exc).__name__}: {exc}",
515
+ },
516
+ ensure_ascii=False,
517
+ ) + "\n"
518
+
519
+
520
+ def create_app() -> FastAPI:
521
+ app = FastAPI(title="OpenCortex")
522
+ app.mount("/assets", StaticFiles(directory=ASSET_DIR), name="assets")
523
+
524
+ @app.get("/", response_class=HTMLResponse)
525
+ def index() -> str:
526
+ return render_index()
527
+
528
+ @app.post("/api/chat")
529
+ async def chat(request: Request) -> StreamingResponse:
530
+ payload = await request.json()
531
+ messages = _parse_messages(payload.get("messages"))
532
+ return StreamingResponse(
533
+ _stream_events(messages),
534
+ media_type="application/x-ndjson",
535
+ )
536
 
537
+ return app
538
 
539
 
540
+ app = create_app()
541
 
542
 
543
+ def main() -> None:
544
+ host = os.getenv("OPEN_CORTEX_HOST", "0.0.0.0" if os.getenv("SPACE_ID") else "127.0.0.1")
545
+ uvicorn.run(
546
+ "open_cortex.ui.app:app",
547
+ host=host,
548
+ port=7860,
549
+ reload=False,
550
+ )
551
 
552
+
553
+ if __name__ == "__main__":
554
+ main()
open_cortex/ui/assets/open_cortex.css ADDED
@@ -0,0 +1,1410 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: #06080d;
3
+ --panel: rgba(12, 16, 24, .82);
4
+ --panel-2: rgba(15, 21, 31, .76);
5
+ --line: rgba(151, 176, 211, .13);
6
+ --line-hot: rgba(84, 216, 255, .38);
7
+ --text: #e8eef7;
8
+ --muted: #7f8b9d;
9
+ --quiet: #536071;
10
+ --cyan: #53d7f7;
11
+ --cyan-2: #3aa7d2;
12
+ --teal: #64e8c2;
13
+ --yellow: #e7cf72;
14
+ --orange: #f2a35d;
15
+ --red: #f06b79;
16
+ --mono: "SFMono-Regular", "Cascadia Code", "Roboto Mono", Consolas, monospace;
17
+ --sans: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
18
+ }
19
+
20
+ * { box-sizing: border-box; }
21
+
22
+ html, body {
23
+ margin: 0;
24
+ min-width: 1180px;
25
+ width: 100%;
26
+ height: 100%;
27
+ min-height: 100dvh;
28
+ color: var(--text);
29
+ background:
30
+ radial-gradient(circle at 72% 38%, rgba(26, 111, 142, .075), transparent 34%),
31
+ var(--bg);
32
+ font-family: var(--sans);
33
+ overflow: hidden;
34
+ }
35
+
36
+ body::before {
37
+ content: "";
38
+ position: fixed;
39
+ inset: 0;
40
+ pointer-events: none;
41
+ opacity: .18;
42
+ background-image:
43
+ linear-gradient(rgba(132, 164, 204, .025) 1px, transparent 1px),
44
+ linear-gradient(90deg, rgba(132, 164, 204, .025) 1px, transparent 1px);
45
+ background-size: 32px 32px;
46
+ mask-image: linear-gradient(to bottom, black, transparent 88%);
47
+ }
48
+
49
+ button, select, textarea { font: inherit; }
50
+ button { color: inherit; }
51
+
52
+ .app {
53
+ --chat-width: 40%;
54
+ position: relative;
55
+ height: 100vh;
56
+ height: 100dvh;
57
+ min-height: 100dvh;
58
+ padding: 12px;
59
+ display: grid;
60
+ grid-template-rows: 44px minmax(0, 1fr);
61
+ gap: 8px;
62
+ }
63
+
64
+ .topbar {
65
+ grid-row: 1;
66
+ position: relative;
67
+ z-index: 50;
68
+ min-height: 44px;
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ padding: 0 13px;
73
+ border: 1px solid var(--line);
74
+ border-radius: 12px;
75
+ background: rgba(9, 12, 18, .74);
76
+ backdrop-filter: blur(22px);
77
+ }
78
+
79
+ .brand, .top-actions, .runtime-connection, .scenario-meta, .metric-row,
80
+ .composer-actions, .model-state, .organ-head, .evidence-head {
81
+ display: flex;
82
+ align-items: center;
83
+ }
84
+
85
+ .brand { gap: 11px; }
86
+ .mark {
87
+ position: relative;
88
+ width: 25px;
89
+ height: 25px;
90
+ border: 1px solid rgba(91, 219, 250, .55);
91
+ border-radius: 8px;
92
+ background: radial-gradient(circle, rgba(83, 215, 247, .28), transparent 68%);
93
+ }
94
+ .mark::before, .mark::after {
95
+ content: "";
96
+ position: absolute;
97
+ inset: 5px;
98
+ border: 1px solid rgba(91, 219, 250, .5);
99
+ border-radius: 50%;
100
+ }
101
+ .mark::after {
102
+ inset: 10px;
103
+ border: 0;
104
+ background: var(--cyan);
105
+ box-shadow: 0 0 12px var(--cyan);
106
+ }
107
+
108
+ .brand-name { font-weight: 650; letter-spacing: -.02em; }
109
+ .top-actions { gap: 8px; }
110
+ .runtime-connection {
111
+ gap: 7px;
112
+ color: #79879a;
113
+ font: 9px var(--mono);
114
+ }
115
+ .live-dot {
116
+ width: 6px;
117
+ height: 6px;
118
+ border-radius: 50%;
119
+ background: var(--teal);
120
+ box-shadow: 0 0 8px var(--teal);
121
+ }
122
+ .icon-btn {
123
+ height: 28px;
124
+ padding: 0 10px;
125
+ color: #9ca8b8;
126
+ border: 1px solid var(--line);
127
+ border-radius: 7px;
128
+ background: rgba(255, 255, 255, .018);
129
+ cursor: pointer;
130
+ }
131
+ .icon-btn:hover { color: white; border-color: rgba(151, 176, 211, .28); }
132
+
133
+ .workspace {
134
+ grid-row: 2;
135
+ height: 100%;
136
+ min-height: 0;
137
+ display: grid;
138
+ grid-template-columns: var(--chat-width) 8px minmax(0, 1fr);
139
+ gap: 0;
140
+ transition: grid-template-columns .24s ease;
141
+ }
142
+ .workspace:has(.resizer.dragging) {
143
+ transition: none;
144
+ }
145
+
146
+ .surface {
147
+ height: 100%;
148
+ min-height: 0;
149
+ border: 1px solid var(--line);
150
+ border-radius: 14px;
151
+ background: var(--panel);
152
+ backdrop-filter: blur(20px);
153
+ overflow: hidden;
154
+ }
155
+
156
+ .conversation {
157
+ display: grid;
158
+ grid-template-rows: auto minmax(0, 1fr) auto;
159
+ transition: opacity .18s ease, transform .24s ease, border-color .18s ease;
160
+ }
161
+ .app.chat-collapsed {
162
+ --chat-width: 0px;
163
+ }
164
+ .app.chat-collapsed .conversation {
165
+ opacity: 0;
166
+ transform: translateX(-18px);
167
+ pointer-events: none;
168
+ border-color: transparent;
169
+ }
170
+ .app.collapse-preview .conversation {
171
+ opacity: .28;
172
+ transform: translateX(-8px);
173
+ }
174
+ .resizer {
175
+ position: relative;
176
+ z-index: 20;
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ cursor: col-resize;
181
+ touch-action: none;
182
+ }
183
+ .resizer::before {
184
+ content: "";
185
+ width: 1px;
186
+ height: calc(100% - 24px);
187
+ background: rgba(151,176,211,.12);
188
+ transition: width .15s, background .15s;
189
+ }
190
+ .resizer:hover::before,
191
+ .resizer.dragging::before {
192
+ width: 2px;
193
+ background: rgba(83,215,247,.5);
194
+ }
195
+ .resize-grip {
196
+ position: absolute;
197
+ width: 4px;
198
+ height: 34px;
199
+ border-radius: 99px;
200
+ background: #202a38;
201
+ box-shadow: 0 0 0 1px rgba(151,176,211,.08);
202
+ }
203
+ .drawer-toggle {
204
+ position: absolute;
205
+ top: 14px;
206
+ width: 22px;
207
+ height: 22px;
208
+ padding: 0;
209
+ border: 1px solid rgba(151,176,211,.16);
210
+ border-radius: 6px;
211
+ color: #7f8c9d;
212
+ background: #0b1018;
213
+ cursor: pointer;
214
+ opacity: 0;
215
+ transition: opacity .15s, color .15s;
216
+ }
217
+ .app.chat-collapsed .drawer-toggle {
218
+ opacity: 1;
219
+ color: var(--cyan);
220
+ }
221
+
222
+ .section-head {
223
+ min-height: 52px;
224
+ padding: 0 15px;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ border-bottom: 1px solid var(--line);
229
+ }
230
+ .eyebrow {
231
+ color: #708094;
232
+ font: 9px var(--mono);
233
+ letter-spacing: .14em;
234
+ text-transform: uppercase;
235
+ }
236
+ .panel-title {
237
+ font-size: 13px;
238
+ font-weight: 590;
239
+ letter-spacing: -.01em;
240
+ }
241
+ .scenario {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 7px;
245
+ padding: 6px 8px;
246
+ color: #aeb9c7;
247
+ border: 1px solid rgba(151, 176, 211, .13);
248
+ border-radius: 6px;
249
+ background: rgba(255, 255, 255, .018);
250
+ font-size: 11px;
251
+ }
252
+ .scenario::before {
253
+ content: "";
254
+ width: 5px;
255
+ height: 5px;
256
+ border-radius: 50%;
257
+ background: var(--cyan);
258
+ box-shadow: 0 0 7px rgba(83, 215, 247, .45);
259
+ }
260
+
261
+ .messages {
262
+ overflow-y: auto;
263
+ padding: 18px 18px 10px;
264
+ scrollbar-width: thin;
265
+ scrollbar-color: #263244 transparent;
266
+ }
267
+ .message {
268
+ max-width: 88%;
269
+ margin-bottom: 17px;
270
+ }
271
+ .message.user { margin-left: auto; }
272
+ .message-meta {
273
+ margin-bottom: 6px;
274
+ color: #667386;
275
+ font: 9px var(--mono);
276
+ letter-spacing: .08em;
277
+ text-transform: uppercase;
278
+ }
279
+ .user .message-meta { text-align: right; }
280
+ .bubble {
281
+ padding: 11px 13px;
282
+ border: 1px solid var(--line);
283
+ border-radius: 11px;
284
+ color: #cbd5e2;
285
+ background: rgba(255, 255, 255, .025);
286
+ font-size: 12px;
287
+ line-height: 1.6;
288
+ }
289
+ .user .bubble {
290
+ color: #dcf7ff;
291
+ border-color: rgba(83, 215, 247, .18);
292
+ background: rgba(40, 128, 158, .1);
293
+ }
294
+ .assistant-live .bubble {
295
+ border-color: rgba(83, 215, 247, .2);
296
+ background: linear-gradient(135deg, rgba(42, 108, 137, .08), rgba(255,255,255,.018));
297
+ }
298
+ .markdown-body p,
299
+ .markdown-body ul,
300
+ .markdown-body pre,
301
+ .markdown-body h1,
302
+ .markdown-body h2,
303
+ .markdown-body h3 {
304
+ margin: 0 0 10px;
305
+ }
306
+ .markdown-body :last-child { margin-bottom: 0; }
307
+ .markdown-body h1,
308
+ .markdown-body h2,
309
+ .markdown-body h3 {
310
+ color: #e8eef7;
311
+ font-weight: 650;
312
+ letter-spacing: -.01em;
313
+ line-height: 1.35;
314
+ }
315
+ .markdown-body h1 { font-size: 17px; }
316
+ .markdown-body h2 { font-size: 15px; }
317
+ .markdown-body h3 { font-size: 13px; }
318
+ .markdown-body ul {
319
+ padding-left: 17px;
320
+ }
321
+ .markdown-body li {
322
+ margin: 4px 0;
323
+ }
324
+ .markdown-body code {
325
+ padding: 1px 5px;
326
+ border: 1px solid rgba(151,176,211,.14);
327
+ border-radius: 5px;
328
+ color: #d9f8ff;
329
+ background: rgba(3, 8, 14, .5);
330
+ font: 11px var(--mono);
331
+ }
332
+ .markdown-body pre {
333
+ overflow-x: auto;
334
+ padding: 10px 11px;
335
+ border: 1px solid rgba(151,176,211,.12);
336
+ border-radius: 9px;
337
+ background: rgba(3, 8, 14, .62);
338
+ }
339
+ .markdown-body pre code {
340
+ padding: 0;
341
+ border: 0;
342
+ background: transparent;
343
+ white-space: pre;
344
+ }
345
+ .cursor {
346
+ display: inline-block;
347
+ width: 5px;
348
+ height: 13px;
349
+ margin-left: 3px;
350
+ vertical-align: -2px;
351
+ background: var(--cyan);
352
+ animation: blink .82s step-end infinite;
353
+ }
354
+ @keyframes blink { 50% { opacity: 0; } }
355
+
356
+ .composer-wrap {
357
+ padding: 10px;
358
+ border-top: 1px solid var(--line);
359
+ background: rgba(7, 10, 16, .66);
360
+ }
361
+ .composer {
362
+ padding: 10px;
363
+ border: 1px solid rgba(151, 176, 211, .18);
364
+ border-radius: 10px;
365
+ background: rgba(16, 22, 32, .72);
366
+ }
367
+ textarea {
368
+ display: block;
369
+ width: 100%;
370
+ height: 42px;
371
+ resize: none;
372
+ border: 0;
373
+ outline: 0;
374
+ color: #dce5ef;
375
+ background: transparent;
376
+ font-size: 12px;
377
+ }
378
+ textarea::placeholder { color: #596577; }
379
+ .composer-actions { justify-content: space-between; }
380
+ .hint { color: #566273; font: 9px var(--mono); }
381
+ .send {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 7px;
385
+ height: 29px;
386
+ padding: 0 11px;
387
+ border: 1px solid rgba(91, 219, 250, .38);
388
+ border-radius: 7px;
389
+ color: #dcf8ff;
390
+ background: rgba(46, 155, 190, .13);
391
+ cursor: pointer;
392
+ }
393
+ .send:hover { background: rgba(46, 155, 190, .2); }
394
+
395
+ .observatory {
396
+ display: grid;
397
+ grid-template-rows: auto minmax(0, 1fr) auto;
398
+ background:
399
+ radial-gradient(circle at 49% 47%, rgba(32, 132, 168, .09), transparent 39%),
400
+ rgba(8, 12, 19, .84);
401
+ }
402
+ .observatory-head {
403
+ min-height: 52px;
404
+ padding: 0 14px;
405
+ display: flex;
406
+ justify-content: space-between;
407
+ align-items: center;
408
+ border-bottom: 1px solid var(--line);
409
+ }
410
+ .model-state { gap: 8px; }
411
+ .model-name {
412
+ color: #aab5c4;
413
+ font: 10px var(--mono);
414
+ }
415
+ .phase-pill {
416
+ min-width: 82px;
417
+ padding: 5px 8px;
418
+ border: 1px solid rgba(83, 215, 247, .25);
419
+ border-radius: 99px;
420
+ color: var(--cyan);
421
+ background: rgba(83, 215, 247, .06);
422
+ font: 9px var(--mono);
423
+ text-align: center;
424
+ letter-spacing: .11em;
425
+ text-transform: uppercase;
426
+ }
427
+
428
+ .observatory-stage {
429
+ position: relative;
430
+ min-height: 0;
431
+ overflow: hidden;
432
+ }
433
+ .observatory-stage::before,
434
+ .observatory-stage::after {
435
+ content: "";
436
+ position: absolute;
437
+ pointer-events: none;
438
+ }
439
+ .observatory-stage::before {
440
+ inset: 9% 18%;
441
+ border: 1px solid rgba(91, 219, 250, .06);
442
+ border-radius: 50%;
443
+ }
444
+ .observatory-stage::after {
445
+ inset: 23% 32%;
446
+ border: 1px dashed rgba(91, 219, 250, .09);
447
+ border-radius: 50%;
448
+ animation: rotate 28s linear infinite;
449
+ }
450
+ @keyframes rotate { to { transform: rotate(360deg); } }
451
+
452
+ .conduit {
453
+ position: absolute;
454
+ z-index: 1;
455
+ height: 1px;
456
+ transform-origin: left center;
457
+ background: linear-gradient(90deg, rgba(83,215,247,.05), rgba(83,215,247,.28), rgba(83,215,247,.05));
458
+ }
459
+ .conduit::after {
460
+ content: "";
461
+ position: absolute;
462
+ top: -1px;
463
+ left: 0;
464
+ width: 34px;
465
+ height: 3px;
466
+ background: linear-gradient(90deg, transparent, var(--cyan), transparent);
467
+ filter: blur(.2px);
468
+ animation: signal 2.2s linear infinite;
469
+ }
470
+ @keyframes signal { from { transform: translateX(0); } to { transform: translateX(190px); } }
471
+ .c1 { left: 30%; top: 28%; width: 190px; transform: rotate(25deg); }
472
+ .c2 { left: 54%; top: 45%; width: 180px; transform: rotate(-27deg); }
473
+ .c3 { left: 30%; top: 70%; width: 190px; transform: rotate(-23deg); }
474
+ .c4 { left: 54%; top: 53%; width: 175px; transform: rotate(28deg); }
475
+
476
+ .core-wrap {
477
+ position: absolute;
478
+ z-index: 3;
479
+ left: 50%;
480
+ top: 50%;
481
+ width: 188px;
482
+ height: 188px;
483
+ transform: translate(-50%, -50%);
484
+ }
485
+ .core-rings, .core, .core::before, .core::after {
486
+ position: absolute;
487
+ border-radius: 50%;
488
+ }
489
+ .core-rings {
490
+ inset: 0;
491
+ border: 1px solid rgba(83, 215, 247, .13);
492
+ box-shadow: inset 0 0 42px rgba(83, 215, 247, .04);
493
+ animation: breathe 3s ease-in-out infinite;
494
+ }
495
+ .core-rings::before, .core-rings::after {
496
+ content: "";
497
+ position: absolute;
498
+ border-radius: 50%;
499
+ border: 1px dashed rgba(83, 215, 247, .2);
500
+ }
501
+ .core-rings::before { inset: 13px; animation: rotate 16s linear infinite; }
502
+ .core-rings::after { inset: 31px; animation: rotate 11s linear infinite reverse; }
503
+ .core {
504
+ inset: 43px;
505
+ background:
506
+ radial-gradient(circle at 36% 28%, rgba(220, 251, 255, .92), rgba(92, 219, 247, .5) 11%, rgba(27, 119, 153, .4) 29%, rgba(6, 18, 27, .9) 68%);
507
+ box-shadow:
508
+ 0 0 25px rgba(83, 215, 247, .35),
509
+ 0 0 70px rgba(23, 130, 168, .16),
510
+ inset -9px -13px 25px rgba(0, 0, 0, .6);
511
+ animation: core-pulse 1.35s ease-in-out infinite;
512
+ }
513
+ .core::before {
514
+ content: "";
515
+ inset: 9px;
516
+ border: 1px solid rgba(186, 245, 255, .23);
517
+ }
518
+ .core::after {
519
+ content: "";
520
+ left: 24px;
521
+ top: 18px;
522
+ width: 17px;
523
+ height: 9px;
524
+ background: rgba(225, 253, 255, .55);
525
+ filter: blur(5px);
526
+ transform: rotate(-28deg);
527
+ }
528
+ @keyframes breathe {
529
+ 50% { transform: scale(1.025); border-color: rgba(83, 215, 247, .24); }
530
+ }
531
+ @keyframes core-pulse {
532
+ 50% {
533
+ transform: scale(1.075);
534
+ box-shadow: 0 0 36px rgba(83, 215, 247, .48), 0 0 90px rgba(23, 130, 168, .22), inset -9px -13px 25px rgba(0,0,0,.6);
535
+ }
536
+ }
537
+ .core-copy {
538
+ position: absolute;
539
+ z-index: 4;
540
+ left: 50%;
541
+ top: calc(50% + 105px);
542
+ transform: translateX(-50%);
543
+ text-align: center;
544
+ white-space: nowrap;
545
+ }
546
+ .core-label {
547
+ color: #718195;
548
+ font: 9px var(--mono);
549
+ letter-spacing: .15em;
550
+ text-transform: uppercase;
551
+ }
552
+ .core-state {
553
+ margin-top: 5px;
554
+ color: #dffaff;
555
+ font-size: 15px;
556
+ font-weight: 590;
557
+ }
558
+ .core-detail {
559
+ margin-top: 4px;
560
+ color: #688195;
561
+ font: 9px var(--mono);
562
+ }
563
+
564
+ .organ {
565
+ position: absolute;
566
+ z-index: 5;
567
+ width: 218px;
568
+ min-height: 112px;
569
+ padding: 12px;
570
+ border: 1px solid rgba(151, 176, 211, .14);
571
+ border-radius: 12px;
572
+ background: rgba(11, 16, 24, .78);
573
+ backdrop-filter: blur(16px);
574
+ transition: border-color .3s, transform .3s, box-shadow .3s;
575
+ }
576
+ .organ.active {
577
+ border-color: rgba(151, 176, 211, .17);
578
+ box-shadow: inset 0 0 25px rgba(83, 215, 247, .012);
579
+ }
580
+ .memory { left: 3.5%; top: 8%; }
581
+ .context { right: 3.5%; top: 8%; }
582
+ .tokens { left: 3.5%; bottom: 7%; }
583
+ .health { right: 3.5%; bottom: 7%; }
584
+ .organ-head { justify-content: space-between; }
585
+ .organ-name {
586
+ color: #8f9db0;
587
+ font-size: 10px;
588
+ font-weight: 600;
589
+ letter-spacing: .025em;
590
+ }
591
+ .organ-state {
592
+ color: #8198a8;
593
+ font: 9px var(--mono);
594
+ letter-spacing: .03em;
595
+ }
596
+ .metric-row {
597
+ justify-content: space-between;
598
+ margin-top: 12px;
599
+ }
600
+ .metric-value {
601
+ font: 18px var(--mono);
602
+ letter-spacing: -.04em;
603
+ }
604
+ .metric-unit {
605
+ margin-left: 3px;
606
+ color: #637084;
607
+ font: 9px var(--mono);
608
+ }
609
+
610
+ .meter {
611
+ position: relative;
612
+ height: 5px;
613
+ margin-top: 11px;
614
+ overflow: hidden;
615
+ border-radius: 99px;
616
+ background: #1a2330;
617
+ }
618
+ .meter-fill {
619
+ width: 63%;
620
+ height: 100%;
621
+ border-radius: inherit;
622
+ background: linear-gradient(90deg, #277f9d, var(--cyan));
623
+ box-shadow: 0 0 9px rgba(83, 215, 247, .35);
624
+ transition: width .45s ease;
625
+ }
626
+ .memory-blocks {
627
+ display: grid;
628
+ grid-template-columns: repeat(6, 1fr);
629
+ grid-template-rows: repeat(2, 1fr);
630
+ gap: 3px;
631
+ height: 30px;
632
+ margin-top: 9px;
633
+ padding: 3px;
634
+ border: 1px solid rgba(151, 176, 211, .12);
635
+ border-radius: 6px;
636
+ background: rgba(3, 8, 14, .72);
637
+ }
638
+ .memory-blocks i {
639
+ position: relative;
640
+ border: 1px solid rgba(83, 215, 247, .18);
641
+ border-radius: 2px;
642
+ background: rgba(83, 215, 247, .08);
643
+ transition: opacity .35s, transform .35s, background .35s;
644
+ }
645
+ .memory-blocks i::after {
646
+ content: "";
647
+ position: absolute;
648
+ left: 3px;
649
+ right: 3px;
650
+ bottom: 2px;
651
+ height: 1px;
652
+ background: rgba(255,255,255,.1);
653
+ }
654
+ .memory-blocks i.filled {
655
+ background: rgba(83, 215, 247, .48);
656
+ box-shadow: 0 0 5px rgba(83, 215, 247, .18);
657
+ }
658
+ .context-vessel {
659
+ position: relative;
660
+ height: 34px;
661
+ margin-top: 10px;
662
+ overflow: hidden;
663
+ border: 1px solid rgba(151, 176, 211, .14);
664
+ border-radius: 7px;
665
+ background: rgba(3, 8, 14, .8);
666
+ }
667
+ .context-fill {
668
+ position: absolute;
669
+ z-index: 1;
670
+ left: 0;
671
+ top: 0;
672
+ width: 59%;
673
+ height: 100%;
674
+ background: linear-gradient(90deg, rgba(48,148,181,.08), rgba(48,148,181,.22));
675
+ border-right: 1px solid rgba(83, 215, 247, .48);
676
+ transition: width .45s ease;
677
+ }
678
+ .context-ticks {
679
+ position: absolute;
680
+ z-index: 3;
681
+ inset: 0;
682
+ background: repeating-linear-gradient(90deg, transparent 0 17px, rgba(255,255,255,.055) 18px);
683
+ }
684
+ .memory-tape {
685
+ position: absolute;
686
+ z-index: 2;
687
+ inset: 4px;
688
+ display: flex;
689
+ gap: 3px;
690
+ align-items: stretch;
691
+ }
692
+ .memory-segment {
693
+ flex: 1;
694
+ border-radius: 2px;
695
+ background: rgba(104, 205, 232, .16);
696
+ border: 1px solid rgba(104, 205, 232, .08);
697
+ transition: opacity .7s, transform .7s, filter .7s;
698
+ }
699
+ .memory-segment.old { opacity: .45; }
700
+ .memory-segment.recent {
701
+ background: rgba(83, 215, 247, .38);
702
+ box-shadow: 0 0 6px rgba(83,215,247,.15);
703
+ }
704
+ .context-forgetting .memory-segment.old {
705
+ opacity: 0;
706
+ transform: translateX(-18px) scaleX(.2);
707
+ filter: blur(3px);
708
+ }
709
+ .tape-direction {
710
+ position: absolute;
711
+ z-index: 4;
712
+ right: 5px;
713
+ top: 50%;
714
+ transform: translateY(-50%);
715
+ color: rgba(196, 242, 252, .6);
716
+ font: 9px var(--mono);
717
+ }
718
+
719
+ .token-river {
720
+ position: relative;
721
+ display: flex;
722
+ align-items: center;
723
+ gap: 5px;
724
+ height: 31px;
725
+ margin-top: 10px;
726
+ overflow: hidden;
727
+ padding: 0 7px;
728
+ border: 1px solid rgba(151,176,211,.13);
729
+ border-radius: 99px;
730
+ background: linear-gradient(180deg, rgba(83,215,247,.04), rgba(3,8,14,.84));
731
+ }
732
+ .token-river::before,
733
+ .token-river::after {
734
+ content: "";
735
+ position: absolute;
736
+ left: 5px;
737
+ right: 5px;
738
+ height: 1px;
739
+ background: linear-gradient(90deg, transparent, rgba(83,215,247,.24), transparent);
740
+ }
741
+ .token-river::before { top: 5px; }
742
+ .token-river::after { bottom: 5px; }
743
+ .particle {
744
+ flex: 0 0 auto;
745
+ width: 17px;
746
+ height: 6px;
747
+ border-radius: 2px;
748
+ background: linear-gradient(90deg, rgba(83,215,247,.17), rgba(83,215,247,.92));
749
+ box-shadow: 0 0 7px rgba(83,215,247,.2);
750
+ animation: flow 1.35s linear infinite;
751
+ }
752
+ .particle:nth-child(2n) { width: 8px; opacity: .72; animation-delay: -.5s; }
753
+ .particle:nth-child(3n) { width: 12px; opacity: .45; animation-delay: -.9s; }
754
+ @keyframes flow {
755
+ from { transform: translateX(-26px); opacity: .15; }
756
+ 25% { opacity: 1; }
757
+ to { transform: translateX(48px); opacity: .15; }
758
+ }
759
+
760
+ .latency-trace {
761
+ position: relative;
762
+ display: flex;
763
+ align-items: center;
764
+ gap: 3px;
765
+ height: 30px;
766
+ margin-top: 9px;
767
+ padding: 0 4px;
768
+ border-left: 1px solid rgba(151,176,211,.15);
769
+ border-right: 1px solid rgba(151,176,211,.15);
770
+ background: linear-gradient(180deg, transparent 48%, rgba(151,176,211,.1) 49%, rgba(151,176,211,.1) 51%, transparent 52%);
771
+ }
772
+ .latency-trace i {
773
+ flex: 1;
774
+ height: 12%;
775
+ border-radius: 2px;
776
+ background: rgba(83, 215, 247, .42);
777
+ animation: trace 1.6s ease-in-out infinite;
778
+ }
779
+ .latency-trace i:nth-child(2) { height: 24%; animation-delay: -.2s; }
780
+ .latency-trace i:nth-child(3) { height: 52%; animation-delay: -.4s; }
781
+ .latency-trace i:nth-child(4) { height: 92%; animation-delay: -.6s; }
782
+ .latency-trace i:nth-child(5) { height: 28%; animation-delay: -.8s; }
783
+ .latency-trace i:nth-child(6) { height: 18%; animation-delay: -1s; }
784
+ .latency-trace i:nth-child(7) { height: 38%; animation-delay: -1.2s; }
785
+ .latency-trace i:nth-child(8) { height: 14%; animation-delay: -1.4s; }
786
+ @keyframes trace { 50% { opacity: .35; transform: scaleY(.75); } }
787
+
788
+ .evidence {
789
+ min-height: 38px;
790
+ display: flex;
791
+ align-items: center;
792
+ gap: 18px;
793
+ padding: 0 14px;
794
+ border-top: 1px solid var(--line);
795
+ background: rgba(7, 10, 15, .74);
796
+ }
797
+ .evidence-cell {
798
+ display: flex;
799
+ align-items: center;
800
+ gap: 6px;
801
+ min-width: 0;
802
+ padding: 0;
803
+ border: 0;
804
+ }
805
+ .evidence-head { gap: 7px; color: #78879a; font-size: 9px; }
806
+ .measured-dot {
807
+ width: 5px;
808
+ height: 5px;
809
+ border-radius: 50%;
810
+ background: var(--cyan);
811
+ box-shadow: 0 0 6px var(--cyan);
812
+ }
813
+ .evidence-label {
814
+ color: #5f6d80;
815
+ font: 8px var(--mono);
816
+ letter-spacing: .07em;
817
+ text-transform: uppercase;
818
+ }
819
+ .evidence-value {
820
+ margin-top: 0;
821
+ overflow: hidden;
822
+ color: #b9c6d5;
823
+ font: 11px var(--mono);
824
+ text-overflow: ellipsis;
825
+ white-space: nowrap;
826
+ }
827
+
828
+ .controls {
829
+ display: flex;
830
+ align-items: center;
831
+ gap: 7px;
832
+ }
833
+ select {
834
+ height: 27px;
835
+ padding: 0 25px 0 8px;
836
+ color: #9ba8b8;
837
+ border: 1px solid var(--line);
838
+ border-radius: 7px;
839
+ outline: 0;
840
+ background: #0c1119;
841
+ font-size: 10px;
842
+ }
843
+ .run-experiment {
844
+ height: 27px;
845
+ padding: 0 10px;
846
+ color: #d9f8ff;
847
+ border: 1px solid rgba(83, 215, 247, .3);
848
+ border-radius: 7px;
849
+ background: rgba(83, 215, 247, .075);
850
+ font-size: 10px;
851
+ cursor: pointer;
852
+ }
853
+
854
+ .app.phase-idle .core {
855
+ opacity: .42;
856
+ animation: none;
857
+ transform: scale(.86);
858
+ }
859
+ .app.phase-idle .core-rings,
860
+ .app.phase-idle .conduit::after,
861
+ .app.phase-idle .particle {
862
+ animation-play-state: paused;
863
+ opacity: .2;
864
+ }
865
+ .app.phase-prefill .core {
866
+ animation: charge .72s ease-in-out infinite alternate;
867
+ }
868
+ .app.phase-prefill .core-rings::before { animation-duration: 2.2s; }
869
+ .app.phase-prefill .core-rings::after { animation-duration: 1.4s; }
870
+ .app.phase-prefill .tokens .particle { animation-play-state: paused; opacity: .14; }
871
+ .app.phase-prefill .memory,
872
+ .app.phase-prefill .context {
873
+ border-color: rgba(83, 215, 247, .4);
874
+ box-shadow: inset 0 0 24px rgba(83, 215, 247, .05);
875
+ }
876
+ @keyframes charge {
877
+ from { transform: scale(.75); filter: brightness(.7); }
878
+ to { transform: scale(1.12); filter: brightness(1.35); }
879
+ }
880
+ .app.phase-decode .core { animation: core-pulse 1.35s ease-in-out infinite; }
881
+ .app.core-slow .core { animation-duration: 2.9s; }
882
+ .assistant-live.slow-stream .cursor { animation-duration: 1.8s; }
883
+ .app.phase-recovery .core {
884
+ animation: recovery 1.1s ease-out infinite;
885
+ background: radial-gradient(circle at 36% 28%, #fff2e4, rgba(242,163,93,.7) 12%, rgba(145,73,39,.42) 35%, rgba(12,9,8,.94) 72%);
886
+ }
887
+
888
+ .memory.memory-pressure {
889
+ border-color: rgba(242, 163, 93, .28);
890
+ box-shadow: inset 0 0 25px rgba(242,163,93,.04);
891
+ }
892
+ .memory.memory-pressure .organ-state { color: var(--orange); }
893
+ .memory.memory-pressure .memory-blocks i.filled {
894
+ background: rgba(242,163,93,.5);
895
+ border-color: rgba(242,163,93,.4);
896
+ }
897
+ .memory.memory-pressure .memory-blocks i:nth-child(3n) {
898
+ animation: fragment .66s ease-in-out infinite alternate;
899
+ }
900
+ .memory.memory-pressure .memory-blocks i:nth-child(4n) {
901
+ animation: evict 1.8s ease-in-out infinite;
902
+ }
903
+ .memory.memory-overloaded {
904
+ border-color: rgba(240,107,121,.4);
905
+ animation: shake .28s linear infinite;
906
+ }
907
+ .memory.memory-overloaded .organ-state { color: var(--red); }
908
+ .memory.memory-overloaded .memory-blocks i.filled {
909
+ background: rgba(240,107,121,.56);
910
+ border-color: rgba(240,107,121,.45);
911
+ }
912
+
913
+ .context.context-pressure {
914
+ border-color: rgba(242, 163, 93, .3);
915
+ }
916
+ .context.context-pressure .organ-state { color: var(--orange); }
917
+ .context.context-pressure .context-vessel {
918
+ animation: pressure 1.35s ease-in-out infinite;
919
+ }
920
+ .context.context-pressure .context-fill {
921
+ background: rgba(242,163,93,.18);
922
+ border-color: rgba(242,163,93,.62);
923
+ }
924
+ .tokens.tokens-slow {
925
+ border-color: rgba(242,163,93,.34);
926
+ }
927
+ .tokens.tokens-slow .organ-state { color: var(--orange); }
928
+ .tokens.tokens-slow .particle {
929
+ animation: broken-flow 3.2s steps(7, end) infinite;
930
+ background: linear-gradient(90deg, rgba(242,163,93,.12), rgba(242,163,93,.9));
931
+ }
932
+ .tokens.tokens-stalled {
933
+ border-color: rgba(240,107,121,.4);
934
+ }
935
+ .tokens.tokens-stalled .organ-state { color: var(--red); }
936
+ .tokens.tokens-stalled .particle {
937
+ animation: stalled-flow 5s steps(3, end) infinite;
938
+ background: rgba(240,107,121,.55);
939
+ }
940
+
941
+ .health.engine-strained {
942
+ border-color: rgba(242,163,93,.3);
943
+ }
944
+ .health.engine-strained .organ-state { color: var(--orange); }
945
+ .health.engine-strained .latency-trace i {
946
+ background: rgba(242,163,93,.55);
947
+ animation-duration: .7s;
948
+ }
949
+ .health.engine-recovery {
950
+ border-color: rgba(240,107,121,.34);
951
+ }
952
+ .health.engine-recovery .organ-state { color: var(--red); }
953
+ .health.engine-recovery .latency-trace i {
954
+ background: rgba(240,107,121,.58);
955
+ animation: recovery-bars .52s steps(3, end) infinite;
956
+ }
957
+
958
+ /* Scenario-specific signal paths into the Cortex Core. */
959
+ .app.scenario-memory .c1 {
960
+ height: 2px;
961
+ background: repeating-linear-gradient(90deg, rgba(242,163,93,.08) 0 8px, rgba(242,163,93,.72) 8px 15px, transparent 15px 24px);
962
+ animation: connection-fault .38s steps(2, end) infinite;
963
+ }
964
+ .app.scenario-memory .c1::after {
965
+ background: linear-gradient(90deg, transparent, var(--orange), transparent);
966
+ animation: signal-stutter 1.1s steps(4, end) infinite;
967
+ }
968
+ .app.scenario-memory .core {
969
+ animation: memory-core-strain .82s steps(5, end) infinite;
970
+ }
971
+ .app.scenario-memory .core-rings::before {
972
+ border-left-color: rgba(242,163,93,.85);
973
+ border-top-color: rgba(242,163,93,.36);
974
+ animation-duration: 2.1s;
975
+ }
976
+
977
+ .app.scenario-slow .c3 {
978
+ height: 2px;
979
+ background: repeating-linear-gradient(90deg, rgba(242,163,93,.65) 0 13px, transparent 13px 31px);
980
+ animation: conduit-blackout 3.4s steps(5, end) infinite;
981
+ }
982
+ .app.scenario-slow .c3::after {
983
+ background: linear-gradient(90deg, transparent, var(--orange), transparent);
984
+ animation: signal-burst 3.4s steps(7, end) infinite;
985
+ }
986
+ .app.scenario-slow .core {
987
+ animation: slow-core-beat 3.4s steps(7, end) infinite;
988
+ }
989
+
990
+ .app.scenario-collapse .c2 {
991
+ height: 2px;
992
+ opacity: .24;
993
+ background: repeating-linear-gradient(90deg, rgba(240,107,121,.55) 0 7px, transparent 7px 20px);
994
+ animation: context-disconnect .7s steps(2, end) infinite;
995
+ }
996
+ .app.scenario-collapse .c2::after { display: none; }
997
+ .app.scenario-collapse .core {
998
+ animation: context-core-loss 1.15s steps(5, end) infinite;
999
+ }
1000
+ .app.scenario-collapse .core-rings::after {
1001
+ border-right-color: rgba(240,107,121,.72);
1002
+ opacity: .42;
1003
+ }
1004
+
1005
+ .app.scenario-loop .c3 {
1006
+ height: 2px;
1007
+ background: repeating-linear-gradient(90deg, rgba(240,107,121,.7) 0 9px, transparent 9px 18px);
1008
+ animation: loop-conduit 1.15s steps(4, end) infinite;
1009
+ }
1010
+ .app.scenario-loop .c3::after {
1011
+ background: linear-gradient(90deg, transparent, var(--red), transparent);
1012
+ animation: loop-signal 1.15s steps(5, end) infinite;
1013
+ }
1014
+ .app.scenario-loop .tokens {
1015
+ border-color: rgba(240,107,121,.44);
1016
+ box-shadow: 0 0 30px rgba(240,107,121,.14);
1017
+ }
1018
+ .app.scenario-loop .tokens .particle {
1019
+ background: var(--red);
1020
+ box-shadow: 0 0 10px rgba(240,107,121,.7);
1021
+ animation: loop-token 1.15s steps(5, end) infinite;
1022
+ }
1023
+ .app.scenario-loop .tokens .particle:nth-child(2n) { animation-delay: -.08s; }
1024
+ .app.scenario-loop .tokens .particle:nth-child(3n) { animation-delay: -.17s; }
1025
+ .app.scenario-loop .core {
1026
+ animation: thought-loop-core 1.15s steps(5, end) infinite;
1027
+ }
1028
+ .app.scenario-loop .core-rings::before {
1029
+ border-bottom-color: rgba(240,107,121,.84);
1030
+ border-left-color: rgba(240,107,121,.42);
1031
+ animation-duration: 1.15s;
1032
+ }
1033
+
1034
+ .app.hazard-context-full .observatory-stage::before,
1035
+ .app.hazard-loop .observatory-stage::before {
1036
+ border-color: rgba(240,107,121,.16);
1037
+ box-shadow:
1038
+ inset 0 0 80px rgba(240,107,121,.045),
1039
+ 0 0 120px rgba(240,107,121,.045);
1040
+ animation: danger-atmosphere 1.8s ease-in-out infinite;
1041
+ }
1042
+ .app.hazard-context-full .runtime-event {
1043
+ border-color: rgba(242,163,93,.9);
1044
+ color: #fff1dc;
1045
+ background:
1046
+ linear-gradient(90deg, rgba(49,22,8,.94), rgba(76,31,10,.94), rgba(49,22,8,.94));
1047
+ box-shadow:
1048
+ 0 0 0 1px rgba(242,163,93,.18),
1049
+ 0 0 38px rgba(242,163,93,.24),
1050
+ 0 0 92px rgba(240,107,121,.12);
1051
+ }
1052
+ .app.hazard-loop .runtime-event {
1053
+ border-color: rgba(240,107,121,.9);
1054
+ color: #ffe3e7;
1055
+ background:
1056
+ linear-gradient(90deg, rgba(45,7,17,.95), rgba(76,12,26,.94), rgba(45,7,17,.95));
1057
+ box-shadow:
1058
+ 0 0 0 1px rgba(240,107,121,.2),
1059
+ 0 0 42px rgba(240,107,121,.3),
1060
+ 0 0 110px rgba(240,107,121,.16);
1061
+ }
1062
+ .app.hazard-context-full .context {
1063
+ border-color: rgba(242,163,93,.62);
1064
+ box-shadow: 0 0 36px rgba(242,163,93,.12), inset 0 0 28px rgba(242,163,93,.05);
1065
+ }
1066
+ .app.hazard-context-full .context .context-vessel {
1067
+ animation: context-full-shock .74s steps(3, end) infinite;
1068
+ }
1069
+ .app.hazard-context-full .context .context-fill {
1070
+ background: rgba(242,163,93,.24);
1071
+ border-color: rgba(255,211,137,.9);
1072
+ box-shadow: inset -9px 0 16px rgba(242,163,93,.24);
1073
+ }
1074
+ .app.hazard-context-full .core-rings {
1075
+ border-color: rgba(242,163,93,.32);
1076
+ box-shadow: 0 0 46px rgba(242,163,93,.08), inset 0 0 52px rgba(242,163,93,.06);
1077
+ }
1078
+ .app.hazard-context-full .core-rings::after {
1079
+ border-color: rgba(242,163,93,.42);
1080
+ animation: rotate 3.8s linear infinite reverse;
1081
+ }
1082
+ .app.hazard-loop .core-rings {
1083
+ border-color: rgba(240,107,121,.34);
1084
+ box-shadow: 0 0 52px rgba(240,107,121,.1), inset 0 0 52px rgba(240,107,121,.06);
1085
+ }
1086
+ .app.hazard-loop .core-rings::after {
1087
+ border-color: rgba(240,107,121,.46);
1088
+ animation: rotate 2.2s steps(9, end) infinite;
1089
+ }
1090
+
1091
+ /* Memory pressure is relocation, eviction, and refill rather than a color change. */
1092
+ .memory.memory-pressure .memory-blocks {
1093
+ animation: bank-jolt 1.8s steps(4, end) infinite;
1094
+ }
1095
+ .memory.memory-pressure .memory-blocks i:nth-child(1) {
1096
+ animation: relocate-a 1.8s steps(4, end) infinite;
1097
+ }
1098
+ .memory.memory-pressure .memory-blocks i:nth-child(2) {
1099
+ animation: relocate-b 1.8s steps(4, end) infinite;
1100
+ }
1101
+ .memory.memory-pressure .memory-blocks i:nth-child(5) {
1102
+ animation: relocate-c 1.8s steps(4, end) infinite;
1103
+ }
1104
+ .memory.memory-pressure .memory-blocks i:nth-child(8) {
1105
+ animation: relocate-d 1.8s steps(4, end) infinite;
1106
+ }
1107
+ .memory.memory-pressure .memory-blocks i:nth-child(4),
1108
+ .memory.memory-pressure .memory-blocks i:nth-child(10) {
1109
+ animation: cell-blackout 1.8s steps(4, end) infinite;
1110
+ }
1111
+
1112
+ /* Slow decode is an unmistakable stop-burst-stop rhythm. */
1113
+ .tokens.tokens-slow .particle {
1114
+ animation: token-burst 3.4s steps(8, end) infinite;
1115
+ }
1116
+ .tokens.tokens-slow .particle:nth-child(2n) { animation-delay: -.12s; }
1117
+ .tokens.tokens-slow .particle:nth-child(3n) { animation-delay: -.24s; }
1118
+
1119
+ .message.context-fading { opacity: .4; transition: opacity 1s; }
1120
+ .message.outside-context {
1121
+ opacity: .23;
1122
+ filter: grayscale(.8);
1123
+ }
1124
+ .message.context-ejected {
1125
+ animation: context-pop-out 1.05s cubic-bezier(.2, .7, .2, 1) both;
1126
+ }
1127
+ .message.outside-context .bubble {
1128
+ border-style: dashed;
1129
+ }
1130
+ .context-badge {
1131
+ display: none;
1132
+ margin-top: 5px;
1133
+ color: var(--red);
1134
+ font: 8px var(--mono);
1135
+ letter-spacing: .08em;
1136
+ text-transform: uppercase;
1137
+ }
1138
+ .message.outside-context .context-badge { display: block; }
1139
+ .active-context-boundary {
1140
+ display: none;
1141
+ align-items: center;
1142
+ gap: 9px;
1143
+ margin: -2px 0 17px;
1144
+ color: rgba(240, 107, 121, .88);
1145
+ font: 8px var(--mono);
1146
+ letter-spacing: .1em;
1147
+ text-transform: uppercase;
1148
+ }
1149
+ .active-context-boundary::before,
1150
+ .active-context-boundary::after {
1151
+ content: "";
1152
+ height: 1px;
1153
+ flex: 1;
1154
+ background: linear-gradient(90deg, transparent, rgba(240,107,121,.72));
1155
+ }
1156
+ .active-context-boundary::after {
1157
+ background: linear-gradient(90deg, rgba(240,107,121,.72), transparent);
1158
+ }
1159
+ .messages.context-limited .active-context-boundary {
1160
+ display: flex;
1161
+ animation: boundary-arrive .7s ease-out;
1162
+ }
1163
+ .messages.context-limited .historical-context {
1164
+ opacity: .18;
1165
+ filter: grayscale(.9) blur(.25px);
1166
+ transform: scale(.985);
1167
+ transition: opacity .9s, filter .9s, transform .9s;
1168
+ }
1169
+ .messages.context-limited .historical-context .bubble {
1170
+ border-style: dashed;
1171
+ }
1172
+
1173
+ .runtime-event {
1174
+ position: absolute;
1175
+ z-index: 10;
1176
+ left: 50%;
1177
+ top: 50%;
1178
+ min-width: 220px;
1179
+ padding: 12px 16px;
1180
+ transform: translate(-50%, -50%) scale(.9);
1181
+ border: 1px solid rgba(240,107,121,.5);
1182
+ border-radius: 10px;
1183
+ color: #ffdce2;
1184
+ background: rgba(30,8,16,.9);
1185
+ box-shadow: 0 0 42px rgba(240,107,121,.14);
1186
+ font: 10px var(--mono);
1187
+ letter-spacing: .08em;
1188
+ text-align: center;
1189
+ opacity: 0;
1190
+ pointer-events: none;
1191
+ overflow: hidden;
1192
+ }
1193
+ .runtime-event.hazard {
1194
+ min-width: 276px;
1195
+ padding: 14px 20px;
1196
+ font-size: 11px;
1197
+ letter-spacing: .11em;
1198
+ }
1199
+ .runtime-event.critical {
1200
+ transform: translate(-50%, -50%) scale(1.04);
1201
+ }
1202
+ .runtime-event.hazard::before,
1203
+ .runtime-event.hazard::after {
1204
+ content: "";
1205
+ position: absolute;
1206
+ inset: 0;
1207
+ pointer-events: none;
1208
+ }
1209
+ .runtime-event.hazard::before {
1210
+ border-radius: inherit;
1211
+ box-shadow: inset 0 0 20px rgba(255,255,255,.05);
1212
+ }
1213
+ .runtime-event.hazard::after {
1214
+ width: 42%;
1215
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,.18), transparent);
1216
+ transform: skewX(-18deg) translateX(-140%);
1217
+ animation: loop-warning-scan 1.25s linear infinite;
1218
+ }
1219
+ .runtime-event.visible {
1220
+ animation: event-pop 1.55s ease-in-out forwards;
1221
+ }
1222
+ .runtime-event.sticky {
1223
+ opacity: 1;
1224
+ transform: translate(-50%, -50%) scale(1);
1225
+ animation: none;
1226
+ }
1227
+
1228
+ @keyframes fragment {
1229
+ to { transform: translateY(2px) rotate(3deg); opacity: .55; }
1230
+ }
1231
+ @keyframes evict {
1232
+ 55% { transform: translateY(-7px) scale(.8); opacity: .08; }
1233
+ 72% { transform: translateY(2px) scale(.7); opacity: 0; }
1234
+ 100% { transform: none; opacity: 1; }
1235
+ }
1236
+ @keyframes broken-flow {
1237
+ 0% { transform: translateX(-25px); opacity: .1; }
1238
+ 18%, 42% { transform: translateX(5px); opacity: 1; }
1239
+ 43%, 70% { transform: translateX(5px); opacity: .2; }
1240
+ 100% { transform: translateX(48px); opacity: .35; }
1241
+ }
1242
+ @keyframes boundary-arrive {
1243
+ from { opacity: 0; transform: scaleX(.72); }
1244
+ to { opacity: 1; transform: scaleX(1); }
1245
+ }
1246
+ @keyframes context-pop-out {
1247
+ 0% { opacity: 1; transform: translateX(0) scale(1); filter: none; }
1248
+ 42% { opacity: .72; transform: translateX(-10px) scale(.99); filter: grayscale(.2); }
1249
+ 100% { opacity: .23; transform: translateX(-20px) scale(.97); filter: grayscale(.8) blur(.25px); }
1250
+ }
1251
+ @keyframes connection-fault {
1252
+ 50% { opacity: .28; filter: brightness(1.8); }
1253
+ }
1254
+ @keyframes signal-stutter {
1255
+ 0%, 22% { transform: translateX(0); opacity: .1; }
1256
+ 35% { transform: translateX(55px); opacity: 1; }
1257
+ 36%, 58% { transform: translateX(55px); opacity: .12; }
1258
+ 100% { transform: translateX(175px); opacity: .8; }
1259
+ }
1260
+ @keyframes signal-burst {
1261
+ 0%, 57% { transform: translateX(0); opacity: 0; }
1262
+ 63% { opacity: 1; }
1263
+ 78% { transform: translateX(165px); opacity: 1; }
1264
+ 79%, 100% { transform: translateX(165px); opacity: 0; }
1265
+ }
1266
+ @keyframes conduit-blackout {
1267
+ 0%, 55% { opacity: .12; }
1268
+ 60%, 79% { opacity: 1; filter: brightness(1.6); }
1269
+ 80%, 100% { opacity: .18; }
1270
+ }
1271
+ @keyframes context-disconnect {
1272
+ 50% { opacity: .06; transform: rotate(-27deg) translateX(3px); }
1273
+ }
1274
+ @keyframes memory-core-strain {
1275
+ 0%, 100% {
1276
+ transform: translate(-2px, -2px) scale(.92);
1277
+ box-shadow: -18px -15px 34px rgba(242,163,93,.35), 0 0 45px rgba(83,215,247,.18), inset -9px -13px 25px rgba(0,0,0,.6);
1278
+ }
1279
+ 24% { transform: translate(3px, -1px) scale(1.1); }
1280
+ 42% { transform: translate(-4px, 2px) scale(.86); filter: brightness(.7); }
1281
+ 70% { transform: translate(1px, 1px) scale(1.04); filter: brightness(1.25); }
1282
+ }
1283
+ @keyframes slow-core-beat {
1284
+ 0%, 55% { transform: scale(.86); opacity: .58; }
1285
+ 60% { transform: scale(1.15); opacity: 1; }
1286
+ 72% { transform: scale(.94); opacity: .75; }
1287
+ 100% { transform: scale(.86); opacity: .58; }
1288
+ }
1289
+ @keyframes context-core-loss {
1290
+ 0%, 100% {
1291
+ transform: translate(2px, -2px) scale(.94);
1292
+ box-shadow: 18px -14px 34px rgba(240,107,121,.27), 0 0 38px rgba(83,215,247,.13), inset -9px -13px 25px rgba(0,0,0,.6);
1293
+ }
1294
+ 42% { transform: translate(-1px, 2px) scale(.8); opacity: .48; }
1295
+ 55% { transform: translate(3px, -3px) scale(1.06); opacity: .9; }
1296
+ }
1297
+ @keyframes bank-jolt {
1298
+ 0%, 100% { transform: translateX(0); }
1299
+ 28% { transform: translateX(-3px); }
1300
+ 32% { transform: translateX(4px); }
1301
+ 38% { transform: translateX(-2px); }
1302
+ }
1303
+ @keyframes relocate-a {
1304
+ 0%, 16%, 100% { transform: translate(0, 0); opacity: 1; }
1305
+ 35%, 58% { transform: translate(34px, 15px) rotate(5deg); opacity: .78; }
1306
+ 72% { transform: translate(0, 0); opacity: .35; }
1307
+ }
1308
+ @keyframes relocate-b {
1309
+ 0%, 18%, 100% { transform: translate(0, 0); }
1310
+ 36%, 62% { transform: translate(68px, 0) rotate(-4deg); opacity: .65; }
1311
+ }
1312
+ @keyframes relocate-c {
1313
+ 0%, 22%, 100% { transform: translate(0, 0); }
1314
+ 38%, 68% { transform: translate(-67px, 15px) rotate(7deg); opacity: .75; }
1315
+ }
1316
+ @keyframes relocate-d {
1317
+ 0%, 20%, 100% { transform: translate(0, 0); }
1318
+ 42%, 66% { transform: translate(-35px, -15px) rotate(-6deg); opacity: .7; }
1319
+ }
1320
+ @keyframes cell-blackout {
1321
+ 0%, 23%, 100% { opacity: 1; transform: scale(1); }
1322
+ 34%, 69% { opacity: .04; transform: scale(.62); background: rgba(3,8,14,.8); }
1323
+ 82% { opacity: .55; transform: scale(1.08); }
1324
+ }
1325
+ @keyframes token-burst {
1326
+ 0%, 52% { transform: translateX(-18px); opacity: .05; }
1327
+ 58% { transform: translateX(-8px); opacity: .9; }
1328
+ 72% { transform: translateX(52px); opacity: 1; }
1329
+ 76%, 100% { transform: translateX(58px); opacity: .04; }
1330
+ }
1331
+ @keyframes stalled-flow {
1332
+ 0%, 70% { transform: translateX(-15px); opacity: .08; }
1333
+ 82% { transform: translateX(12px); opacity: .8; }
1334
+ 100% { transform: translateX(18px); opacity: .1; }
1335
+ }
1336
+ @keyframes thought-loop-core {
1337
+ 0%, 100% {
1338
+ transform: scale(.9);
1339
+ box-shadow: 0 0 32px rgba(240,107,121,.2), inset -9px -13px 25px rgba(0,0,0,.6);
1340
+ }
1341
+ 28% { transform: scale(1.12) rotate(3deg); filter: brightness(1.3); }
1342
+ 45% { transform: scale(.78) rotate(-4deg); filter: brightness(.65); }
1343
+ 64% { transform: scale(1.04) rotate(2deg); }
1344
+ }
1345
+ @keyframes loop-conduit {
1346
+ 0%, 100% { opacity: .26; filter: brightness(.8); }
1347
+ 30% { opacity: 1; filter: brightness(1.7); }
1348
+ 54% { opacity: .08; }
1349
+ }
1350
+ @keyframes loop-signal {
1351
+ 0% { transform: translateX(0); opacity: 0; }
1352
+ 20% { opacity: 1; }
1353
+ 42% { transform: translateX(78px); opacity: 1; }
1354
+ 43%, 64% { transform: translateX(78px); opacity: .12; }
1355
+ 88% { transform: translateX(18px); opacity: .9; }
1356
+ 100% { transform: translateX(0); opacity: .1; }
1357
+ }
1358
+ @keyframes loop-token {
1359
+ 0%, 45% { transform: translateX(-16px); opacity: .08; }
1360
+ 55% { transform: translateX(46px); opacity: 1; }
1361
+ 70% { transform: translateX(20px); opacity: .55; }
1362
+ 100% { transform: translateX(46px); opacity: .16; }
1363
+ }
1364
+ @keyframes recovery {
1365
+ 0% { transform: scale(.75); opacity: .45; }
1366
+ 55% { transform: scale(1.08); opacity: 1; }
1367
+ 100% { transform: scale(.92); opacity: .75; }
1368
+ }
1369
+ @keyframes recovery-bars {
1370
+ 50% { height: 15%; opacity: .28; }
1371
+ }
1372
+ @keyframes event-pop {
1373
+ 0% { opacity: 0; transform: translate(-50%, -50%) scale(.8); }
1374
+ 18%, 72% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
1375
+ 100% { opacity: 0; transform: translate(-50%, -50%) scale(1.04); }
1376
+ }
1377
+ @keyframes danger-atmosphere {
1378
+ 0%, 100% {
1379
+ opacity: 1;
1380
+ transform: scale(1);
1381
+ }
1382
+ 50% {
1383
+ opacity: .58;
1384
+ transform: scale(.985);
1385
+ }
1386
+ }
1387
+ @keyframes context-full-shock {
1388
+ 0%, 100% {
1389
+ transform: scaleX(1);
1390
+ filter: brightness(1);
1391
+ }
1392
+ 36% {
1393
+ transform: scaleX(.955) translateX(-2px);
1394
+ filter: brightness(1.35);
1395
+ }
1396
+ 62% {
1397
+ transform: scaleX(1.015) translateX(1px);
1398
+ filter: brightness(.9);
1399
+ }
1400
+ }
1401
+ @keyframes loop-warning-scan {
1402
+ to { transform: skewX(-18deg) translateX(330%); }
1403
+ }
1404
+ @keyframes pressure {
1405
+ 50% { transform: scaleX(.97); border-color: rgba(242, 163, 93, .42); }
1406
+ }
1407
+ @keyframes shake {
1408
+ 25% { transform: translateX(-1px); }
1409
+ 75% { transform: translateX(1px); }
1410
+ }
open_cortex/ui/assets/open_cortex.js ADDED
@@ -0,0 +1,738 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const app = document.getElementById("app");
2
+ const phase = document.getElementById("phase");
3
+ const coreState = document.getElementById("core-state");
4
+ const coreDetail = document.getElementById("core-detail");
5
+ const kv = document.getElementById("kv");
6
+ const contextUsed = document.getElementById("context-used");
7
+ const contextUnit = document.getElementById("context-unit");
8
+ const contextFill = document.getElementById("context-fill");
9
+ const tps = document.getElementById("tps");
10
+ const ttft = document.getElementById("ttft");
11
+ const memoryState = document.getElementById("memory-state");
12
+ const contextState = document.getElementById("context-state");
13
+ const tokenState = document.getElementById("token-state");
14
+ const healthState = document.getElementById("health-state");
15
+ const scenarioLabel = document.getElementById("scenario-label");
16
+ const memoryBlocks = [...document.querySelectorAll("#memory-blocks i")];
17
+ const messages = document.getElementById("messages");
18
+ const runtimeEvent = document.getElementById("runtime-event");
19
+ const evidence3Label = document.getElementById("evidence-3-label");
20
+ const evidence3Value = document.getElementById("kv-retained");
21
+ const promptEval = document.getElementById("prompt-eval");
22
+ const prompt = document.getElementById("prompt");
23
+ const send = document.getElementById("send");
24
+ const experiment = document.getElementById("experiment");
25
+ const runExperimentButton = document.getElementById("run-experiment");
26
+ const workspace = document.querySelector(".workspace");
27
+ const resizer = document.getElementById("resizer");
28
+ const drawerToggle = document.getElementById("drawer-toggle");
29
+ const memoryOrgan = document.getElementById("memory-organ");
30
+ const contextOrgan = document.getElementById("context-organ");
31
+ const tokensOrgan = document.getElementById("tokens-organ");
32
+ const engineOrgan = document.getElementById("engine-organ");
33
+
34
+ let chatHistory = [];
35
+ let activeAssistant = null;
36
+ let previousChatWidth = workspace.getBoundingClientRect().width * 0.4;
37
+ let pendingChatWidth = previousChatWidth;
38
+ let collapseHoldUntil = 0;
39
+ let contextWindowFullPending = false;
40
+ const collapseThreshold = 230;
41
+
42
+ function finishActiveAssistant() {
43
+ if (activeAssistant) {
44
+ activeAssistant.isStreaming = false;
45
+ }
46
+ }
47
+
48
+ function clearHazardState() {
49
+ app.classList.remove("hazard-loop", "hazard-context-full");
50
+ runtimeEvent.classList.remove("hazard", "critical");
51
+ }
52
+
53
+ function setHazardState(kind, critical = false) {
54
+ clearHazardState();
55
+ app.classList.add(kind);
56
+ runtimeEvent.classList.add("hazard");
57
+ runtimeEvent.classList.toggle("critical", critical);
58
+ }
59
+
60
+ function showRuntimeEvent(label, sticky = false) {
61
+ runtimeEvent.textContent = label;
62
+ runtimeEvent.classList.remove("visible", "sticky");
63
+ void runtimeEvent.offsetWidth;
64
+ runtimeEvent.classList.add("visible");
65
+ runtimeEvent.classList.toggle("sticky", sticky);
66
+ }
67
+
68
+ function escapeHtml(value) {
69
+ return value
70
+ .replaceAll("&", "&amp;")
71
+ .replaceAll("<", "&lt;")
72
+ .replaceAll(">", "&gt;")
73
+ .replaceAll('"', "&quot;")
74
+ .replaceAll("'", "&#039;");
75
+ }
76
+
77
+ function renderInlineMarkdown(value) {
78
+ return escapeHtml(value)
79
+ .replace(/`([^`]+)`/g, "<code>$1</code>")
80
+ .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
81
+ }
82
+
83
+ function renderMarkdown(value) {
84
+ const lines = value.replace(/\r\n/g, "\n").split("\n");
85
+ const html = [];
86
+ let paragraph = [];
87
+ let list = [];
88
+ let inCode = false;
89
+ let codeLines = [];
90
+ let codeLang = "";
91
+
92
+ function flushParagraph() {
93
+ if (!paragraph.length) return;
94
+ html.push(`<p>${paragraph.map(renderInlineMarkdown).join("<br>")}</p>`);
95
+ paragraph = [];
96
+ }
97
+
98
+ function flushList() {
99
+ if (!list.length) return;
100
+ html.push(`<ul>${list.map((item) => `<li>${renderInlineMarkdown(item)}</li>`).join("")}</ul>`);
101
+ list = [];
102
+ }
103
+
104
+ for (const line of lines) {
105
+ const codeFence = line.match(/^```(\w+)?\s*$/);
106
+ if (codeFence) {
107
+ if (inCode) {
108
+ html.push(`<pre><code${codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""}>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
109
+ inCode = false;
110
+ codeLines = [];
111
+ codeLang = "";
112
+ } else {
113
+ flushParagraph();
114
+ flushList();
115
+ inCode = true;
116
+ codeLang = codeFence[1] || "";
117
+ }
118
+ continue;
119
+ }
120
+
121
+ if (inCode) {
122
+ codeLines.push(line);
123
+ continue;
124
+ }
125
+
126
+ if (!line.trim()) {
127
+ flushParagraph();
128
+ flushList();
129
+ continue;
130
+ }
131
+
132
+ const heading = line.match(/^(#{1,3})\s+(.+)$/);
133
+ if (heading) {
134
+ flushParagraph();
135
+ flushList();
136
+ const level = heading[1].length;
137
+ html.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
138
+ continue;
139
+ }
140
+
141
+ const bullet = line.match(/^\s*[-*]\s+(.+)$/);
142
+ if (bullet) {
143
+ flushParagraph();
144
+ list.push(bullet[1]);
145
+ continue;
146
+ }
147
+
148
+ paragraph.push(line);
149
+ }
150
+
151
+ if (inCode) {
152
+ html.push(`<pre><code${codeLang ? ` data-lang="${escapeHtml(codeLang)}"` : ""}>${escapeHtml(codeLines.join("\n"))}</code></pre>`);
153
+ }
154
+ flushParagraph();
155
+ flushList();
156
+ return html.join("");
157
+ }
158
+
159
+ function renderMessages() {
160
+ if (!chatHistory.length) {
161
+ messages.classList.remove("context-limited");
162
+ messages.innerHTML = `
163
+ <div class="message">
164
+ <div class="message-meta">Cortex</div>
165
+ <div class="bubble">Send a message to begin observing local inference.</div>
166
+ </div>
167
+ `;
168
+ return;
169
+ }
170
+
171
+ const hasCollapsedContext = chatHistory.some((message) => message.outsideContext);
172
+ messages.classList.toggle("context-limited", hasCollapsedContext);
173
+ let boundaryInserted = false;
174
+
175
+ messages.innerHTML = chatHistory.map((message, index) => {
176
+ const role = message.role === "user" ? "user" : "";
177
+ const live = message.role === "assistant" && message.isStreaming ? " assistant-live" : "";
178
+ const outside = message.outsideContext ? " historical-context outside-context" : "";
179
+ const ejected = message.contextEjected ? " context-ejected" : "";
180
+ const author = message.role === "user" ? "You" : "Cortex";
181
+ const cursor = live ? '<span class="cursor"></span>' : "";
182
+ const boundary = !boundaryInserted && hasCollapsedContext && !message.outsideContext
183
+ ? '<div class="active-context-boundary"><span>Active context boundary · outside active context above</span></div>'
184
+ : "";
185
+ if (boundary) boundaryInserted = true;
186
+ const body = message.role === "assistant" ? renderMarkdown(message.content) : escapeHtml(message.content);
187
+ return `
188
+ ${boundary}
189
+ <div class="message ${role}${live}${outside}${ejected}">
190
+ <div class="message-meta">${author}</div>
191
+ <div class="bubble markdown-body">${body}${cursor}</div>
192
+ ${message.outsideContext ? '<div class="context-badge">outside active context</div>' : ""}
193
+ </div>
194
+ `;
195
+ }).join("");
196
+ messages.scrollTop = messages.scrollHeight;
197
+ }
198
+
199
+ function setPhase(name, label, core, detail) {
200
+ const preserveCollapse = app.classList.contains("scenario-collapse") && Date.now() < collapseHoldUntil;
201
+ const removableClasses = [
202
+ "phase-idle",
203
+ "phase-prefill",
204
+ "phase-decode",
205
+ "phase-recovery",
206
+ "scenario-loop",
207
+ "scenario-memory",
208
+ "scenario-slow",
209
+ "scenario-collapse",
210
+ "hazard-loop",
211
+ "hazard-context-full"
212
+ ].filter((className) => !(preserveCollapse && className === "scenario-collapse"));
213
+
214
+ app.classList.remove(...removableClasses);
215
+ app.classList.add("phase-" + name);
216
+ phase.textContent = label;
217
+ coreState.textContent = core;
218
+ coreDetail.textContent = detail;
219
+ }
220
+
221
+ function resetRuntimeVisuals() {
222
+ app.classList.remove(
223
+ "scenario-loop",
224
+ "scenario-memory",
225
+ "scenario-slow",
226
+ "scenario-collapse",
227
+ "core-slow",
228
+ "hazard-loop",
229
+ "hazard-context-full"
230
+ );
231
+ clearHazardState();
232
+ collapseHoldUntil = 0;
233
+ contextWindowFullPending = false;
234
+ memoryOrgan.className = "organ memory active";
235
+ contextOrgan.className = "organ context active";
236
+ tokensOrgan.className = "organ tokens active";
237
+ engineOrgan.className = "organ health active";
238
+ runtimeEvent.classList.remove("visible", "sticky");
239
+ }
240
+
241
+ function setMetrics({ contextTokens, contextSize, speed, firstToken, promptRate }) {
242
+ const used = contextTokens == null ? "—" : Number(contextTokens).toLocaleString();
243
+ const size = contextSize == null ? "—" : Number(contextSize).toLocaleString();
244
+ contextUsed.textContent = used;
245
+ contextUnit.textContent = `/ ${size}`;
246
+ const fill = contextTokens && contextSize ? Math.min(100, Math.round(contextTokens / contextSize * 100)) : 0;
247
+ contextFill.style.width = fill + "%";
248
+ kv.textContent = contextTokens && contextSize ? String(fill) : "—";
249
+ tps.textContent = speed == null ? "—" : Number(speed).toFixed(1);
250
+ ttft.textContent = firstToken == null ? "—" : Number(firstToken).toFixed(0);
251
+ promptEval.textContent = promptRate == null ? "measuring" : `${Number(promptRate).toFixed(1)} tok/s`;
252
+
253
+ const filled = Math.max(1, Math.min(12, Math.round(fill / 100 * memoryBlocks.length)));
254
+ memoryBlocks.forEach((block, index) => block.classList.toggle("filled", index < filled));
255
+
256
+ memoryOrgan.classList.toggle("memory-pressure", fill >= 70 && fill < 85);
257
+ memoryOrgan.classList.toggle("memory-overloaded", fill >= 85);
258
+ contextOrgan.classList.toggle("context-pressure", fill >= 70);
259
+
260
+ if (fill >= 85) {
261
+ memoryState.textContent = "Memory overloaded";
262
+ contextState.textContent = "Near context limit";
263
+ } else if (fill >= 70) {
264
+ memoryState.textContent = "Memory strained";
265
+ contextState.textContent = "Context pressure";
266
+ } else if (fill >= 45) {
267
+ memoryState.textContent = "Memory busy";
268
+ contextState.textContent = "Context growing";
269
+ }
270
+ }
271
+
272
+ function setRuntimeIdle() {
273
+ resetRuntimeVisuals();
274
+ scenarioLabel.textContent = "Live local chat";
275
+ setPhase("idle", "IDLE", "Ready for input", "ENGINE IDLE · SLOT AVAILABLE");
276
+ runtimeEvent.textContent = "READY";
277
+ memoryState.textContent = "Memory quiet";
278
+ contextState.textContent = "Context resting";
279
+ tokenState.textContent = "Stream dormant";
280
+ healthState.textContent = "Engine healthy";
281
+ kv.textContent = "—";
282
+ setMetrics({ contextTokens: 0, contextSize: null, speed: 0, firstToken: null, promptRate: null });
283
+ evidence3Label.textContent = "KV evidence";
284
+ evidence3Value.textContent = "context proxy";
285
+ }
286
+
287
+ function applyRuntimeEvent(event) {
288
+ if (event.kind === "context_collapse") {
289
+ applyContextCollapse(event);
290
+ return;
291
+ }
292
+
293
+ if (event.kind === "request_started") {
294
+ clearHazardState();
295
+ scenarioLabel.textContent = "Live local chat";
296
+ setPhase("prefill", "PREFILL", "Waiting for first token", "PREFILL · PROCESSING PROMPT");
297
+ showRuntimeEvent("PREFILL STARTED");
298
+ memoryState.textContent = "Memory allocating";
299
+ contextState.textContent = "Context pending";
300
+ tokenState.textContent = "Stream waiting";
301
+ healthState.textContent = "Engine charging";
302
+ kv.textContent = "—";
303
+ evidence3Label.textContent = "KV proxy";
304
+ evidence3Value.textContent = "waiting for context";
305
+ return;
306
+ }
307
+
308
+ if (event.kind === "first_token") {
309
+ const snapshot = event.snapshot || {};
310
+ const contextTokens = event.context_tokens ?? snapshot.slot_context_tokens?.[0] ?? null;
311
+ const contextSize = event.context_size ?? snapshot.slot_context_size ?? null;
312
+ setPhase("decode", "DECODE", "Generating response", `TTFT ${Number(event.ttft_ms || 0).toFixed(1)} ms · LIVE`);
313
+ showRuntimeEvent("FIRST TOKEN");
314
+ memoryState.textContent = "Memory holding";
315
+ contextState.textContent = "Context active";
316
+ tokenState.textContent = "Decode flowing";
317
+ healthState.textContent = `processing=${snapshot.requests_processing ?? "—"} deferred=${snapshot.requests_deferred ?? "—"}`;
318
+ setMetrics({
319
+ contextTokens,
320
+ contextSize,
321
+ speed: snapshot.decode_tps,
322
+ firstToken: event.ttft_ms,
323
+ promptRate: snapshot.prompt_tps
324
+ });
325
+ evidence3Label.textContent = "KV proxy";
326
+ evidence3Value.textContent = event.working_memory_percent == null ? "context-derived" : `${event.working_memory_percent.toFixed(1)}% context-derived`;
327
+ return;
328
+ }
329
+
330
+ if (event.kind === "token") {
331
+ if (event.repetition_detected) {
332
+ app.classList.add("scenario-loop");
333
+ setHazardState("hazard-loop", true);
334
+ showRuntimeEvent("THOUGHT LOOP DETECTED", true);
335
+ coreState.textContent = "Thought loop emerging";
336
+ tokenState.textContent = `Thought loop · ${event.live_tps ? event.live_tps.toFixed(1) : "—"} tok/s`;
337
+ healthState.textContent = "Repetition detected";
338
+ evidence3Label.textContent = "Generation pattern";
339
+ evidence3Value.textContent = "thought loop detected";
340
+ } else {
341
+ runtimeEvent.textContent = "TOKEN STREAM";
342
+ coreState.textContent = "Generating response";
343
+ tokenState.textContent = `Decode flowing · ${event.live_tps ? event.live_tps.toFixed(1) : "—"} tok/s`;
344
+ healthState.textContent = "Engine active";
345
+ evidence3Label.textContent = "Generation pattern";
346
+ evidence3Value.textContent = "no repetition detected";
347
+ }
348
+ phase.textContent = "DECODE";
349
+ coreDetail.textContent = `DECODE TOKEN ${event.generated_tokens || "—"} · LIVE`;
350
+ setMetrics({
351
+ contextTokens: event.context_tokens,
352
+ contextSize: event.context_size,
353
+ speed: event.live_tps,
354
+ firstToken: Number(ttft.textContent) || null,
355
+ promptRate: null
356
+ });
357
+ promptEval.textContent = `${event.generated_tokens || 0} generated tokens`;
358
+ if (event.working_memory_percent != null) {
359
+ evidence3Label.textContent = "KV proxy";
360
+ evidence3Value.textContent = `${event.working_memory_percent.toFixed(1)}% context-derived`;
361
+ }
362
+ return;
363
+ }
364
+
365
+ if (event.kind === "request_completed") {
366
+ finishActiveAssistant();
367
+ const contextFull = event.context_size && event.context_tokens >= event.context_size;
368
+ phase.textContent = "DONE";
369
+ showRuntimeEvent(
370
+ contextFull ? "CONTEXT WINDOW FULL" : (event.repetition_detected ? "THOUGHT LOOP DETECTED" : "REQUEST COMPLETE"),
371
+ contextFull || event.repetition_detected
372
+ );
373
+ coreState.textContent = contextFull
374
+ ? "Context full; next turn will evict earliest memory"
375
+ : (event.repetition_detected ? "Generation ended in a loop" : "Generation complete");
376
+ coreDetail.textContent = "FINAL TIMINGS RECEIVED";
377
+ setMetrics({
378
+ contextTokens: event.context_tokens,
379
+ contextSize: event.context_size,
380
+ speed: event.decode_tps,
381
+ firstToken: Number(ttft.textContent) || null,
382
+ promptRate: event.prompt_tps
383
+ });
384
+ tokenState.textContent = `Decode ${event.decode_tps ? event.decode_tps.toFixed(1) : "—"} tok/s`;
385
+ healthState.textContent = "Engine settled";
386
+ promptEval.textContent = `Prefill ${event.prompt_tps ? event.prompt_tps.toFixed(1) : "—"} tok/s`;
387
+ evidence3Label.textContent = "Output tokens";
388
+ evidence3Value.textContent = event.completion_tokens ?? "—";
389
+ if (contextFull) {
390
+ applyContextFullWarning();
391
+ } else {
392
+ contextState.textContent = event.repetition_detected ? "Context saturated by loop" : "Context retained";
393
+ }
394
+ renderMessages();
395
+ }
396
+ }
397
+
398
+ function applyContextFullWarning() {
399
+ contextWindowFullPending = true;
400
+ collapseHoldUntil = Date.now() + 2500;
401
+ scenarioLabel.textContent = "Context full";
402
+ setHazardState("hazard-context-full", true);
403
+ app.classList.add("scenario-collapse");
404
+ contextOrgan.classList.add("context-pressure");
405
+ memoryOrgan.classList.add("memory-overloaded");
406
+ engineOrgan.classList.add("engine-strained");
407
+ runtimeEvent.textContent = "CONTEXT WINDOW FULL";
408
+ runtimeEvent.classList.add("visible", "sticky");
409
+ memoryState.textContent = "Memory overloaded";
410
+ contextState.textContent = "Next turn will evict earliest message";
411
+ tokenState.textContent = "Stream complete";
412
+ healthState.textContent = "Awaiting next turn";
413
+ evidence3Label.textContent = "Next action";
414
+ evidence3Value.textContent = "pop earliest turn";
415
+ }
416
+
417
+ function applyDeferredContextEjection() {
418
+ if (!contextWindowFullPending) return false;
419
+
420
+ const target = chatHistory.find((message) => !message.outsideContext);
421
+ if (!target) return false;
422
+
423
+ target.outsideContext = true;
424
+ target.contextEjected = true;
425
+ contextWindowFullPending = false;
426
+ clearHazardState();
427
+ collapseHoldUntil = Date.now() + 2500;
428
+ scenarioLabel.textContent = "Context collapse";
429
+ setPhase("decode", "COLLAPSE", "Context boundary shifted", "EARLIEST TURN OUTSIDE ACTIVE MEMORY");
430
+ app.classList.add("scenario-collapse");
431
+ showRuntimeEvent("CONTEXT COLLAPSE", true);
432
+ memoryState.textContent = "Memory rebalanced";
433
+ contextState.textContent = "Earliest message ejected";
434
+ tokenState.textContent = "Sending recent context";
435
+ healthState.textContent = "Engine recovering";
436
+ evidence3Label.textContent = "Dropped turns";
437
+ evidence3Value.textContent = "1";
438
+ return true;
439
+ }
440
+
441
+ function applyContextCollapse(event) {
442
+ const dropped = Math.max(1, Number(event.dropped_messages || 1));
443
+ let marked = 0;
444
+ for (const message of chatHistory) {
445
+ if (message === activeAssistant) continue;
446
+ if (marked >= dropped) break;
447
+ message.outsideContext = true;
448
+ message.contextEjected = marked === 0;
449
+ marked += 1;
450
+ }
451
+
452
+ collapseHoldUntil = Date.now() + 2500;
453
+ clearHazardState();
454
+ scenarioLabel.textContent = "Context collapse";
455
+ setPhase("decode", "COLLAPSE", "Context boundary shifted", "EARLIEST TURNS OUTSIDE ACTIVE MEMORY");
456
+ app.classList.add("scenario-collapse");
457
+ showRuntimeEvent("CONTEXT COLLAPSE", true);
458
+ memoryState.textContent = "Memory rebalanced";
459
+ contextState.textContent = "Earliest turns forgotten";
460
+ tokenState.textContent = "Retrying with recent context";
461
+ healthState.textContent = "Recovered locally";
462
+ contextUsed.textContent = "recent";
463
+ contextUnit.textContent = event.context_size ? `/ ${event.context_size}` : "/ active";
464
+ contextFill.style.width = "58%";
465
+ kv.textContent = "58";
466
+ evidence3Label.textContent = "Dropped turns";
467
+ evidence3Value.textContent = String(event.dropped_messages ?? marked);
468
+
469
+ if (activeAssistant && !activeAssistant.content) {
470
+ activeAssistant.content = "Earlier turns fell outside the active context.\n\n";
471
+ }
472
+ renderMessages();
473
+ }
474
+
475
+ function seedExperimentMessages({ outside = false } = {}) {
476
+ chatHistory = [
477
+ {
478
+ role: "user",
479
+ content: "Why does a long conversation make the model feel slower?",
480
+ outsideContext: outside
481
+ },
482
+ {
483
+ role: "assistant",
484
+ content: "Each new turn carries more history back into the model, increasing prefill work before decoding begins.",
485
+ outsideContext: outside
486
+ },
487
+ {
488
+ role: "user",
489
+ content: "Can you show me what changes inside the runtime?"
490
+ },
491
+ {
492
+ role: "assistant",
493
+ content: "Watch the runtime organs: context fills, working memory holds KV blocks, and token flow reveals decode health."
494
+ }
495
+ ];
496
+ activeAssistant = chatHistory.at(-1);
497
+ renderMessages();
498
+ }
499
+
500
+ function runLongContextExperiment() {
501
+ resetRuntimeVisuals();
502
+ seedExperimentMessages();
503
+ scenarioLabel.textContent = "Long context stress";
504
+ setPhase("prefill", "PREFILL", "Reading long context", "TTFT RISING · CONTEXT PRESSURE");
505
+ showRuntimeEvent("LONG CONTEXT STRESS", true);
506
+ contextOrgan.classList.add("context-pressure");
507
+ engineOrgan.classList.add("engine-strained");
508
+ memoryState.textContent = "Memory busy";
509
+ contextState.textContent = "Near context limit";
510
+ tokenState.textContent = "Stream waiting";
511
+ healthState.textContent = "Engine strained by prefill";
512
+ setMetrics({ contextTokens: 1840, contextSize: 2048, speed: 0, firstToken: 1380, promptRate: 38.6 });
513
+ evidence3Label.textContent = "Experiment";
514
+ evidence3Value.textContent = "simulated long prompt";
515
+ }
516
+
517
+ function runMemoryPressureExperiment() {
518
+ resetRuntimeVisuals();
519
+ seedExperimentMessages();
520
+ scenarioLabel.textContent = "Memory pressure";
521
+ app.classList.add("scenario-memory");
522
+ setPhase("decode", "STRAINED", "KV blocks reallocating", "WORKING SET PRESSURE · SIMULATED");
523
+ showRuntimeEvent("MEMORY PRESSURE", true);
524
+ memoryOrgan.classList.add("memory-pressure");
525
+ engineOrgan.classList.add("engine-strained");
526
+ memoryState.textContent = "KV blocks fragmenting";
527
+ contextState.textContent = "Context stable";
528
+ tokenState.textContent = "Decode flowing";
529
+ healthState.textContent = "Engine strained";
530
+ setMetrics({ contextTokens: 1240, contextSize: 2048, speed: 24.8, firstToken: 648, promptRate: 72.4 });
531
+ kv.textContent = "91";
532
+ evidence3Label.textContent = "KV proxy";
533
+ evidence3Value.textContent = "simulated pressure";
534
+ }
535
+
536
+ function runSlowDecodeExperiment() {
537
+ resetRuntimeVisuals();
538
+ seedExperimentMessages();
539
+ scenarioLabel.textContent = "Slow decode";
540
+ app.classList.add("scenario-slow", "core-slow");
541
+ setPhase("decode", "DECODE", "Engine decoding slowly", "TOKEN FLOW INTERRUPTED · SIMULATED");
542
+ showRuntimeEvent("SLOW DECODE", true);
543
+ tokensOrgan.classList.add("tokens-slow");
544
+ engineOrgan.classList.add("engine-strained");
545
+ memoryState.textContent = "Memory healthy";
546
+ contextState.textContent = "Context stable";
547
+ tokenState.textContent = "Flow interrupted";
548
+ healthState.textContent = "Decode scheduler strained";
549
+ setMetrics({ contextTokens: 980, contextSize: 2048, speed: 6.8, firstToken: 510, promptRate: 86.2 });
550
+ evidence3Label.textContent = "Experiment";
551
+ evidence3Value.textContent = "simulated decode delay";
552
+ }
553
+
554
+ function runContextCollapseExperiment() {
555
+ resetRuntimeVisuals();
556
+ seedExperimentMessages({ outside: true });
557
+ scenarioLabel.textContent = "Context collapse";
558
+ setPhase("recovery", "COLLAPSE", "Engine recovering", "EARLIEST MEMORY OUTSIDE ACTIVE WINDOW");
559
+ setHazardState("hazard-context-full", true);
560
+ app.classList.add("scenario-collapse");
561
+ showRuntimeEvent("CONTEXT COLLAPSE", true);
562
+ contextOrgan.classList.add("context-pressure", "context-forgetting");
563
+ engineOrgan.classList.add("engine-recovery");
564
+ memoryState.textContent = "Memory rebalanced";
565
+ contextState.textContent = "Earlier memory forgotten";
566
+ tokenState.textContent = "Retrying recent context";
567
+ healthState.textContent = "Engine recovering";
568
+ setMetrics({ contextTokens: 1180, contextSize: 2048, speed: 29.2, firstToken: 1742, promptRate: 34.8 });
569
+ evidence3Label.textContent = "Dropped turns";
570
+ evidence3Value.textContent = "2";
571
+ }
572
+
573
+ function runExperiment() {
574
+ switch (experiment.value) {
575
+ case "long-context":
576
+ runLongContextExperiment();
577
+ break;
578
+ case "memory-pressure":
579
+ runMemoryPressureExperiment();
580
+ break;
581
+ case "slow-decode":
582
+ runSlowDecodeExperiment();
583
+ break;
584
+ case "context-collapse":
585
+ runContextCollapseExperiment();
586
+ break;
587
+ default:
588
+ setRuntimeIdle();
589
+ prompt.focus();
590
+ }
591
+ }
592
+
593
+ async function streamChat() {
594
+ const value = prompt.value.trim();
595
+ if (!value) return;
596
+
597
+ prompt.value = "";
598
+ send.disabled = true;
599
+ const ejectedBeforeSend = applyDeferredContextEjection();
600
+ chatHistory.push({ role: "user", content: value });
601
+ activeAssistant = { role: "assistant", content: "", isStreaming: true };
602
+ chatHistory.push(activeAssistant);
603
+ if (ejectedBeforeSend) {
604
+ activeAssistant.content = "Earlier turns fell outside the active context.\n\n";
605
+ }
606
+ renderMessages();
607
+
608
+ try {
609
+ const response = await fetch("/api/chat", {
610
+ method: "POST",
611
+ headers: { "Content-Type": "application/json" },
612
+ body: JSON.stringify({
613
+ messages: chatHistory
614
+ .slice(0, -1)
615
+ .filter((message) => !message.outsideContext)
616
+ .map(({ role, content }) => ({ role, content }))
617
+ })
618
+ });
619
+
620
+ if (!response.ok || !response.body) {
621
+ finishActiveAssistant();
622
+ activeAssistant.content += `\n[OpenCortex backend error: ${response.status}]`;
623
+ renderMessages();
624
+ return;
625
+ }
626
+
627
+ const reader = response.body.getReader();
628
+ const decoder = new TextDecoder();
629
+ let buffer = "";
630
+
631
+ while (true) {
632
+ const { value: chunk, done } = await reader.read();
633
+ if (done) break;
634
+ buffer += decoder.decode(chunk, { stream: true });
635
+ const lines = buffer.split("\n");
636
+ buffer = lines.pop() || "";
637
+
638
+ for (const line of lines) {
639
+ if (!line.trim()) continue;
640
+ const event = JSON.parse(line);
641
+ if (event.kind === "error") {
642
+ finishActiveAssistant();
643
+ const isContextOverflow = event.code === "context_overflow";
644
+ showRuntimeEvent(isContextOverflow ? "CONTEXT WINDOW EXCEEDED" : "BACKEND ERROR", true);
645
+ if (isContextOverflow) {
646
+ setPhase("decode", "DEGRADED", "Context window exceeded", "REQUEST REJECTED · CONTEXT FULL");
647
+ memoryState.textContent = "Memory overloaded";
648
+ contextState.textContent = "Context limit reached";
649
+ tokenState.textContent = "Stream blocked";
650
+ healthState.textContent = "Increase -c or start a new run";
651
+ activeAssistant.content += `\n[OpenCortex] ${event.message}`;
652
+ } else {
653
+ activeAssistant.content += `\n[OpenCortex backend error: ${event.message}]`;
654
+ }
655
+ renderMessages();
656
+ continue;
657
+ }
658
+ applyRuntimeEvent(event);
659
+ if (event.text_delta) {
660
+ activeAssistant.content += event.text_delta;
661
+ renderMessages();
662
+ }
663
+ }
664
+ }
665
+ } catch (error) {
666
+ showRuntimeEvent("BACKEND ERROR", true);
667
+ finishActiveAssistant();
668
+ activeAssistant.content += `\n[OpenCortex backend error: ${error.message || error}]`;
669
+ renderMessages();
670
+ } finally {
671
+ finishActiveAssistant();
672
+ renderMessages();
673
+ send.disabled = false;
674
+ }
675
+ }
676
+
677
+ send.addEventListener("click", streamChat);
678
+ prompt.addEventListener("keydown", (event) => {
679
+ if (event.key === "Enter" && !event.shiftKey) {
680
+ event.preventDefault();
681
+ streamChat();
682
+ }
683
+ });
684
+ runExperimentButton.addEventListener("click", runExperiment);
685
+
686
+ function collapseConversation() {
687
+ const current = parseFloat(getComputedStyle(app).getPropertyValue("--chat-width"));
688
+ if (current > collapseThreshold) previousChatWidth = current;
689
+ app.classList.remove("collapse-preview");
690
+ app.classList.add("chat-collapsed");
691
+ app.style.setProperty("--chat-width", "0px");
692
+ }
693
+
694
+ function restoreConversation() {
695
+ app.classList.remove("chat-collapsed", "collapse-preview");
696
+ const width = Math.max(340, previousChatWidth || workspace.clientWidth * 0.4);
697
+ app.style.setProperty("--chat-width", width + "px");
698
+ }
699
+
700
+ resizer.addEventListener("pointerdown", (event) => {
701
+ if (event.target === drawerToggle || app.classList.contains("chat-collapsed")) return;
702
+ event.preventDefault();
703
+ resizer.setPointerCapture(event.pointerId);
704
+ resizer.classList.add("dragging");
705
+ document.body.style.cursor = "col-resize";
706
+ });
707
+
708
+ resizer.addEventListener("pointermove", (event) => {
709
+ if (!resizer.hasPointerCapture(event.pointerId)) return;
710
+ const bounds = workspace.getBoundingClientRect();
711
+ const maximum = bounds.width * 0.62;
712
+ pendingChatWidth = Math.max(72, Math.min(maximum, event.clientX - bounds.left));
713
+ app.style.setProperty("--chat-width", pendingChatWidth + "px");
714
+ app.classList.toggle("collapse-preview", pendingChatWidth < collapseThreshold);
715
+ });
716
+
717
+ resizer.addEventListener("pointerup", (event) => {
718
+ if (!resizer.hasPointerCapture(event.pointerId)) return;
719
+ resizer.releasePointerCapture(event.pointerId);
720
+ resizer.classList.remove("dragging");
721
+ document.body.style.cursor = "";
722
+ if (pendingChatWidth < collapseThreshold) collapseConversation();
723
+ else {
724
+ previousChatWidth = pendingChatWidth;
725
+ app.classList.remove("collapse-preview");
726
+ }
727
+ });
728
+
729
+ resizer.addEventListener("dblclick", () => {
730
+ if (app.classList.contains("chat-collapsed")) restoreConversation();
731
+ else collapseConversation();
732
+ });
733
+ drawerToggle.addEventListener("click", (event) => {
734
+ event.stopPropagation();
735
+ restoreConversation();
736
+ });
737
+
738
+ setRuntimeIdle();
pyproject.toml CHANGED
@@ -5,8 +5,10 @@ description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
 
8
  "gradio>=6.18.0",
9
  "httpx>=0.28.1",
 
10
  ]
11
 
12
  [build-system]
 
5
  readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
+ "fastapi>=0.137.0",
9
  "gradio>=6.18.0",
10
  "httpx>=0.28.1",
11
+ "uvicorn>=0.49.0",
12
  ]
13
 
14
  [build-system]
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi>=0.137.0
2
+ gradio>=6.18.0
3
+ httpx>=0.28.1
4
+ uvicorn>=0.49.0
tests/runtime/test_client.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ import httpx
3
+
4
+ from open_cortex.runtime.client import (
5
+ _detect_repetition,
6
+ _raise_for_status_with_body,
7
+ _working_memory_percent,
8
+ )
9
+
10
+
11
+ def test_detect_repetition_marks_repeated_generation_loop():
12
+ repeated = (
13
+ "艾丽和卡斯一起探索了整个宇宙,他们发现了一个惊人的事实:"
14
+ "整个宇宙的中心并不是银河系,而是整个宇宙的中心。"
15
+ )
16
+
17
+ assert _detect_repetition(repeated * 3) is True
18
+
19
+
20
+ def test_detect_repetition_allows_short_normal_text():
21
+ text = "KV Cache stores attention keys and values so decode can reuse prior context."
22
+
23
+ assert _detect_repetition(text) is False
24
+
25
+
26
+ def test_working_memory_percent_is_context_derived_and_capped():
27
+ assert _working_memory_percent(512, 2048) == 25.0
28
+ assert _working_memory_percent(4096, 2048) == 100.0
29
+ assert _working_memory_percent(None, 2048) is None
30
+
31
+
32
+ def test_raise_for_status_with_body_preserves_streaming_error_body():
33
+ request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
34
+ response = httpx.Response(
35
+ 400,
36
+ request=request,
37
+ stream=httpx.ByteStream(
38
+ b"request (2059 tokens) exceeds the available context size (2048 tokens)"
39
+ ),
40
+ )
41
+
42
+ with pytest.raises(httpx.HTTPStatusError) as exc_info:
43
+ _raise_for_status_with_body(response)
44
+
45
+ assert "exceeds the available context size" in exc_info.value.response.text
tests/ui/test_app.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.testclient import TestClient
5
+ import httpx
6
+
7
+ from open_cortex.runtime.events import RuntimeEvent
8
+ from open_cortex.runtime.messages import ChatMessage
9
+ from open_cortex.runtime.metrics import RuntimeSnapshot
10
+ from open_cortex.ui.app import (
11
+ create_app,
12
+ event_to_payload,
13
+ render_index,
14
+ _backend_mode,
15
+ _stream_events,
16
+ _stream_simulated_events,
17
+ _http_error_text,
18
+ _is_context_overflow,
19
+ _trim_for_context_collapse,
20
+ )
21
+
22
+
23
+ def test_render_index_uses_original_product_shell():
24
+ html = render_index()
25
+
26
+ assert 'id="app"' in html
27
+ assert 'class="topbar"' in html
28
+ assert "OpenCortex" in html
29
+ assert "llama.cpp · local" in html
30
+ assert "EN / 中" not in html
31
+ assert 'id="lang"' not in html
32
+ assert 'id="clear"' not in html
33
+ assert ">Clear<" not in html
34
+ assert ">Send<" in html
35
+ assert "Send message" not in html
36
+ assert 'class="workspace"' in html
37
+ assert 'id="messages"' in html
38
+ assert 'id="prompt"' in html
39
+ assert 'id="send"' in html
40
+ assert 'id="runtime-event"' in html
41
+ assert 'id="experiment"' in html
42
+ assert 'id="run-experiment"' in html
43
+ assert "Live detected" in html
44
+ assert "Sim · Memory pressure" in html
45
+ assert "Sim · Context collapse" in html
46
+ assert 'id="memory-organ"' in html
47
+ assert 'id="context-used"' in html
48
+ assert 'id="core-state"' in html
49
+ assert "/assets/open_cortex.css?v=" in html
50
+ assert "/assets/open_cortex.js?v=" in html
51
+
52
+
53
+ def test_styles_use_dynamic_viewport_height():
54
+ css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
55
+
56
+ assert "height: 100dvh" in css
57
+ assert "min-height: 100dvh" in css
58
+
59
+
60
+ def test_frontend_exposes_experiment_and_engine_state_handlers():
61
+ js = (create_app.__globals__["ASSET_DIR"] / "open_cortex.js").read_text()
62
+
63
+ assert "function renderMarkdown" in js
64
+ assert "message.role === \"assistant\" ? renderMarkdown" in js
65
+ assert "isStreaming" in js
66
+ assert 'message.role === "assistant" && message.isStreaming' in js
67
+ assert "activeAssistant.isStreaming = false" in js
68
+ assert "function runExperiment" in js
69
+ assert "function runMemoryPressureExperiment" in js
70
+ assert "function runSlowDecodeExperiment" in js
71
+ assert "function runContextCollapseExperiment" in js
72
+ assert "collapseHoldUntil" in js
73
+ assert "contextWindowFullPending" in js
74
+ assert "function applyContextFullWarning" in js
75
+ assert "function applyDeferredContextEjection" in js
76
+ assert "CONTEXT WINDOW FULL" in js
77
+ assert "hazard-context-full" in js
78
+ assert "hazard-loop" in js
79
+ assert "runtimeEvent.classList.add(\"hazard\"" in js
80
+ assert "Earlier turns fell outside the active context." in js
81
+ assert "engine-strained" in js
82
+ assert "Engine recovering" in js
83
+
84
+
85
+ def test_danger_states_have_decisive_visual_treatment():
86
+ css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
87
+
88
+ assert ".app.hazard-context-full .runtime-event" in css
89
+ assert ".app.hazard-loop .runtime-event" in css
90
+ assert ".runtime-event.hazard" in css
91
+ assert ".runtime-event.critical" in css
92
+ assert "context-full-shock" in css
93
+ assert "loop-warning-scan" in css
94
+ assert "danger-atmosphere" in css
95
+
96
+
97
+ def test_context_collapse_styles_pop_out_oldest_message():
98
+ css = (create_app.__globals__["ASSET_DIR"] / "open_cortex.css").read_text()
99
+
100
+ assert "context-pop-out" in css
101
+ assert "@keyframes context-pop-out" in css
102
+
103
+
104
+ def test_create_app_serves_index():
105
+ app = create_app()
106
+ client = TestClient(app)
107
+
108
+ response = client.get("/")
109
+
110
+ assert isinstance(app, FastAPI)
111
+ assert response.status_code == 200
112
+ assert "OpenCortex" in response.text
113
+ assert 'id="send"' in response.text
114
+
115
+
116
+ def test_space_defaults_to_simulated_backend(monkeypatch):
117
+ monkeypatch.delenv("OPEN_CORTEX_BACKEND", raising=False)
118
+ monkeypatch.setenv("SPACE_ID", "build-small-hackathon/open-cortex")
119
+
120
+ assert _backend_mode() == "simulated"
121
+
122
+
123
+ def test_explicit_backend_overrides_space_default(monkeypatch):
124
+ monkeypatch.setenv("OPEN_CORTEX_BACKEND", "llama_cpp")
125
+ monkeypatch.setenv("SPACE_ID", "build-small-hackathon/open-cortex")
126
+
127
+ assert _backend_mode() == "llama_cpp"
128
+
129
+
130
+ def test_stream_events_can_run_without_llama_cpp_in_simulated_mode(monkeypatch):
131
+ monkeypatch.setenv("OPEN_CORTEX_BACKEND", "simulated")
132
+ monkeypatch.setenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0")
133
+
134
+ messages = [ChatMessage(role="user", content="What is OpenCortex?")]
135
+
136
+ payloads = [json.loads(line) for line in _stream_events(messages)]
137
+
138
+ assert payloads[0]["kind"] == "request_started"
139
+ assert payloads[1]["kind"] == "first_token"
140
+ assert payloads[-1]["kind"] == "request_completed"
141
+ assert "OpenCortex" in "".join(payload["text_delta"] for payload in payloads)
142
+ assert payloads[-1]["context_size"] == 2048
143
+ assert payloads[-1]["decode_tps"] > 0
144
+
145
+
146
+ def test_simulated_story_can_surface_thought_loop(monkeypatch):
147
+ monkeypatch.setenv("OPEN_CORTEX_SIMULATOR_DELAY_SECONDS", "0")
148
+
149
+ messages = [ChatMessage(role="user", content="Write a long repeating story.")]
150
+
151
+ payloads = [
152
+ event_to_payload(event)
153
+ for event in _stream_simulated_events(messages)
154
+ ]
155
+
156
+ assert any(payload["repetition_detected"] for payload in payloads)
157
+
158
+
159
+ def test_event_to_payload_includes_live_loop_state():
160
+ event = RuntimeEvent(
161
+ kind="token",
162
+ text_delta="银河系",
163
+ ttft_ms=None,
164
+ snapshot=RuntimeSnapshot(
165
+ prompt_tps=88.8,
166
+ decode_tps=21.5,
167
+ requests_processing=1,
168
+ requests_deferred=0,
169
+ active_slots=1,
170
+ slot_context_tokens=(512,),
171
+ slot_context_size=2048,
172
+ ),
173
+ generated_tokens=66,
174
+ elapsed_ms=2100.0,
175
+ live_tps=19.7,
176
+ repetition_detected=True,
177
+ context_tokens=578,
178
+ context_size=2048,
179
+ working_memory_percent=28.2,
180
+ )
181
+
182
+ payload = event_to_payload(event)
183
+
184
+ assert payload["kind"] == "token"
185
+ assert payload["text_delta"] == "银河系"
186
+ assert payload["generated_tokens"] == 66
187
+ assert payload["live_tps"] == 19.7
188
+ assert payload["repetition_detected"] is True
189
+ assert payload["context_tokens"] == 578
190
+ assert payload["context_size"] == 2048
191
+ assert payload["working_memory_percent"] == 28.2
192
+ assert payload["snapshot"]["slot_context_tokens"] == [512]
193
+ assert payload["snapshot"]["slot_context_size"] == 2048
194
+
195
+
196
+ def test_stream_events_emits_context_collapse_and_retries_trimmed_history(monkeypatch):
197
+ calls: list[list[ChatMessage]] = []
198
+ request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
199
+ overflow = httpx.HTTPStatusError(
200
+ "400 Bad Request",
201
+ request=request,
202
+ response=httpx.Response(
203
+ 400,
204
+ request=request,
205
+ text="request (2059 tokens) exceeds the available context size (2048 tokens)",
206
+ ),
207
+ )
208
+
209
+ def fake_stream_chat_events(messages: list[ChatMessage]):
210
+ calls.append(messages)
211
+ if len(calls) == 1:
212
+ yield RuntimeEvent(
213
+ kind="request_started",
214
+ text_delta="",
215
+ ttft_ms=None,
216
+ snapshot=None,
217
+ )
218
+ raise overflow
219
+ yield RuntimeEvent(
220
+ kind="first_token",
221
+ text_delta="继续",
222
+ ttft_ms=12.3,
223
+ snapshot=None,
224
+ context_tokens=128,
225
+ context_size=2048,
226
+ working_memory_percent=6.2,
227
+ )
228
+
229
+ monkeypatch.setattr("open_cortex.ui.app.stream_chat_events", fake_stream_chat_events)
230
+ messages = [
231
+ ChatMessage(role="system", content="You are Cortex."),
232
+ ChatMessage(role="user", content="old question"),
233
+ ChatMessage(role="assistant", content="old long answer"),
234
+ ChatMessage(role="user", content="recent question"),
235
+ ChatMessage(role="assistant", content="recent answer"),
236
+ ChatMessage(role="user", content="继续"),
237
+ ]
238
+
239
+ payloads = [json.loads(line) for line in _stream_events(messages)]
240
+
241
+ assert [payload["kind"] for payload in payloads] == [
242
+ "request_started",
243
+ "context_collapse",
244
+ "first_token",
245
+ ]
246
+ assert payloads[1]["dropped_messages"] == 2
247
+ assert payloads[1]["retained_messages"] == 4
248
+ assert payloads[1]["context_size"] == 2048
249
+ assert [message.content for message in calls[1]] == [
250
+ "You are Cortex.",
251
+ "recent question",
252
+ "recent answer",
253
+ "继续",
254
+ ]
255
+
256
+
257
+ def test_context_overflow_detection_reads_streaming_error_body():
258
+ request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
259
+ response = httpx.Response(
260
+ 400,
261
+ request=request,
262
+ stream=httpx.ByteStream(
263
+ b"request (2059 tokens) exceeds the available context size (2048 tokens)"
264
+ ),
265
+ )
266
+ error = httpx.HTTPStatusError("400 Bad Request", request=request, response=response)
267
+
268
+ assert _is_context_overflow(error) is True
269
+
270
+
271
+ def test_http_error_text_handles_closed_stream_without_crashing():
272
+ request = httpx.Request("POST", "http://127.0.0.1:8080/v1/chat/completions")
273
+ response = httpx.Response(
274
+ 400,
275
+ request=request,
276
+ stream=httpx.ByteStream(b"closed"),
277
+ )
278
+ response.close()
279
+ error = httpx.HTTPStatusError("400 Bad Request", request=request, response=response)
280
+
281
+ assert _http_error_text(error) == ""
282
+
283
+
284
+ def test_trim_for_context_collapse_drops_oversized_assistant_history():
285
+ messages = [
286
+ ChatMessage(role="user", content="write a long story"),
287
+ ChatMessage(role="assistant", content="故事" * 4000),
288
+ ChatMessage(role="user", content="what happened next?"),
289
+ ChatMessage(role="assistant", content="recent answer"),
290
+ ChatMessage(role="user", content="继续"),
291
+ ]
292
+
293
+ trimmed, dropped = _trim_for_context_collapse(messages)
294
+
295
+ assert dropped == 2
296
+ assert trimmed == [
297
+ ChatMessage(role="user", content="what happened next?"),
298
+ ChatMessage(role="assistant", content="recent answer"),
299
+ ChatMessage(role="user", content="继续"),
300
+ ]
301
+
302
+
303
+ def test_trim_for_context_collapse_keeps_recent_tail_of_oversized_answer():
304
+ messages = [
305
+ ChatMessage(role="user", content="write a long story"),
306
+ ChatMessage(role="assistant", content="A" * 5000 + "recent ending"),
307
+ ChatMessage(role="user", content="1"),
308
+ ]
309
+
310
+ trimmed, dropped = _trim_for_context_collapse(messages)
311
+
312
+ assert dropped == 1
313
+ assert len(trimmed) == 2
314
+ assert trimmed[0].role == "assistant"
315
+ assert "Earlier content collapsed" in trimmed[0].content
316
+ assert "recent ending" in trimmed[0].content
317
+ assert trimmed[1] == ChatMessage(role="user", content="1")
uv.lock CHANGED
@@ -549,8 +549,10 @@ name = "open-cortex"
549
  version = "0.1.0"
550
  source = { editable = "." }
551
  dependencies = [
 
552
  { name = "gradio" },
553
  { name = "httpx" },
 
554
  ]
555
 
556
  [package.dev-dependencies]
@@ -560,8 +562,10 @@ dev = [
560
 
561
  [package.metadata]
562
  requires-dist = [
 
563
  { name = "gradio", specifier = ">=6.18.0" },
564
  { name = "httpx", specifier = ">=0.28.1" },
 
565
  ]
566
 
567
  [package.metadata.requires-dev]
 
549
  version = "0.1.0"
550
  source = { editable = "." }
551
  dependencies = [
552
+ { name = "fastapi" },
553
  { name = "gradio" },
554
  { name = "httpx" },
555
+ { name = "uvicorn" },
556
  ]
557
 
558
  [package.dev-dependencies]
 
562
 
563
  [package.metadata]
564
  requires-dist = [
565
+ { name = "fastapi", specifier = ">=0.137.0" },
566
  { name = "gradio", specifier = ">=6.18.0" },
567
  { name = "httpx", specifier = ">=0.28.1" },
568
+ { name = "uvicorn", specifier = ">=0.49.0" },
569
  ]
570
 
571
  [package.metadata.requires-dev]