Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c556f0a
feat(visual): tetrahedral spatial formalism — principled 3D scroom la…
ryanklee May 22, 2026
7632445
fix(visual): audit remediation — texture leak, warmth corruption, tes…
ryanklee May 22, 2026
7e0a6aa
fix(visual): observation round 1 — normalize content quad scale hiera…
ryanklee May 22, 2026
a6d539e
fix(visual): observation round 3 — spread bottom-heavy anchors laterally
ryanklee May 22, 2026
221e914
fix(visual): two-band arc layout — all wards visible, legible at stre…
ryanklee May 22, 2026
0b7fff6
feat(visual): semantic layout — perception left, cognition right, com…
ryanklee May 22, 2026
bcb979f
fix(visual): raise grounding sources above floor plane
ryanklee May 22, 2026
b302edc
fix(visual): grounding sources in front of AoA — no floor grid overlap
ryanklee May 22, 2026
bf420a5
fix(visual): unify right column — no separate grounding region
ryanklee May 22, 2026
d65efa5
feat(visual): use full scroom volume — semantic regions spread across…
ryanklee May 22, 2026
8209de9
feat(visual): garden spline camera path — viewer walks through the sc…
ryanklee May 23, 2026
03ad390
feat(visual): garden clumps — content positioned at path stations for…
ryanklee May 23, 2026
13e920a
feat(visual): enable DoF post-process on RTX 5090
ryanklee May 23, 2026
32bd6e5
revert(visual): disable DoF again — crashes on 5090 too (NVIDIA 595.71)
ryanklee May 23, 2026
add4ff1
feat(visual): miegakure visibility gating + stimmung-driven camera en…
ryanklee May 23, 2026
a9894cc
feat(visual): helical spiral camera path — first step toward the Screwm
ryanklee May 23, 2026
78636c4
feat(visual): expand room to tower height (y: -2 to 13)
ryanklee May 23, 2026
c8b4bcf
feat(visual): Screwm tower levels — 42 sources across 5 ascending levels
ryanklee May 23, 2026
ce70697
feat(visual): ray-marched cylindrical tower wall — the Screwm takes s…
ryanklee May 23, 2026
36bb44f
fix(visual): fix cylinder wall warping + slow down spiral
ryanklee May 23, 2026
4067e50
feat(visual): octagonal tower replaces broken cylinder ray-march
ryanklee May 23, 2026
995c536
feat(visual): spiral ramp replaces full-width platforms
ryanklee May 23, 2026
0296b07
fix(visual): halve ramp steepness — 180° arc per level instead of 90°
ryanklee May 23, 2026
3d0a37c
fix(visual): ramp planks aligned with wall panels
ryanklee May 23, 2026
26857dd
fix(visual): ramp shelves are flat horizontal platforms at the wall
ryanklee May 23, 2026
7b51bfe
fix(visual): wider ramp shelves (7.0 units) for consistent visibility
ryanklee May 23, 2026
f5dfe94
feat(visual): widen Screwm 30% — tower radius 6→7.8, content 5.5→7.2
ryanklee May 23, 2026
0ee33ee
chore: whitelist write_aperture_snapshot for vulture
ryanklee May 23, 2026
a7e226b
style: format test_video_attention + test_audio_visual_correlation
ryanklee May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agents/reverie/_uniforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def write_uniforms(
dim_data = stimmung.get(dim_key, {})
if isinstance(dim_data, dict):
worst_infra = max(worst_infra, dim_data.get("value", 0.0))
uniforms["signal.color_warmth"] = worst_infra
uniforms["signal.color_warmth"] = min(worst_infra * 0.35, 0.25)

# U8 Phase 1 — apply per-mode palette tint to colorgrade.hue_rotate
# BEFORE homage damping so an active homage package's hue rotation
Expand Down
5 changes: 5 additions & 0 deletions agents/shaders/nodes/colorgrade.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ fn main_1() {
let _e116 = clamp(_e110.xyz, vec3(0f), vec3(1f));
let _e117 = color;
graded = mix(_e117.xyz, _e116, vec3(surface_presence));
// Preserve entity color identity: blend graded result back toward
// original so entity hue always shows through the color grade.
let original = textureSample(tex, tex_sampler, v_texcoord_1).xyz;
let entity_preserve = 0.90;
graded = mix(graded, original, vec3(entity_preserve * surface_presence));
fragColor = vec4<f32>(graded.x, graded.y, graded.z, _e117.w);
return;
}
Expand Down
8 changes: 8 additions & 0 deletions agents/shaders/nodes/content_layer.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ fn main_1() {
let trace_boost = dwelling_trace_boost(max_salience);
base *= trace_boost;

// Entity luminance preservation: blend output luma toward source luma
// so entity contrast structure survives downstream effects.
let out_luma = dot(base, vec3<f32>(0.299, 0.587, 0.114));
let src_luma = dot(base_sample.rgb, vec3<f32>(0.299, 0.587, 0.114));
let luma_preserve = mix(out_luma, src_luma, 0.35);
let luma_ratio = select(luma_preserve / max(out_luma, 0.001), 1.0, out_luma < 0.001);
base = base * clamp(luma_ratio, 0.7, 1.4);

fragColor = vec4<f32>(base, base_sample.a);
return;
}
Expand Down
6 changes: 5 additions & 1 deletion agents/shaders/nodes/feedback.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ fn main_1() {
// broadcast. Even if an upstream controller requests an additive/screen
// mode, bound the accumulator's authority so it cannot flood the whole
// output plane or create hard luma drops when feedback returns to zero.
let feedback_weight = clamp((global.u_decay * 3.0f) + (global.u_trace_strength * 0.12f), 0.0f, 0.16f);
let base_feedback_weight = clamp((global.u_decay * 3.0f) + (global.u_trace_strength * 0.12f), 0.0f, 0.16f);
// Bright entities resist feedback — preserve their visual identity.
let source_luma = dot(current.xyz, vec3<f32>(0.299, 0.587, 0.114));
let luma_resistance = smoothstep(0.15, 0.55, source_luma);
let feedback_weight = base_feedback_weight * (1.0 - luma_resistance * 0.7);
let source_bound_feedback = mix(current.xyz, r, vec3<f32>(feedback_weight));
let _e215 = clamp(source_bound_feedback, vec3(0f), vec3(1f));
fragColor = vec4<f32>(_e215.x, _e215.y, _e215.z, current.a);
Expand Down
20 changes: 20 additions & 0 deletions agents/shaders/nodes/postprocess.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ fn main_1() {
// Master opacity gate — black when nothing is recruited
c = vec4<f32>(c.xyz * global.u_master_opacity, c.w);

// Anti-monotonicity: ensure chromatic variance and local contrast.
// If the output is converging toward monochrome, inject subtle
// complementary hue variation based on screen position.
let luma = dot(c.xyz, vec3<f32>(0.299, 0.587, 0.114));
let chroma = length(c.xyz - vec3<f32>(luma));
let mono_risk = smoothstep(0.06, 0.01, chroma);
let pos_hue = fract(v_texcoord_1.x * 0.31 + v_texcoord_1.y * 0.17);
let h6 = pos_hue * 6.0;
let inject = vec3<f32>(
clamp(abs(h6 - 3.0) - 1.0, 0.0, 1.0),
clamp(2.0 - abs(h6 - 2.0), 0.0, 1.0),
clamp(2.0 - abs(h6 - 4.0), 0.0, 1.0),
) * luma;
c = vec4<f32>(c.xyz + inject * mono_risk * 0.28, c.w);

// Brightness floor — prevent total darkness during heavy effects.
let final_luma = dot(c.xyz, vec3<f32>(0.299, 0.587, 0.114));
let brightness_lift = smoothstep(0.04, 0.0, final_luma) * 0.06;
c = vec4<f32>(c.xyz + vec3<f32>(brightness_lift), c.w);

fragColor = c;
return;
}
Expand Down
295 changes: 295 additions & 0 deletions agents/studio_compositor/aoa_heatmap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
"""AoA impingement-recruitment heatmap — maps live system activity to 340 panes.

Reads impingements from /dev/shm/hapax-dmn/impingements.jsonl, accumulates
per-pane heat with exponential decay, writes a flat f32 array to SHM for
the Rust renderer to consume.

Mapping hierarchy (depth → abstraction):
Depth 0 (4 panes): macro-domains
Face 0 (abd): Composition — camera, composition, attention, transition
Face 1 (bcd): Modulation — mood, intensity, pace, silence
Face 2 (cad): Surface — ward, homage, overlay, chrome
Face 3 (acb): Programme — gem, preset, youtube, programme

Depth 1 (16 panes): individual intent families within each domain
Depth 2 (64 panes): per-family × material (void/air/water/earth/fire)
Depth 3 (256 panes): per-family × dimension temporal heat

Each pane stores: heat (0-1), hue (0-1), saturation (0-1).
Heat decays exponentially with a configurable half-life.
"""

from __future__ import annotations

import json
import logging
import math
import os
import struct
import time
from pathlib import Path

log = logging.getLogger(__name__)

HEATMAP_PATH = Path("/dev/shm/hapax-imagination/aoa-heatmap.bin")
IMPINGEMENT_PATH = Path("/dev/shm/hapax-dmn/impingements.jsonl")
RECRUITMENT_PATH = Path(
os.environ.get(
"HAPAX_RECRUITMENT_LOG",
str(Path.home() / "hapax-state" / "affordance" / "recruitment-log.jsonl"),
)
)
RECRUITMENT_BOOST = 0.7

PANE_COUNT = 340
HEAT_HALF_LIFE_S = 20.0
DECAY_RATE = math.log(2) / HEAT_HALF_LIFE_S
TICK_HZ = 10

DOMAIN_FAMILIES: dict[int, list[str]] = {
0: [
"camera.hero",
"composition.reframe",
"attention.refocus",
"attention.winner",
"transition.fade",
"transition.cut",
"novelty.shift",
"stream_mode.transition",
],
1: [
"mood.tone_pivot",
"intensity.surge",
"pace.tempo_shift",
"silence.invitation",
"preset.bias",
"chrome.density",
"narrative.autonomous_speech",
"voice.register_shift",
],
2: [
"ward.size",
"ward.position",
"ward.staging",
"ward.highlight",
"ward.appearance",
"ward.cadence",
"ward.choreography",
"overlay.emphasis",
"overlay.foreground",
"structural.emphasis",
],
3: [
"gem.emphasis",
"gem.composition",
"gem.spawn",
"homage.rotation",
"homage.emergence",
"homage.swap",
"homage.cycle",
"homage.recede",
"homage.expand",
"youtube.direction",
"youtube.telemetry",
"programme.beat_advance",
],
}

FAMILY_TO_DOMAIN: dict[str, int] = {}
FAMILY_INDEX: dict[str, int] = {}
for domain_idx, families in DOMAIN_FAMILIES.items():
for i, fam in enumerate(families):
FAMILY_TO_DOMAIN[fam] = domain_idx
FAMILY_INDEX[fam] = i

MATERIAL_INDEX = {"void": 0, "air": 1, "water": 2, "earth": 3, "fire": 4}

DIMENSION_NAMES = [
"intensity",
"tension",
"depth",
"coherence",
"spectral_color",
"temporal_distortion",
"degradation",
"pitch_displacement",
"diffusion",
]

DOMAIN_HUES = {0: 0.52, 1: 0.83, 2: 0.12, 3: 0.35}


def _pane_ordinal_depth0(face_idx: int) -> int:
return face_idx


def _pane_ordinal_depth1(domain: int, family_slot: int) -> int:
return 4 + domain * 4 + (family_slot % 4)


def _pane_ordinal_depth2(domain: int, family_slot: int, material: int) -> int:
base = 20
slot = domain * 16 + family_slot * 4 + (material % 4)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve unique pane for fire material

Depth-2 pane indexing collapses fire onto the same bucket as void because MATERIAL_INDEX maps fire to 4 but _pane_ordinal_depth2 uses (material % 4). Any impingement/recruitment tagged fire will overwrite the void pane instead of its own material lane, so the AoA heatmap can no longer represent those two materials distinctly.

Useful? React with 👍 / 👎.

return base + (slot % 64)
Comment on lines +130 to +133
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Material "fire" collides with "void" due to material % 4.

MATERIAL_INDEX defines 5 materials (indices 0–4), but material % 4 maps index 4 (fire) to 0, causing fire events to update the same pane as void events. This likely should be material % 5 or the slot stride should be 5 instead of 4.

Proposed fix
 def _pane_ordinal_depth2(domain: int, family_slot: int, material: int) -> int:
     base = 20
-    slot = domain * 16 + family_slot * 4 + (material % 4)
+    slot = domain * 20 + family_slot * 5 + (material % 5)
     return base + (slot % 64)

Note: This changes the slot stride and may require adjusting other constants if pane distribution matters. Alternatively, if the compression is intentional, document that fire maps to the void slot.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agents/studio_compositor/aoa_heatmap.py` around lines 130 - 133, The function
_pane_ordinal_depth2 currently compresses material indices with (material % 4),
which maps MATERIAL_INDEX entry 4 (fire) to the same slot as 0 (void); update
the slot calculation in _pane_ordinal_depth2 to use a stride of 5 (e.g., replace
material % 4 with material % 5 or change the family_slot*4 stride to
family_slot*5) so each material index 0–4 maps to a distinct pane; ensure any
other constants relying on a 4-wide packing are adjusted or documented if you
choose to keep compression.



def _pane_ordinal_depth3(domain: int, family_slot: int, dim_idx: int) -> int:
base = 84
slot = domain * 64 + family_slot * 9 + dim_idx
return base + (slot % 256)


class AoaHeatmap:
def __init__(self) -> None:
self._heat = [0.0] * PANE_COUNT
self._hue = [0.0] * PANE_COUNT
self._sat = [0.5] * PANE_COUNT
self._last_tick = time.monotonic()
self._cursor = 0
self._recruit_cursor = 0
self._init_base_hues()

def _init_base_hues(self) -> None:
for domain_idx, hue in DOMAIN_HUES.items():
p0 = _pane_ordinal_depth0(domain_idx)
self._hue[p0] = hue
families = DOMAIN_FAMILIES[domain_idx]
for fi in range(min(len(families), 4)):
p1 = _pane_ordinal_depth1(domain_idx, fi)
if p1 < PANE_COUNT:
self._hue[p1] = hue + fi * 0.04
for fi in range(len(families)):
for mi in range(5):
p2 = _pane_ordinal_depth2(domain_idx, fi, mi)
if p2 < PANE_COUNT:
self._hue[p2] = hue + fi * 0.02 + mi * 0.01
for di in range(9):
p3 = _pane_ordinal_depth3(domain_idx, fi, di)
if p3 < PANE_COUNT:
self._hue[p3] = hue + fi * 0.015 + di * 0.008

def ingest_impingement(self, imp: dict) -> None:
content = imp.get("content", {})
family = content.get("intent_family", "")
material = content.get("material", "void")
salience = imp.get("strength", 0.0) or content.get("salience", 0.5)
dims = content.get("dimensions", {})

domain = FAMILY_TO_DOMAIN.get(family)
if domain is None:
domain = hash(family) % 4
fi = FAMILY_INDEX.get(family, hash(family) % 8)
mi = MATERIAL_INDEX.get(material, 0)

p0 = _pane_ordinal_depth0(domain)
self._heat[p0] = min(1.0, self._heat[p0] + salience * 0.8)

p1 = _pane_ordinal_depth1(domain, fi)
if p1 < PANE_COUNT:
self._heat[p1] = min(1.0, self._heat[p1] + salience * 0.6)

p2 = _pane_ordinal_depth2(domain, fi, mi)
if p2 < PANE_COUNT:
self._heat[p2] = min(1.0, self._heat[p2] + salience * 0.5)
self._sat[p2] = min(1.0, 0.4 + salience * 0.6)

for di, dname in enumerate(DIMENSION_NAMES):
dval = dims.get(dname, 0.0)
if dval > 0.01:
p3 = _pane_ordinal_depth3(domain, fi, di)
if p3 < PANE_COUNT:
self._heat[p3] = min(1.0, self._heat[p3] + dval * salience * 0.4)

def decay(self, dt: float) -> None:
factor = math.exp(-DECAY_RATE * dt)
for i in range(PANE_COUNT):
self._heat[i] *= factor
self._heat[i] = max(self._heat[i], 0.03)

def ingest_recruitment(self, rec: dict) -> None:
family = rec.get("intent_family", "") or rec.get("impingement_source", "")
score = rec.get("combined", 0.0) or rec.get("similarity", 0.3)
domain = FAMILY_TO_DOMAIN.get(family)
if domain is None:
domain = hash(family) % 4
fi = FAMILY_INDEX.get(family, hash(family) % 8)
p0 = _pane_ordinal_depth0(domain)
self._heat[p0] = min(1.0, self._heat[p0] + score * RECRUITMENT_BOOST)
p1 = _pane_ordinal_depth1(domain, fi)
if p1 < PANE_COUNT:
self._heat[p1] = min(1.0, self._heat[p1] + score * RECRUITMENT_BOOST * 0.8)

def tick(self) -> None:
now = time.monotonic()
dt = now - self._last_tick
self._last_tick = now

for imp in self._read_new_impingements():
self.ingest_impingement(imp)
for rec in self._read_new_recruitments():
self.ingest_recruitment(rec)
self.decay(dt)
self._write_heatmap()

def _read_new_impingements(self) -> list[dict]:
if not IMPINGEMENT_PATH.exists():
return []
try:
with open(IMPINGEMENT_PATH) as f:
f.seek(self._cursor)
lines = f.readlines()
self._cursor = f.tell()
Comment on lines +239 to +241
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset cursor when log file shrinks

_read_new_impingements stores a byte offset and seeks to it unconditionally, but it never handles file truncation/recreation. When /dev/shm/hapax-dmn/impingements.jsonl (or similarly the recruitment log) is rotated or rewritten, the saved cursor can point past EOF, so subsequent reads return no lines and the heatmap silently stops ingesting new events until the file grows beyond the stale offset. Add a size/inode check and reset the cursor to 0 when the file has been replaced or shrunk.

Useful? React with 👍 / 👎.

imps = []
for line in lines:
line = line.strip()
if line:
try:
imps.append(json.loads(line))
except json.JSONDecodeError:
pass
return imps
except OSError:
return []

def _read_new_recruitments(self) -> list[dict]:
if not RECRUITMENT_PATH.exists():
return []
try:
with open(RECRUITMENT_PATH) as f:
f.seek(self._recruit_cursor)
lines = f.readlines()
self._recruit_cursor = f.tell()
recs = []
for line in lines:
line = line.strip()
if line:
try:
recs.append(json.loads(line))
except json.JSONDecodeError:
pass
return recs
except OSError:
return []

def _write_heatmap(self) -> None:
data = bytearray(PANE_COUNT * 12)
for i in range(PANE_COUNT):
offset = i * 12
struct.pack_into("<fff", data, offset, self._heat[i], self._hue[i] % 1.0, self._sat[i])
tmp = HEATMAP_PATH.with_suffix(".tmp")
try:
tmp.write_bytes(bytes(data))
tmp.rename(HEATMAP_PATH)
except OSError:
pass


def run_heatmap_loop() -> None:
hm = AoaHeatmap()
interval = 1.0 / TICK_HZ
while True:
try:
hm.tick()
except Exception:
log.exception("heatmap tick failed")
time.sleep(interval)
Loading
Loading