fwerkor commited on
Commit
152d70c
·
1 Parent(s): db4512b

Add interactive trace-grounded CID mechanism replay

Browse files
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .ruff_cache/
README.md CHANGED
@@ -24,6 +24,8 @@ Interactive Hugging Face Space for the 398.8M-parameter `fwerkor/CID-v1-0.4B` ch
24
 
25
  The demo exposes CID's revisable Display and runtime trace with example prompts for the released 0.4B checkpoint. `calculator`, `symbolic_math`, and the task-local workspace are enabled explicitly from the UI. The workspace editor is disabled until its tool switch is enabled; document-backed examples enable it automatically. Workspace mode exposes read-only `workspace_search` and `workspace_read` information needs while reasoning.
26
 
 
 
27
  The model is mechanism-focused, with general-purpose chat outside the primary evaluation target. The Space uses the published unified checkpoint and pins the CID runtime implementation to commit `49c02313044278c93e929834261bebebf3f66bf5`.
28
 
29
  **Paper:** [Continuous Interaction Diffusion](https://arxiv.org/abs/2608.10438)
@@ -38,3 +40,8 @@ The model is mechanism-focused, with general-purpose chat outside the primary ev
38
  - `cid_demo/workspace.py` — task-local document parsing.
39
  - `cid_demo/examples.py` — public examples and workspace fixtures.
40
  - `cid_demo/presentation.py` and `cid_demo/assets/styles.css` — display formatting, HTML, and styling.
 
 
 
 
 
 
24
 
25
  The demo exposes CID's revisable Display and runtime trace with example prompts for the released 0.4B checkpoint. `calculator`, `symbolic_math`, and the task-local workspace are enabled explicitly from the UI. The workspace editor is disabled until its tool switch is enabled; document-backed examples enable it automatically. Workspace mode exposes read-only `workspace_search` and `workspace_read` information needs while reasoning.
26
 
27
+ The **CID in motion** replay sits between the result and detailed inspection tabs. Play or scrub through the current run, jump to tool returns and evidence projections, inspect observed TCT cell states, and compare successive Display snapshots. The shared clock uses measured trace timestamps; playback speed is adjustable. The visualization uses the full runtime events, including asynchronous completion callbacks, without additional model calls. Cell semantics and unrecorded tool contents are not inferred.
28
+
29
  The model is mechanism-focused, with general-purpose chat outside the primary evaluation target. The Space uses the published unified checkpoint and pins the CID runtime implementation to commit `49c02313044278c93e929834261bebebf3f66bf5`.
30
 
31
  **Paper:** [Continuous Interaction Diffusion](https://arxiv.org/abs/2608.10438)
 
40
  - `cid_demo/workspace.py` — task-local document parsing.
41
  - `cid_demo/examples.py` — public examples and workspace fixtures.
42
  - `cid_demo/presentation.py` and `cid_demo/assets/styles.css` — display formatting, HTML, and styling.
43
+ - `cid_demo/mechanism.py` and `cid_demo/assets/mechanism.*` — isolated interactive trace replay, event reducer, and responsive mechanism view.
44
+
45
+ ## Frontend checks
46
+
47
+ Run `python -m pytest tests` and `node --test tests/mechanism-state.test.cjs`. These cover trace preservation, text isolation, asynchronous ordering, lifecycle reconstruction, failure/cancellation, and non-converged runs. The `/run_cid` API retains its first four outputs and appends the replay HTML as its fifth output.
cid_demo/assets/mechanism.css ADDED
@@ -0,0 +1,625 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: light dark;
3
+ --bg: #fff;
4
+ --soft: #f7f8fc;
5
+ --ink: #20263b;
6
+ --muted: #6c7387;
7
+ --line: #e5e8f1;
8
+ --indigo: #6256db;
9
+ --teal: #078b83;
10
+ --amber: #ba7515;
11
+ --purple: #8960c8;
12
+ --active: #7563df;
13
+ --waiting: #e4a443;
14
+ --stable: #37a797;
15
+ --retired: #a8afbf;
16
+ }
17
+ * {
18
+ box-sizing: border-box;
19
+ }
20
+ body {
21
+ margin: 0;
22
+ background: transparent;
23
+ color: var(--ink);
24
+ font:
25
+ 14px/1.5 system-ui,
26
+ -apple-system,
27
+ BlinkMacSystemFont,
28
+ "Segoe UI",
29
+ sans-serif;
30
+ }
31
+ .replay {
32
+ padding: 25px 26px 18px;
33
+ border: 1px solid var(--line);
34
+ border-radius: 20px;
35
+ background: var(--bg);
36
+ overflow: hidden;
37
+ }
38
+ .heading {
39
+ display: flex;
40
+ align-items: flex-start;
41
+ justify-content: space-between;
42
+ gap: 16px;
43
+ }
44
+ .eyebrow {
45
+ font-size: 10px;
46
+ letter-spacing: 0.16em;
47
+ font-weight: 750;
48
+ color: var(--indigo);
49
+ }
50
+ h2 {
51
+ font-size: 27px;
52
+ letter-spacing: -0.035em;
53
+ margin: 3px 0 4px;
54
+ line-height: 1.2;
55
+ }
56
+ p {
57
+ margin: 0;
58
+ color: var(--muted);
59
+ }
60
+ .heading p {
61
+ font-size: 13px;
62
+ }
63
+ .badge {
64
+ font-size: 11px;
65
+ white-space: nowrap;
66
+ border: 1px solid var(--line);
67
+ padding: 5px 10px;
68
+ border-radius: 30px;
69
+ color: var(--muted);
70
+ }
71
+ .question {
72
+ font-size: 13px;
73
+ line-height: 1.5;
74
+ margin: 16px 0;
75
+ color: var(--muted);
76
+ white-space: pre-wrap;
77
+ max-height: 65px;
78
+ overflow: auto;
79
+ overflow-wrap: anywhere;
80
+ padding-left: 11px;
81
+ border-left: 2px solid var(--line);
82
+ }
83
+ .transport {
84
+ display: flex;
85
+ gap: 7px;
86
+ align-items: center;
87
+ }
88
+ button,
89
+ select {
90
+ font: inherit;
91
+ color: var(--ink);
92
+ background: var(--bg);
93
+ border: 1px solid var(--line);
94
+ border-radius: 8px;
95
+ min-height: 32px;
96
+ padding: 5px 11px;
97
+ cursor: pointer;
98
+ }
99
+ button:hover:enabled,
100
+ select:hover {
101
+ border-color: var(--indigo);
102
+ }
103
+ button:disabled {
104
+ opacity: 0.4;
105
+ cursor: default;
106
+ }
107
+ button.primary {
108
+ background: var(--indigo);
109
+ color: white;
110
+ border-color: var(--indigo);
111
+ min-width: 90px;
112
+ }
113
+ button:focus-visible,
114
+ select:focus-visible,
115
+ input:focus-visible,
116
+ summary:focus-visible {
117
+ outline: 2px solid var(--indigo);
118
+ outline-offset: 3px;
119
+ }
120
+ .speed {
121
+ font-size: 11px;
122
+ color: var(--muted);
123
+ display: flex;
124
+ gap: 6px;
125
+ align-items: center;
126
+ margin-left: 6px;
127
+ }
128
+ .speed select {
129
+ font-size: 12px;
130
+ padding: 3px 5px;
131
+ }
132
+ .position {
133
+ margin-left: auto;
134
+ display: flex;
135
+ gap: 12px;
136
+ align-items: baseline;
137
+ font-variant-numeric: tabular-nums;
138
+ }
139
+ .position strong {
140
+ font-size: 13px;
141
+ }
142
+ .position span {
143
+ font-size: 11px;
144
+ color: var(--muted);
145
+ }
146
+ #scrubber {
147
+ width: 100%;
148
+ accent-color: var(--indigo);
149
+ margin: 12px 0 3px;
150
+ cursor: pointer;
151
+ }
152
+ .scene {
153
+ position: relative;
154
+ display: grid;
155
+ grid-template-columns: 26% 34% 28%;
156
+ justify-content: space-between;
157
+ padding: 20px 0 14px;
158
+ min-height: 330px;
159
+ }
160
+ .connections {
161
+ position: absolute;
162
+ inset: 72px 0 0;
163
+ width: 100%;
164
+ height: 230px;
165
+ pointer-events: none;
166
+ z-index: 0;
167
+ overflow: visible;
168
+ }
169
+ .connections text {
170
+ font-size: 9px;
171
+ fill: var(--muted);
172
+ }
173
+ .path {
174
+ fill: none;
175
+ stroke: var(--line);
176
+ stroke-width: 1.5;
177
+ stroke-dasharray: 4 4;
178
+ }
179
+ .path.lit {
180
+ stroke-width: 2.5;
181
+ stroke-dasharray: 7 5;
182
+ }
183
+ .playing .path.lit {
184
+ animation: flow 1s linear infinite;
185
+ }
186
+ .need.lit {
187
+ stroke: var(--amber);
188
+ }
189
+ .evidence.lit {
190
+ stroke: var(--teal);
191
+ }
192
+ .display.lit {
193
+ stroke: var(--purple);
194
+ }
195
+ @keyframes flow {
196
+ to {
197
+ stroke-dashoffset: -24;
198
+ }
199
+ }
200
+ .scene section {
201
+ position: relative;
202
+ z-index: 1;
203
+ min-width: 0;
204
+ }
205
+ .channel-label {
206
+ font-size: 10px;
207
+ font-weight: 750;
208
+ letter-spacing: 0.1em;
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 7px;
212
+ }
213
+ .dot {
214
+ width: 7px;
215
+ height: 7px;
216
+ display: inline-block;
217
+ border-radius: 50%;
218
+ flex-shrink: 0;
219
+ }
220
+ .tool {
221
+ background: var(--teal);
222
+ }
223
+ .thought,
224
+ .active {
225
+ background: var(--active);
226
+ }
227
+ .display {
228
+ background: var(--purple);
229
+ }
230
+ .waiting {
231
+ background: var(--waiting);
232
+ }
233
+ .stable {
234
+ background: var(--stable);
235
+ }
236
+ .retired {
237
+ background: var(--retired);
238
+ }
239
+ h3 {
240
+ font-size: 13px;
241
+ font-weight: 600;
242
+ margin: 6px 0 12px;
243
+ }
244
+ .tools {
245
+ display: flex;
246
+ flex-direction: column;
247
+ gap: 7px;
248
+ max-height: 204px;
249
+ overflow: auto;
250
+ }
251
+ .tool-row {
252
+ background: var(--soft);
253
+ border: 1px solid var(--line);
254
+ border-radius: 10px;
255
+ padding: 9px 10px;
256
+ }
257
+ .tool-row strong {
258
+ display: block;
259
+ font-size: 12px;
260
+ overflow-wrap: anywhere;
261
+ }
262
+ .tool-row span {
263
+ font-size: 11px;
264
+ color: var(--muted);
265
+ }
266
+ .tool-row.busy {
267
+ border-color: var(--amber);
268
+ }
269
+ .tool-row.ready,
270
+ .tool-row.projected {
271
+ border-color: var(--teal);
272
+ }
273
+ .tool-row small {
274
+ display: block;
275
+ font-size: 10px;
276
+ color: var(--muted);
277
+ margin-top: 3px;
278
+ }
279
+ .empty {
280
+ color: var(--muted);
281
+ font-size: 12px;
282
+ padding: 18px 8px;
283
+ border: 1px dashed var(--line);
284
+ border-radius: 10px;
285
+ }
286
+ .field-wrap {
287
+ position: relative;
288
+ }
289
+ .field-wrap svg {
290
+ width: 100%;
291
+ max-height: 217px;
292
+ display: block;
293
+ }
294
+ .field-shell {
295
+ fill: var(--soft);
296
+ stroke: var(--line);
297
+ stroke-width: 1;
298
+ }
299
+ .field-inner {
300
+ fill: none;
301
+ stroke: var(--line);
302
+ stroke-dasharray: 3 6;
303
+ }
304
+ .field-wrap text {
305
+ fill: var(--muted);
306
+ font-size: 11px;
307
+ }
308
+ .cell {
309
+ stroke: var(--bg);
310
+ stroke-width: 2;
311
+ transition:
312
+ fill 0.15s,
313
+ opacity 0.15s;
314
+ cursor: pointer;
315
+ }
316
+ .cell.changed {
317
+ stroke: var(--ink);
318
+ stroke-width: 2.3;
319
+ }
320
+ .cell-label {
321
+ font-size: 7px !important;
322
+ fill: var(--bg) !important;
323
+ pointer-events: none;
324
+ font-weight: 600;
325
+ }
326
+ .model-state {
327
+ position: absolute;
328
+ bottom: 1px;
329
+ left: 50%;
330
+ transform: translateX(-50%);
331
+ font-size: 10px;
332
+ border: 1px solid var(--line);
333
+ border-radius: 20px;
334
+ padding: 2px 9px;
335
+ background: var(--bg);
336
+ white-space: nowrap;
337
+ }
338
+ .legend {
339
+ display: flex;
340
+ justify-content: center;
341
+ gap: 8px;
342
+ flex-wrap: wrap;
343
+ margin: 10px 0 4px;
344
+ font-size: 10px;
345
+ color: var(--muted);
346
+ }
347
+ .legend span {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 4px;
351
+ }
352
+ .micro {
353
+ font-size: 10px;
354
+ color: var(--muted);
355
+ margin-top: 7px;
356
+ line-height: 1.5;
357
+ }
358
+ .thought-area .micro {
359
+ text-align: center;
360
+ }
361
+ .answer {
362
+ border-top: 2px solid var(--purple);
363
+ padding: 13px 0 8px;
364
+ min-height: 98px;
365
+ max-height: 170px;
366
+ overflow: auto;
367
+ white-space: pre-wrap;
368
+ overflow-wrap: anywhere;
369
+ font-size: 23px;
370
+ line-height: 1.4;
371
+ font-weight: 620;
372
+ letter-spacing: -0.02em;
373
+ }
374
+ .answer mark {
375
+ background: #ebe4ff;
376
+ color: inherit;
377
+ border-radius: 3px;
378
+ }
379
+ .tokens {
380
+ display: flex;
381
+ gap: 3px;
382
+ flex-wrap: wrap;
383
+ max-height: 40px;
384
+ overflow: auto;
385
+ margin-top: 10px;
386
+ }
387
+ .token {
388
+ width: 6px;
389
+ height: 12px;
390
+ border-radius: 2px;
391
+ background: var(--line);
392
+ }
393
+ .token.changed {
394
+ background: var(--purple);
395
+ }
396
+ .event-story {
397
+ display: flex;
398
+ align-items: center;
399
+ gap: 13px;
400
+ background: var(--soft);
401
+ padding: 13px 16px;
402
+ border-radius: 11px;
403
+ min-height: 82px;
404
+ border-left: 3px solid var(--indigo);
405
+ }
406
+ .event-icon {
407
+ font-size: 24px;
408
+ color: var(--indigo);
409
+ min-width: 28px;
410
+ text-align: center;
411
+ }
412
+ .event-story strong {
413
+ font-size: 13px;
414
+ }
415
+ .event-story p {
416
+ font-size: 12px;
417
+ margin-top: 3px;
418
+ overflow-wrap: anywhere;
419
+ }
420
+ .timeline-area {
421
+ margin-top: 20px;
422
+ }
423
+ .timeline-heading {
424
+ display: flex;
425
+ justify-content: space-between;
426
+ gap: 10px;
427
+ font-size: 11px;
428
+ }
429
+ .timeline-heading span {
430
+ color: var(--muted);
431
+ font-size: 10px;
432
+ }
433
+ #timeline {
434
+ display: block;
435
+ width: 100%;
436
+ height: 116px;
437
+ margin-top: 8px;
438
+ overflow: visible;
439
+ }
440
+ #timeline text {
441
+ fill: var(--muted);
442
+ font-size: 10px;
443
+ }
444
+ #timeline .tick {
445
+ cursor: pointer;
446
+ }
447
+ #timeline .cursor {
448
+ stroke: var(--ink);
449
+ stroke-width: 1.4;
450
+ }
451
+ .timeline-footer {
452
+ display: flex;
453
+ justify-content: space-between;
454
+ color: var(--muted);
455
+ font-size: 10px;
456
+ gap: 12px;
457
+ }
458
+ .jumps {
459
+ display: flex;
460
+ gap: 6px;
461
+ flex-wrap: wrap;
462
+ margin-top: 14px;
463
+ }
464
+ .jumps button {
465
+ font-size: 11px;
466
+ min-height: 27px;
467
+ padding: 3px 9px;
468
+ }
469
+ details {
470
+ margin-top: 13px;
471
+ font-size: 11px;
472
+ color: var(--muted);
473
+ }
474
+ summary {
475
+ cursor: pointer;
476
+ width: fit-content;
477
+ }
478
+ pre {
479
+ font-size: 11px;
480
+ max-height: 190px;
481
+ overflow: auto;
482
+ background: var(--soft);
483
+ padding: 12px;
484
+ border-radius: 8px;
485
+ white-space: pre-wrap;
486
+ overflow-wrap: anywhere;
487
+ }
488
+ .footnote {
489
+ font-size: 10px;
490
+ line-height: 1.5;
491
+ margin-top: 11px;
492
+ }
493
+ @media (prefers-color-scheme: dark) {
494
+ :root {
495
+ --bg: #111727;
496
+ --soft: #192134;
497
+ --ink: #e5e9f5;
498
+ --muted: #9aa5bd;
499
+ --line: #2b354b;
500
+ --indigo: #978bfa;
501
+ --teal: #43c3b8;
502
+ --amber: #e8b15a;
503
+ --purple: #b498ee;
504
+ }
505
+ .answer mark {
506
+ background: #433761;
507
+ }
508
+ button.primary {
509
+ color: #111727;
510
+ }
511
+ }
512
+ @media (max-width: 650px) {
513
+ .replay {
514
+ padding: 18px 14px 14px;
515
+ border-radius: 14px;
516
+ }
517
+ .heading {
518
+ gap: 7px;
519
+ }
520
+ h2 {
521
+ font-size: 24px;
522
+ }
523
+ .heading p {
524
+ font-size: 12px;
525
+ }
526
+ .badge {
527
+ font-size: 9px;
528
+ padding: 4px 7px;
529
+ }
530
+ .position {
531
+ display: block;
532
+ text-align: right;
533
+ }
534
+ .position span {
535
+ display: block;
536
+ font-size: 9px;
537
+ }
538
+ .speed {
539
+ margin-left: 0;
540
+ }
541
+ .transport {
542
+ gap: 4px;
543
+ }
544
+ button {
545
+ padding: 5px 8px;
546
+ }
547
+ .primary {
548
+ min-width: 70px !important;
549
+ }
550
+ .scene {
551
+ grid-template-columns: 43% 53%;
552
+ padding-top: 16px;
553
+ gap: 10px;
554
+ min-height: 0;
555
+ }
556
+ .thought-area {
557
+ grid-column: 2;
558
+ }
559
+ .tool-area {
560
+ grid-column: 1;
561
+ }
562
+ .display-area {
563
+ grid-column: 1/-1;
564
+ border-top: 1px solid var(--line);
565
+ padding-top: 14px;
566
+ }
567
+ .connections {
568
+ display: none;
569
+ }
570
+ .field-wrap svg {
571
+ max-height: 176px;
572
+ }
573
+ .legend {
574
+ gap: 5px;
575
+ font-size: 9px;
576
+ }
577
+ .answer {
578
+ min-height: 60px;
579
+ max-height: 130px;
580
+ font-size: 23px;
581
+ }
582
+ .display-area h3 {
583
+ margin-bottom: 7px;
584
+ }
585
+ .timeline-heading {
586
+ display: block;
587
+ }
588
+ .timeline-heading span {
589
+ display: block;
590
+ margin-top: 3px;
591
+ }
592
+ .timeline-footer {
593
+ font-size: 9px;
594
+ }
595
+ .event-story {
596
+ margin-top: 12px;
597
+ padding: 12px;
598
+ }
599
+ .event-story p {
600
+ font-size: 11px;
601
+ }
602
+ .tools {
603
+ max-height: 175px;
604
+ }
605
+ .tool-row {
606
+ padding: 8px;
607
+ }
608
+ .channel-label {
609
+ font-size: 9px;
610
+ }
611
+ .scene h3 {
612
+ font-size: 12px;
613
+ }
614
+ .footnote {
615
+ font-size: 9px;
616
+ }
617
+ }
618
+ @media (prefers-reduced-motion: reduce) {
619
+ *,
620
+ *::before,
621
+ *::after {
622
+ animation: none !important;
623
+ transition: none !important;
624
+ }
625
+ }
cid_demo/assets/mechanism.html ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
3
+ <style>/* REPLAY_CSS */</style></head>
4
+ <body>
5
+ <main class="replay" aria-label="CID mechanism replay">
6
+ <header class="heading"><div><div class="eyebrow">INSIDE THE RUN</div><h2>CID in motion</h2>
7
+ <p>Follow the cognitive cells, tool activity and revisable answer through this run.</p></div>
8
+ <span class="badge" id="run-badge">Awaiting a run</span></header>
9
+ <div class="question" id="question">Run a prompt above to explore its actual trajectory.</div>
10
+ <div class="transport">
11
+ <button id="play" class="primary" disabled aria-label="Play replay">▶ Play</button>
12
+ <button id="prev" aria-label="Previous event" disabled>←</button>
13
+ <button id="next" aria-label="Next event" disabled>→</button>
14
+ <label class="speed">Pace <select id="speed" aria-label="Playback pace"><option value="900">Slow</option><option value="380" selected>Normal</option><option value="120">Fast</option></select></label>
15
+ <div class="position"><strong id="step-label">Step —</strong><span id="time-label">0.000 s · event —</span></div>
16
+ </div>
17
+ <input id="scrubber" aria-label="Runtime event" type="range" min="0" max="0" value="0" disabled>
18
+ <div class="scene">
19
+ <svg class="connections" viewBox="0 0 1000 290" preserveAspectRatio="none" aria-hidden="true">
20
+ <defs><marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="context-stroke"/></marker></defs>
21
+ <path id="request-path" class="path need" d="M 420 86 C 340 50 306 58 246 86" marker-end="url(#arrow)"/>
22
+ <path id="return-path" class="path evidence" d="M 246 191 C 319 221 355 221 420 191" marker-end="url(#arrow)"/>
23
+ <path id="display-path" class="path display" d="M 586 140 C 646 140 665 140 724 140" marker-end="url(#arrow)"/>
24
+ <text x="326" y="51" text-anchor="middle">information need</text>
25
+ <text x="326" y="242" text-anchor="middle">evidence → projection</text>
26
+ <text x="656" y="123" text-anchor="middle">materialize</text>
27
+ </svg>
28
+ <section class="tool-area"><div class="channel-label"><i class="dot tool"></i>EXTERNAL TOOLS</div>
29
+ <h3>Reach for information</h3>
30
+ <div id="tools" class="tools"><div class="empty">Tool activity will appear here.</div></div>
31
+ <div id="tool-count" class="micro">Requests and returns from the trace</div>
32
+ </section>
33
+ <section class="thought-area"><div class="channel-label"><i class="dot thought"></i>T · THOUGHT</div>
34
+ <h3>A field of cognitive cells</h3>
35
+ <div class="field-wrap"><svg id="field" viewBox="0 0 300 235" role="img" aria-label="Observed TCT cell lifecycle states">
36
+ <ellipse class="field-shell" cx="150" cy="119" rx="140" ry="108"/>
37
+ <ellipse class="field-inner" cx="150" cy="119" rx="118" ry="89"/>
38
+ <g id="cells"></g><text id="field-empty" x="150" y="122" text-anchor="middle">No cells recorded yet</text>
39
+ </svg><span id="model-state" class="model-state">Waiting to begin</span></div>
40
+ <div class="legend"><span><i class="dot active"></i>Active</span><span><i class="dot waiting"></i>Waiting</span><span><i class="dot stable"></i>Stable</span><span><i class="dot retired"></i>Retired</span></div>
41
+ <div class="micro" id="cell-count">Cell colors follow recorded lifecycle events.</div>
42
+ </section>
43
+ <section class="display-area"><div class="channel-label"><i class="dot display"></i>D · DISPLAY</div>
44
+ <h3>An answer that can change</h3>
45
+ <div id="answer" class="answer">No Display materialized yet.</div>
46
+ <div class="micro" id="display-meta">Newly changed text is highlighted.</div>
47
+ <div id="tokens" class="tokens" aria-label="Recorded Display token positions"></div>
48
+ <div class="micro" id="token-caption"></div>
49
+ </section>
50
+ </div>
51
+ <div class="event-story" aria-live="polite" aria-atomic="true"><span class="event-icon" id="event-icon">○</span><div><strong id="event-title">Your run, made visible</strong><p id="event-copy">Play, pause or scrub through the events after a run finishes.</p></div></div>
52
+ <section class="timeline-area"><div class="timeline-heading"><strong>One shared clock</strong><span>Model work & tools can overlap · click a mark to inspect</span></div>
53
+ <svg id="timeline" viewBox="0 0 1000 116" role="img" aria-label="Model, tool, TCT and Display events on the recorded wall clock"></svg>
54
+ <div class="timeline-footer"><span id="duration-label">Recorded wall time</span><span>Playback pace is illustrative; timestamps are measured.</span></div>
55
+ </section>
56
+ <div class="jumps" id="jumps"></div>
57
+ <details id="event-details"><summary>Inspect this event</summary><pre id="raw-event">No runtime event selected.</pre></details>
58
+ <p class="footnote">Cells show observed lifecycle states, not decoded thoughts. Connections highlight recorded event types; the trace does not expose cell-to-cell semantic links.</p>
59
+ </main>
60
+ <script id="replay-data" type="application/json">/* REPLAY_DATA */</script>
61
+ <script>/* REPLAY_JS */</script>
62
+ </body></html>
cid_demo/assets/mechanism.js ADDED
@@ -0,0 +1,782 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Pure event reducer is also exercised by the Node regression tests. */
2
+ function buildReplay(events) {
3
+ let state = {
4
+ cells: {},
5
+ bindings: {},
6
+ jobs: {},
7
+ display: null,
8
+ previousDisplay: null,
9
+ model: "Waiting to begin",
10
+ needs: 0,
11
+ returns: 0,
12
+ projections: 0,
13
+ done: false,
14
+ };
15
+ const frames = [];
16
+ for (let i = 0; i < events.length; i++) {
17
+ const e = events[i],
18
+ p = e.payload || {},
19
+ k = e.kind;
20
+ state = {
21
+ ...state,
22
+ cells: { ...state.cells },
23
+ bindings: { ...state.bindings },
24
+ jobs: { ...state.jobs },
25
+ };
26
+ const binding = (id, patch) => {
27
+ if (id)
28
+ state.bindings[id] = {
29
+ ...(state.bindings[id] || { source: "Unknown source" }),
30
+ ...Object.fromEntries(
31
+ Object.entries(patch).filter(([, v]) => v !== undefined),
32
+ ),
33
+ };
34
+ };
35
+ const forWork = (key, patch) => {
36
+ if (key)
37
+ for (const [id, b] of Object.entries(state.bindings))
38
+ if (b.work_key === key) binding(id, patch);
39
+ };
40
+ if (k === "cell_allocated")
41
+ state.cells[p.cell_id] = { lifecycle: p.lifecycle, event: i };
42
+ if (k === "lifecycle_transition")
43
+ state.cells[p.cell_id] = { lifecycle: p.current, event: i };
44
+ if (k === "cell_reclaimed") delete state.cells[p.cell_id];
45
+ if (k === "model_step_started") state.model = "Refining";
46
+ if (k === "model_step_finished") {
47
+ state.model = p.converged
48
+ ? "Convergence proposed"
49
+ : p.equilibrium
50
+ ? "At equilibrium"
51
+ : "Step complete";
52
+ if (Object.hasOwn(p, "display_materialized_text")) {
53
+ state.previousDisplay = state.display;
54
+ state.display = {
55
+ text: p.display_materialized_text,
56
+ ids: p.display_token_ids || null,
57
+ step: e.step,
58
+ unresolved: p.display_unresolved,
59
+ };
60
+ }
61
+ }
62
+ if (k === "information_need") state.needs++;
63
+ if (k === "binding_active")
64
+ binding(p.binding_id, {
65
+ source: p.source,
66
+ need_id: p.need_id,
67
+ ...(!state.bindings[p.binding_id] ? { status: "Bound" } : {}),
68
+ });
69
+ if (k === "external_refresh_started" || k === "job_started") {
70
+ binding(p.binding_id, { status: "Running", work_key: p.work_key });
71
+ if (p.work_key)
72
+ state.jobs[p.work_key] = {
73
+ binding_id: p.binding_id,
74
+ status: "Running",
75
+ };
76
+ }
77
+ if (k === "external_refresh_ready") {
78
+ binding(p.binding_id, {
79
+ status: p.success === false ? "Failed" : "Returned",
80
+ work_key: p.work_key,
81
+ });
82
+ if (p.work_key)
83
+ state.jobs[p.work_key] = {
84
+ ...(state.jobs[p.work_key] || {}),
85
+ status: p.success === false ? "Failed" : "Returned",
86
+ };
87
+ }
88
+ if (k === "external_refresh_finished" || k === "job_finished") {
89
+ forWork(p.work_key, { status: "Read complete" });
90
+ state.returns++;
91
+ }
92
+ if (k === "binding_observation_updated" || k === "observation_available")
93
+ binding(p.binding_id, {
94
+ status: "Evidence available",
95
+ work_key: p.work_key,
96
+ version: p.version,
97
+ });
98
+ if (k === "cache_hit") binding(p.binding_id, { status: "Cached evidence" });
99
+ if (k === "cognitive_projection") {
100
+ binding(p.binding_id, { status: "Projected", projection: p.index });
101
+ state.projections++;
102
+ }
103
+ if (k === "job_deduplicated")
104
+ binding(p.binding_id, { status: "Shared job" });
105
+ if (k === "external_refresh_cancelled" || k === "stream_cancelled")
106
+ forWork(p.work_key, { status: "Cancelled" });
107
+ if (k === "quiescence_started") state.model = "Waiting for evidence";
108
+ if (k === "quiescence_resumed") state.model = "Resuming";
109
+ if (k === "loop_escape_applied") state.model = "Re-diffusing";
110
+ if (k === "trajectory_finalized") state.model = "Converged";
111
+ if (k === "trajectory_finished") {
112
+ state.model = p.converged ? "Converged" : "Stopped";
113
+ state.done = true;
114
+ }
115
+ frames.push(state);
116
+ }
117
+ return frames;
118
+ }
119
+ if (typeof module !== "undefined") module.exports = { buildReplay };
120
+
121
+ if (typeof document !== "undefined")
122
+ (() => {
123
+ "use strict";
124
+ const data = JSON.parse(document.getElementById("replay-data").textContent);
125
+ const events = data.events || [],
126
+ frames = buildReplay(events),
127
+ $ = (id) => document.getElementById(id);
128
+ const esc = (x) =>
129
+ String(x ?? "").replace(
130
+ /[&<>"']/g,
131
+ (c) =>
132
+ ({
133
+ "&": "&amp;",
134
+ "<": "&lt;",
135
+ ">": "&gt;",
136
+ '"': "&quot;",
137
+ "'": "&#39;",
138
+ })[c],
139
+ );
140
+ const colors = {
141
+ active: "var(--active)",
142
+ waiting: "var(--waiting)",
143
+ stable: "var(--stable)",
144
+ retired: "var(--retired)",
145
+ empty: "var(--line)",
146
+ };
147
+ let index = 0,
148
+ timer = null,
149
+ selectedCell = null;
150
+ const lastTime = Math.max(
151
+ 0,
152
+ ...events.map((e) => Number(e.timestamp_s) || 0),
153
+ );
154
+ const timeX = (t) => 100 + (884 * (Number(t) || 0)) / (lastTime || 1);
155
+ const lane = (k) =>
156
+ /external|binding|information_need|cache_hit|projection|job_|stream_|observation/.test(
157
+ k,
158
+ )
159
+ ? 1
160
+ : /cell_|lifecycle|compaction|loop_/.test(k)
161
+ ? 2
162
+ : k === "model_step_finished"
163
+ ? 3
164
+ : 0;
165
+ const laneColor = (n) =>
166
+ ["var(--indigo)", "var(--teal)", "var(--amber)", "var(--purple)"][n];
167
+ function story(e, s) {
168
+ const p = e.payload || {},
169
+ source = p.source || s.bindings[p.binding_id]?.source || "the tool";
170
+ const known = {
171
+ trajectory_started: [
172
+ "◎",
173
+ "The trajectory begins",
174
+ "The runtime starts iterative refinement for this prompt.",
175
+ ],
176
+ model_step_started: [
177
+ "↻",
178
+ "Refining the current state",
179
+ p.percepts
180
+ ? String(p.percepts) + " recorded percept(s) enter this model step."
181
+ : "The model updates its cognitive field and Display.",
182
+ ],
183
+ model_step_finished: [
184
+ "✦",
185
+ "The Display is materialized",
186
+ String(p.needs ?? 0) +
187
+ " information need(s) proposed. " +
188
+ (p.converged
189
+ ? "The model proposes convergence; the runtime still checks outstanding work."
190
+ : p.equilibrium
191
+ ? "The model reports equilibrium for its current information."
192
+ : "Compare this snapshot with the previous model step."),
193
+ ],
194
+ information_need: [
195
+ "↗",
196
+ "An information need emerges",
197
+ source +
198
+ " · confidence " +
199
+ (Number.isFinite(p.confidence)
200
+ ? p.confidence.toFixed(3)
201
+ : "not recorded") +
202
+ ". " +
203
+ (p.executable
204
+ ? "This need has the required tool arguments."
205
+ : "This need is not executable at this event."),
206
+ ],
207
+ binding_active: [
208
+ "⇄",
209
+ "A need is bound to a source",
210
+ source + " is bound to " + (p.need_id || "the recorded need") + ".",
211
+ ],
212
+ external_refresh_started: [
213
+ "↗",
214
+ "External work starts",
215
+ "A read is launched for " +
216
+ source +
217
+ ". Subsequent model steps and this read have separate events on the clock.",
218
+ ],
219
+ external_refresh_ready: [
220
+ "↙",
221
+ p.success === false
222
+ ? "The tool read failed"
223
+ : "The tool has returned",
224
+ p.success === false
225
+ ? "The completion callback reports failure."
226
+ : "The asynchronous read is ready. The runtime must still collect and project its observation.",
227
+ ],
228
+ external_refresh_finished: [
229
+ "✓",
230
+ "The runtime collects a result",
231
+ "The completed external read is drained; observation updates identify the receiving bindings.",
232
+ ],
233
+ binding_observation_updated: [
234
+ "↓",
235
+ "Evidence becomes available",
236
+ source +
237
+ " now has a recorded observation. A projection event shows when it is fed into the model.",
238
+ ],
239
+ cognitive_projection: [
240
+ "→",
241
+ "Evidence enters the cognitive field",
242
+ source +
243
+ " · projection " +
244
+ (p.index ?? "—") +
245
+ ". The observation is included in the next model context.",
246
+ ],
247
+ cache_hit: [
248
+ "↙",
249
+ "A cached observation is reused",
250
+ "An existing observation satisfies this binding without a new external read.",
251
+ ],
252
+ job_deduplicated: [
253
+ "⇄",
254
+ "Bindings share external work",
255
+ "This need reuses an already running job.",
256
+ ],
257
+ cell_allocated: [
258
+ "●",
259
+ "A cognitive cell is allocated",
260
+ (p.cell_id || "Cell") +
261
+ " enters the " +
262
+ (p.lifecycle || "recorded") +
263
+ " state.",
264
+ ],
265
+ lifecycle_transition: [
266
+ "◉",
267
+ "A cognitive cell changes state",
268
+ (p.cell_id || "Cell") + ": " + p.previous + " → " + p.current + ".",
269
+ ],
270
+ cell_reclaimed: [
271
+ "○",
272
+ "A retired cell is reclaimed",
273
+ (p.cell_id || "Cell") + " is archived and its slot becomes reusable.",
274
+ ],
275
+ cognitive_compaction: [
276
+ "⋮",
277
+ "The cognitive field is compacted",
278
+ String(p.reclaimed ?? 0) +
279
+ " cell(s) reclaimed; " +
280
+ String(p.empty_slots ?? "—") +
281
+ " empty slots reported.",
282
+ ],
283
+ quiescence_started: [
284
+ "Ⅱ",
285
+ "Model computation pauses",
286
+ "The runtime waits for external progress. Reason: " +
287
+ (p.reason || "not recorded") +
288
+ ".",
289
+ ],
290
+ quiescence_resumed: [
291
+ "↻",
292
+ "External progress resumes the loop",
293
+ "Reason: " +
294
+ (p.reason || "not recorded") +
295
+ ". The next recorded model step shows the continuation.",
296
+ ],
297
+ loop_detected: [
298
+ "↻",
299
+ "A repeated pattern is detected",
300
+ "The runtime records a " +
301
+ (p.mode || "") +
302
+ " cycle of period " +
303
+ (p.period ?? "—") +
304
+ ".",
305
+ ],
306
+ loop_escape_applied: [
307
+ "↻",
308
+ "The runtime re-diffuses selected positions",
309
+ "This is a recovery event. The next model snapshot reveals the updated Display.",
310
+ ],
311
+ trajectory_finalized: [
312
+ "✓",
313
+ "The runtime accepts convergence",
314
+ "The finalization conditions are satisfied.",
315
+ ],
316
+ trajectory_finished: [
317
+ s.model === "Converged" ? "✓" : "■",
318
+ s.model === "Converged"
319
+ ? "The run has converged"
320
+ : "The run stopped without convergence",
321
+ "The replay has reached the recorded end of model execution.",
322
+ ],
323
+ total_compute_budget_exhausted: [
324
+ "■",
325
+ "The model-step budget is reached",
326
+ "The run stops at its configured total compute limit.",
327
+ ],
328
+ compute_budget_exhausted: [
329
+ "■",
330
+ "The compute budget is reached",
331
+ "The runtime records a compute-budget stop.",
332
+ ],
333
+ wall_clock_budget_exhausted: [
334
+ "■",
335
+ "The wall-time budget is reached",
336
+ "The run stops at its configured wall-time limit.",
337
+ ],
338
+ loop_escape_exhausted: [
339
+ "■",
340
+ "Recovery attempts are exhausted",
341
+ "The runtime stops after its configured loop-escape attempts.",
342
+ ],
343
+ };
344
+ return (
345
+ known[e.kind] || [
346
+ "·",
347
+ e.kind.replaceAll("_", " "),
348
+ "Inspect the exact recorded payload below.",
349
+ ]
350
+ );
351
+ }
352
+ function displayMarkup(now, before) {
353
+ if (now === null || now === undefined)
354
+ return "No Display materialized yet.";
355
+ if (!now) return '<span class="micro">(empty Display)</span>';
356
+ const a = Array.from(now),
357
+ b = Array.from(before || "");
358
+ let start = 0,
359
+ end = 0;
360
+ while (start < a.length && start < b.length && a[start] === b[start])
361
+ start++;
362
+ while (
363
+ end < a.length - start &&
364
+ end < b.length - start &&
365
+ a[a.length - 1 - end] === b[b.length - 1 - end]
366
+ )
367
+ end++;
368
+ return (
369
+ esc(a.slice(0, start).join("")) +
370
+ (a.length - start - end
371
+ ? "<mark>" + esc(a.slice(start, a.length - end).join("")) + "</mark>"
372
+ : "") +
373
+ esc(end ? a.slice(-end).join("") : "")
374
+ );
375
+ }
376
+ function renderCells(s, e) {
377
+ const entries = Object.entries(s.cells),
378
+ count = entries.length;
379
+ $("field-empty").style.display = count ? "none" : "";
380
+ $("cells").innerHTML = entries
381
+ .map(([id, c], n) => {
382
+ const angle = n * 2.3999632297,
383
+ r = Math.sqrt((n + 0.5) / Math.max(count, 1));
384
+ const x = 150 + 112 * r * Math.cos(angle),
385
+ y = 112 + 82 * r * Math.sin(angle),
386
+ radius = Math.min(16, Math.max(4.5, 67 / Math.sqrt(count)));
387
+ return (
388
+ '<g data-cell="' +
389
+ esc(id) +
390
+ '" tabindex="0" role="button" aria-label="' +
391
+ esc(id) +
392
+ ": " +
393
+ esc(c.lifecycle) +
394
+ '"><title>' +
395
+ esc(id) +
396
+ " · " +
397
+ esc(c.lifecycle) +
398
+ '</title><circle class="cell ' +
399
+ (c.event === index || id === selectedCell ? "changed" : "") +
400
+ '" cx="' +
401
+ x +
402
+ '" cy="' +
403
+ y +
404
+ '" r="' +
405
+ radius +
406
+ '" fill="' +
407
+ (colors[c.lifecycle] || "var(--line)") +
408
+ '" ' +
409
+ (c.lifecycle === "retired" ? 'opacity=".42"' : "") +
410
+ "/>" +
411
+ (count <= 40
412
+ ? '<text class="cell-label" x="' +
413
+ x +
414
+ '" y="' +
415
+ (y + 2.6) +
416
+ '" text-anchor="middle">' +
417
+ (n + 1) +
418
+ "</text>"
419
+ : "") +
420
+ "</g>"
421
+ );
422
+ })
423
+ .join("");
424
+ const counts = {};
425
+ entries.forEach(
426
+ ([, c]) => (counts[c.lifecycle] = (counts[c.lifecycle] || 0) + 1),
427
+ );
428
+ $("cell-count").textContent = count
429
+ ? Object.entries(counts)
430
+ .map(([k, v]) => v + " " + k)
431
+ .join(" · ")
432
+ : "No cell lifecycle events recorded so far.";
433
+ }
434
+ function renderTools(s) {
435
+ const grouped = {};
436
+ for (const [id, b] of Object.entries(s.bindings))
437
+ (grouped[b.source] ??= []).push([id, b]);
438
+ $("tools").innerHTML =
439
+ Object.entries(grouped)
440
+ .map(([source, items]) => {
441
+ const current =
442
+ items.find(([id]) => id === events[index].payload?.binding_id) ||
443
+ items[items.length - 1];
444
+ const b = current[1],
445
+ busy = items.some(([, x]) => x.status === "Running"),
446
+ cls = busy
447
+ ? "busy"
448
+ : /Projected|Evidence|Cached|Returned|complete/i.test(b.status)
449
+ ? "ready"
450
+ : "";
451
+ return (
452
+ '<div class="tool-row ' +
453
+ cls +
454
+ '"><strong>' +
455
+ esc(source) +
456
+ "</strong><span>" +
457
+ esc(b.status) +
458
+ "</span><small>" +
459
+ items.length +
460
+ " observed binding" +
461
+ (items.length === 1 ? "" : "s") +
462
+ "</small></div>"
463
+ );
464
+ })
465
+ .join("") ||
466
+ '<div class="empty">' +
467
+ (s.done
468
+ ? "No tool binding was recorded in this run."
469
+ : "No tool binding recorded yet.") +
470
+ "</div>";
471
+ $("tool-count").textContent =
472
+ s.needs +
473
+ " needs · " +
474
+ s.returns +
475
+ " reads collected · " +
476
+ s.projections +
477
+ " projections";
478
+ }
479
+ function render() {
480
+ if (!events.length) return;
481
+ const e = events[index],
482
+ p = e.payload || {},
483
+ s = frames[index],
484
+ prev = s.previousDisplay;
485
+ $("scrubber").value = index;
486
+ $("step-label").textContent = "Step " + e.step;
487
+ $("time-label").textContent =
488
+ (Number(e.timestamp_s) || 0).toFixed(3) +
489
+ " s · event " +
490
+ (index + 1) +
491
+ "/" +
492
+ events.length;
493
+ $("prev").disabled = index === 0;
494
+ $("next").disabled = index === events.length - 1;
495
+ $("model-state").textContent = s.model;
496
+ $("run-badge").textContent = s.done ? s.model : "Trace replay";
497
+ const showFinal = s.done && typeof data.final_text === "string";
498
+ const text = showFinal ? data.final_text : s.display?.text;
499
+ $("answer").innerHTML = displayMarkup(text, prev?.text);
500
+ $("display-meta").textContent = showFinal
501
+ ? "Final runtime Display"
502
+ : s.display
503
+ ? "Snapshot at step " +
504
+ s.display.step +
505
+ (text === prev?.text
506
+ ? " · unchanged"
507
+ : text?.length < (prev?.text || "").length
508
+ ? " · revised / shortened"
509
+ : " · revised")
510
+ : "Awaiting the first recorded model snapshot.";
511
+ $("tokens").innerHTML = (s.display?.ids || [])
512
+ .map(
513
+ (id, i) =>
514
+ '<span class="token ' +
515
+ (id !== prev?.ids?.[i] ? "changed" : "") +
516
+ '" title="Position ' +
517
+ i +
518
+ " · token ID " +
519
+ id +
520
+ '"></span>',
521
+ )
522
+ .join("");
523
+ $("token-caption").textContent = s.display?.ids
524
+ ? String(s.display.ids.length) +
525
+ " recorded token positions · colored = changed ID"
526
+ : "";
527
+ renderCells(s, e);
528
+ renderTools(s);
529
+ const need =
530
+ /information_need|binding_active|external_refresh_started/.test(
531
+ e.kind,
532
+ ),
533
+ ret =
534
+ /binding_observation_updated|cache_hit|cognitive_projection|external_refresh_ready/.test(
535
+ e.kind,
536
+ );
537
+ $("request-path").classList.toggle("lit", need);
538
+ $("return-path").classList.toggle("lit", ret);
539
+ $("display-path").classList.toggle(
540
+ "lit",
541
+ e.kind === "model_step_finished",
542
+ );
543
+ const [icon, title, copy] = story(e, s);
544
+ $("event-icon").textContent = icon;
545
+ $("event-title").textContent = title;
546
+ $("event-copy").textContent = copy;
547
+ $("raw-event").textContent = JSON.stringify(
548
+ { event: index + 1, ...e },
549
+ null,
550
+ 2,
551
+ );
552
+ const cursor = $("clock-cursor");
553
+ if (cursor) {
554
+ cursor.setAttribute("x1", timeX(e.timestamp_s));
555
+ cursor.setAttribute("x2", timeX(e.timestamp_s));
556
+ }
557
+ resize();
558
+ }
559
+ function pause() {
560
+ clearTimeout(timer);
561
+ timer = null;
562
+ document.querySelector(".replay").classList.remove("playing");
563
+ $("play").textContent =
564
+ index === events.length - 1 ? "↺ Replay" : "▶ Play";
565
+ $("play").setAttribute(
566
+ "aria-label",
567
+ index === events.length - 1 ? "Replay from start" : "Play replay",
568
+ );
569
+ }
570
+ function schedule() {
571
+ document.querySelector(".replay").classList.add("playing");
572
+ timer = setTimeout(
573
+ () => {
574
+ if (index >= events.length - 1) {
575
+ pause();
576
+ return;
577
+ }
578
+ index++;
579
+ render();
580
+ schedule();
581
+ },
582
+ Number($("speed").value),
583
+ );
584
+ }
585
+ function seek(i) {
586
+ pause();
587
+ index = Math.max(0, Math.min(events.length - 1, i));
588
+ render();
589
+ }
590
+ function makeTimeline() {
591
+ const labels = ["Model", "Tools", "TCT", "Display"];
592
+ let markup = labels
593
+ .map(
594
+ (label, n) =>
595
+ '<text x="0" y="' +
596
+ (18 + n * 24) +
597
+ '">' +
598
+ label +
599
+ '</text><line x1="100" x2="984" y1="' +
600
+ (14 + n * 24) +
601
+ '" y2="' +
602
+ (14 + n * 24) +
603
+ '" stroke="var(--line)"/>',
604
+ )
605
+ .join("");
606
+ let pending = null;
607
+ const reads = new Map();
608
+ events.forEach((e, i) => {
609
+ const p = e.payload || {};
610
+ if (e.kind === "external_refresh_started" && p.work_key)
611
+ reads.set(p.work_key, e);
612
+ if (
613
+ (e.kind === "external_refresh_ready" ||
614
+ e.kind === "external_refresh_finished") &&
615
+ reads.has(p.work_key)
616
+ ) {
617
+ const start = reads.get(p.work_key);
618
+ reads.delete(p.work_key);
619
+ markup +=
620
+ '<rect x="' +
621
+ timeX(start.timestamp_s) +
622
+ '" y="33" width="' +
623
+ Math.max(1, timeX(e.timestamp_s) - timeX(start.timestamp_s)) +
624
+ '" height="10" rx="2" fill="var(--teal)" opacity=".3"><title>External read · ' +
625
+ (Number(e.timestamp_s) - Number(start.timestamp_s)).toFixed(3) +
626
+ " s</title></rect>";
627
+ }
628
+ if (e.kind === "model_step_started") pending = e;
629
+ if (e.kind === "model_step_finished" && pending) {
630
+ markup +=
631
+ '<rect x="' +
632
+ timeX(pending.timestamp_s) +
633
+ '" y="9" width="' +
634
+ Math.max(1, timeX(e.timestamp_s) - timeX(pending.timestamp_s)) +
635
+ '" height="10" rx="2" fill="var(--indigo)" opacity=".2"/>';
636
+ pending = null;
637
+ }
638
+ const n = lane(e.kind);
639
+ markup +=
640
+ '<circle class="tick" data-event="' +
641
+ i +
642
+ '" tabindex="0" role="button" aria-label="Event ' +
643
+ (i + 1) +
644
+ ": " +
645
+ esc(e.kind) +
646
+ '" cx="' +
647
+ timeX(e.timestamp_s) +
648
+ '" cy="' +
649
+ (14 + n * 24) +
650
+ '" r="3" fill="' +
651
+ laneColor(n) +
652
+ '"><title>' +
653
+ esc(e.kind) +
654
+ " · " +
655
+ Number(e.timestamp_s || 0).toFixed(3) +
656
+ " s</title></circle>";
657
+ });
658
+ markup +=
659
+ '<line id="clock-cursor" class="cursor" x1="100" x2="100" y1="1" y2="97"/><text x="100" y="113">0 s</text><text x="984" y="113" text-anchor="end">' +
660
+ lastTime.toFixed(3) +
661
+ " s</text>";
662
+ $("timeline").innerHTML = markup;
663
+ $("duration-label").textContent =
664
+ events.length + " events · " + lastTime.toFixed(3) + " s recorded";
665
+ const jumps = [
666
+ ["First need", (e) => e.kind === "information_need"],
667
+ [
668
+ "Tool return",
669
+ (e) =>
670
+ e.kind === "external_refresh_ready" ||
671
+ e.kind === "external_refresh_finished",
672
+ ],
673
+ ["Evidence in", (e) => e.kind === "cognitive_projection"],
674
+ [
675
+ "First revision",
676
+ (e, i) =>
677
+ e.kind === "model_step_finished" &&
678
+ frames[i].previousDisplay &&
679
+ frames[i].display?.text !== frames[i].previousDisplay?.text,
680
+ ],
681
+ ["Recovery", (e) => e.kind === "loop_escape_applied"],
682
+ ["Run end", (e) => e.kind === "trajectory_finished"],
683
+ ];
684
+ $("jumps").innerHTML = jumps
685
+ .map(([label, test]) => {
686
+ const i = events.findIndex(test);
687
+ return i < 0
688
+ ? ""
689
+ : '<button data-jump="' + i + '">' + label + "</button>";
690
+ })
691
+ .join("");
692
+ }
693
+ function resize() {
694
+ requestAnimationFrame(() =>
695
+ parent.postMessage(
696
+ {
697
+ type: "cid-replay-height",
698
+ height:
699
+ Math.ceil(
700
+ document.querySelector(".replay").getBoundingClientRect()
701
+ .height,
702
+ ) + 2,
703
+ },
704
+ "*",
705
+ ),
706
+ );
707
+ }
708
+ $("play").addEventListener("click", () => {
709
+ if (timer) {
710
+ pause();
711
+ return;
712
+ }
713
+ if (index === events.length - 1) index = 0;
714
+ render();
715
+ $("play").textContent = "Ⅱ Pause";
716
+ $("play").setAttribute("aria-label", "Pause replay");
717
+ schedule();
718
+ });
719
+ $("prev").addEventListener("click", () => seek(index - 1));
720
+ $("next").addEventListener("click", () => seek(index + 1));
721
+ $("scrubber").addEventListener("input", () =>
722
+ seek(Number($("scrubber").value)),
723
+ );
724
+ $("speed").addEventListener("change", () => {
725
+ if (timer) {
726
+ clearTimeout(timer);
727
+ schedule();
728
+ }
729
+ });
730
+ $("timeline").addEventListener("click", (e) => {
731
+ const mark = e.target.closest("[data-event]");
732
+ if (mark) seek(Number(mark.dataset.event));
733
+ });
734
+ $("timeline").addEventListener("keydown", (e) => {
735
+ if (e.key === "Enter" || e.key === " ") {
736
+ const mark = e.target.closest("[data-event]");
737
+ if (mark) {
738
+ e.preventDefault();
739
+ seek(Number(mark.dataset.event));
740
+ }
741
+ }
742
+ });
743
+ $("jumps").addEventListener("click", (e) => {
744
+ const b = e.target.closest("[data-jump]");
745
+ if (b) seek(Number(b.dataset.jump));
746
+ });
747
+ function selectCell(e) {
748
+ const g = e.target.closest("[data-cell]");
749
+ if (!g) return;
750
+ selectedCell = g.dataset.cell;
751
+ pause();
752
+ const s = frames[index];
753
+ $("event-title").textContent = "Cognitive cell · " + selectedCell;
754
+ $("event-copy").textContent =
755
+ "Last observed lifecycle: " +
756
+ s.cells[selectedCell].lifecycle +
757
+ ". Its latent semantic content is not recorded.";
758
+ renderCells(s, events[index]);
759
+ }
760
+ $("cells").addEventListener("click", selectCell);
761
+ $("cells").addEventListener("keydown", (e) => {
762
+ if (e.key === "Enter" || e.key === " ") {
763
+ e.preventDefault();
764
+ selectCell(e);
765
+ }
766
+ });
767
+ document.addEventListener("visibilitychange", () => {
768
+ if (document.hidden) pause();
769
+ });
770
+ $("event-details").addEventListener("toggle", resize);
771
+ window.addEventListener("resize", resize);
772
+ new ResizeObserver(resize).observe(document.querySelector(".replay"));
773
+ if (events.length) {
774
+ $("question").textContent = data.prompt || "Recorded CID trajectory";
775
+ $("play").disabled = false;
776
+ $("scrubber").disabled = false;
777
+ $("scrubber").max = events.length - 1;
778
+ makeTimeline();
779
+ render();
780
+ }
781
+ resize();
782
+ })();
cid_demo/assets/styles.css CHANGED
@@ -188,3 +188,9 @@
188
  grid-template-columns: repeat(2, minmax(0, 1fr));
189
  }
190
  }
 
 
 
 
 
 
 
188
  grid-template-columns: repeat(2, minmax(0, 1fr));
189
  }
190
  }
191
+
192
+ #cid-mechanism { margin-top: 24px; }
193
+ @media (max-width: 480px) {
194
+ #prompt-panel, #result-panel { min-width: 0 !important; }
195
+ #cid-hero .hero-title { font-size: 34px; }
196
+ }
cid_demo/mechanism.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Self-contained, trace-grounded CID replay; no model work during playback."""
2
+ from __future__ import annotations
3
+
4
+ import html
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ _ASSETS = Path(__file__).parent / "assets"
10
+
11
+ # The opaque-origin frame can execute only its own bundled visualization code.
12
+ # Match the sending window before accepting its responsive height.
13
+ REPLAY_BRIDGE_JS = """() => {
14
+ if (window.__cidReplayResize) return;
15
+ window.__cidReplayResize = true;
16
+ window.addEventListener("message", (event) => {
17
+ if (event.data?.type !== "cid-replay-height") return;
18
+ for (const frame of document.querySelectorAll("iframe.cid-replay-frame")) {
19
+ if (frame.contentWindow === event.source) {
20
+ const height = Number(event.data.height);
21
+ if (Number.isFinite(height)) frame.style.height = Math.min(2400, Math.max(520, height)) + "px";
22
+ }
23
+ }
24
+ });
25
+ }"""
26
+
27
+
28
+ def replay_document(events: tuple[dict[str, Any], ...] = (), *, prompt: str = "", final_text: str = "") -> str:
29
+ # Escape script terminators as well as the outer srcdoc attribute. All runtime
30
+ # strings are rendered as text by the client, never inserted as trusted HTML.
31
+ data = json.dumps(
32
+ {"events": list(events), "prompt": prompt, "final_text": final_text},
33
+ ensure_ascii=False, separators=(",", ":"), default=str,
34
+ ).replace("&", "\\u0026").replace("<", "\\u003c").replace(">", "\\u003e")
35
+ template = (_ASSETS / "mechanism.html").read_text(encoding="utf-8")
36
+ return (template.replace("/* REPLAY_CSS */", (_ASSETS / "mechanism.css").read_text(encoding="utf-8"))
37
+ .replace("/* REPLAY_JS */", (_ASSETS / "mechanism.js").read_text(encoding="utf-8"))
38
+ .replace("/* REPLAY_DATA */", data))
39
+
40
+
41
+ def mechanism_html(events: tuple[dict[str, Any], ...] = (), *, prompt: str = "", final_text: str = "") -> str:
42
+ return (
43
+ '<iframe class="cid-replay-frame" title="CID mechanism replay" '
44
+ 'sandbox="allow-scripts" referrerpolicy="no-referrer" '
45
+ 'style="width:100%;height:820px;border:0;display:block" srcdoc="'
46
+ + html.escape(replay_document(events, prompt=prompt, final_text=final_text), quote=True)
47
+ + '"></iframe>'
48
+ )
cid_demo/runtime.py CHANGED
@@ -20,6 +20,7 @@ from .config import (
20
  MODEL_REVISION,
21
  )
22
  from .model import MODEL_BUNDLE
 
23
  from .presentation import display_evolution, summary_html, trace_rows
24
  from .tools import (
25
  LIVE_MATH_DESCRIPTORS,
@@ -184,4 +185,5 @@ def run_cid(
184
  summary_html(summary),
185
  display_evolution(events),
186
  trace_rows(events),
 
187
  )
 
20
  MODEL_REVISION,
21
  )
22
  from .model import MODEL_BUNDLE
23
+ from .mechanism import mechanism_html
24
  from .presentation import display_evolution, summary_html, trace_rows
25
  from .tools import (
26
  LIVE_MATH_DESCRIPTORS,
 
185
  summary_html(summary),
186
  display_evolution(events),
187
  trace_rows(events),
188
+ mechanism_html(events, prompt=prompt, final_text=result.final_text or ""),
189
  )
cid_demo/ui.py CHANGED
@@ -13,6 +13,7 @@ from .config import (
13
  from .examples import EXAMPLES, WORKSPACE_PLACEHOLDER
14
  from .presentation import CUSTOM_CSS, FOOTER_HTML, HERO_HTML, READY_SUMMARY_HTML
15
  from .runtime import run_cid
 
16
 
17
 
18
  @dataclass(frozen=True)
@@ -36,6 +37,7 @@ class DemoOutputs:
36
  summary: gr.HTML
37
  display_evolution: gr.Markdown
38
  trace: gr.Dataframe
 
39
 
40
 
41
  def _build_input_panel() -> tuple[DemoInputs, gr.Button]:
@@ -193,6 +195,7 @@ def build_demo() -> gr.Blocks:
193
  with gr.Blocks(
194
  title="CID · Demo",
195
  css=CUSTOM_CSS,
 
196
  theme=gr.themes.Soft(
197
  primary_hue=gr.themes.colors.indigo,
198
  neutral_hue=gr.themes.colors.slate,
@@ -206,8 +209,9 @@ def build_demo() -> gr.Blocks:
206
  with gr.Column(scale=5, min_width=360, elem_id="result-panel"):
207
  final_display, summary = _build_result_panel()
208
 
 
209
  display_evolution, trace = _build_inspection_tabs()
210
- outputs = DemoOutputs(final_display, summary, display_evolution, trace)
211
 
212
  run_button.click(
213
  fn=run_cid,
@@ -229,6 +233,7 @@ def build_demo() -> gr.Blocks:
229
  outputs.summary,
230
  outputs.display_evolution,
231
  outputs.trace,
 
232
  ],
233
  concurrency_limit=1,
234
  api_name="run_cid",
 
13
  from .examples import EXAMPLES, WORKSPACE_PLACEHOLDER
14
  from .presentation import CUSTOM_CSS, FOOTER_HTML, HERO_HTML, READY_SUMMARY_HTML
15
  from .runtime import run_cid
16
+ from .mechanism import REPLAY_BRIDGE_JS, mechanism_html
17
 
18
 
19
  @dataclass(frozen=True)
 
37
  summary: gr.HTML
38
  display_evolution: gr.Markdown
39
  trace: gr.Dataframe
40
+ mechanism: gr.HTML
41
 
42
 
43
  def _build_input_panel() -> tuple[DemoInputs, gr.Button]:
 
195
  with gr.Blocks(
196
  title="CID · Demo",
197
  css=CUSTOM_CSS,
198
+ js=REPLAY_BRIDGE_JS,
199
  theme=gr.themes.Soft(
200
  primary_hue=gr.themes.colors.indigo,
201
  neutral_hue=gr.themes.colors.slate,
 
209
  with gr.Column(scale=5, min_width=360, elem_id="result-panel"):
210
  final_display, summary = _build_result_panel()
211
 
212
+ mechanism = gr.HTML(mechanism_html(), elem_id="cid-mechanism")
213
  display_evolution, trace = _build_inspection_tabs()
214
+ outputs = DemoOutputs(final_display, summary, display_evolution, trace, mechanism)
215
 
216
  run_button.click(
217
  fn=run_cid,
 
233
  outputs.summary,
234
  outputs.display_evolution,
235
  outputs.trace,
236
+ outputs.mechanism,
237
  ],
238
  concurrency_limit=1,
239
  api_name="run_cid",
tests/mechanism-state.test.cjs ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const test = require("node:test");
2
+ const assert = require("node:assert/strict");
3
+ const { buildReplay } = require("../cid_demo/assets/mechanism.js");
4
+ const event = (kind, payload = {}, step = 0, timestamp_s = 0) => ({
5
+ kind,
6
+ payload,
7
+ step,
8
+ timestamp_s,
9
+ });
10
+ test("async completion stays in event order even when callback step goes backwards", () => {
11
+ const f = buildReplay([
12
+ event("binding_active", { binding_id: "b", source: "calculator" }),
13
+ event(
14
+ "external_refresh_started",
15
+ { binding_id: "b", work_key: "w" },
16
+ 0,
17
+ 0.1,
18
+ ),
19
+ event("model_step_started", {}, 1, 0.2),
20
+ event(
21
+ "external_refresh_ready",
22
+ { binding_id: "b", work_key: "w", success: true },
23
+ 0,
24
+ 0.3,
25
+ ),
26
+ event(
27
+ "binding_observation_updated",
28
+ { binding_id: "b", work_key: "w" },
29
+ 1,
30
+ 0.4,
31
+ ),
32
+ event("cognitive_projection", { binding_id: "b", index: 1 }, 2, 0.5),
33
+ ]);
34
+ assert.equal(f[2].bindings.b.status, "Running");
35
+ assert.equal(f[3].model, "Refining");
36
+ assert.equal(f[3].bindings.b.status, "Returned");
37
+ assert.equal(f[3].projections, 0);
38
+ assert.equal(f[5].projections, 1);
39
+ assert.equal(f[0].bindings.b.status, "Bound");
40
+ });
41
+ test("cell retirement and reclamation preserve earlier snapshots", () => {
42
+ const f = buildReplay([
43
+ event("cell_allocated", { cell_id: "c1", lifecycle: "active" }),
44
+ event("lifecycle_transition", {
45
+ cell_id: "c1",
46
+ previous: "active",
47
+ current: "retired",
48
+ }),
49
+ event("cell_reclaimed", { cell_id: "c1" }),
50
+ ]);
51
+ assert.equal(f[0].cells.c1.lifecycle, "active");
52
+ assert.equal(f[1].cells.c1.lifecycle, "retired");
53
+ assert.deepEqual(f[2].cells, {});
54
+ });
55
+ test("failed reads, cache reuse, and cancellation do not create projections", () => {
56
+ const f = buildReplay([
57
+ event("binding_active", { binding_id: "b", source: "calculator" }),
58
+ event("external_refresh_started", { binding_id: "b", work_key: "w" }),
59
+ event("external_refresh_ready", { binding_id: "b", success: false }),
60
+ event("external_refresh_cancelled", { work_key: "w" }),
61
+ event("cache_hit", { binding_id: "b" }),
62
+ ]);
63
+ assert.equal(f[2].bindings.b.status, "Failed");
64
+ assert.equal(f[3].bindings.b.status, "Cancelled");
65
+ assert.equal(f[4].bindings.b.status, "Cached evidence");
66
+ assert.equal(f[4].projections, 0);
67
+ });
68
+ test("Display snapshots remain distinct from convergence and final status", () => {
69
+ const f = buildReplay([
70
+ event("model_step_finished", {
71
+ display_materialized_text: "12.",
72
+ display_token_ids: [1, 2],
73
+ converged: true,
74
+ }),
75
+ event("model_step_finished", {
76
+ display_materialized_text: "12",
77
+ display_token_ids: [1],
78
+ converged: false,
79
+ }),
80
+ event("trajectory_finished", { converged: false }),
81
+ ]);
82
+ assert.equal(f[0].done, false);
83
+ assert.equal(f[1].previousDisplay.text, "12.");
84
+ assert.equal(f[1].display.text, "12");
85
+ assert.equal(f[2].model, "Stopped");
86
+ assert.equal(f[2].done, true);
87
+ assert.deepEqual(f[2].bindings, {});
88
+ });
89
+ test("empty and unknown events are safe; no fictitious cells or bindings", () => {
90
+ assert.deepEqual(buildReplay([]), []);
91
+ const [f] = buildReplay([event("future_runtime_event", { cell_id: "x" })]);
92
+ assert.deepEqual(f.cells, {});
93
+ assert.deepEqual(f.bindings, {});
94
+ });
tests/test_mechanism.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import html
2
+ import json
3
+ from html.parser import HTMLParser
4
+
5
+ from cid_demo.mechanism import mechanism_html, replay_document
6
+
7
+
8
+ class FrameParser(HTMLParser):
9
+ def handle_starttag(self, tag, attrs):
10
+ if tag == "iframe":
11
+ self.attrs = dict(attrs)
12
+
13
+
14
+ def test_full_trace_and_markup_are_isolated():
15
+ value = '</script><img src=x onerror="alert(1)"> & /* REPLAY_JS */'
16
+ events = [{"step": 0, "timestamp_s": 0, "kind": "model_step_finished",
17
+ "payload": {"display_materialized_text": value, "long": "x" * 6000}}]
18
+ frame = FrameParser()
19
+ frame.feed(mechanism_html(events, prompt=value, final_text=value))
20
+ assert frame.attrs["sandbox"] == "allow-scripts"
21
+ doc = frame.attrs["srcdoc"]
22
+ data = doc.split('<script id="replay-data" type="application/json">', 1)[1].split("</script>", 1)[0]
23
+ parsed = json.loads(data)
24
+ assert parsed["events"] == events
25
+ assert parsed["prompt"] == value
26
+ assert parsed["final_text"] == value
27
+ assert "<img" not in doc
28
+ assert "onerror" in data # Preserved as text, never trusted markup.
29
+
30
+
31
+ def test_empty_trace_has_disabled_controls_and_no_demo_trajectory():
32
+ doc = replay_document()
33
+ assert 'id="play" class="primary" disabled' in doc
34
+ assert '"events":[]' in doc
35
+ assert 'No cells recorded yet' in doc