user_limbic_mirror
User Limbic Mirror v2 — per-user relational modeling with conflict detection.
Tracks each user’s inferred emotional state via a 25-node shadow vector, maintaining separate game and genuine context layers. Detects inter-user conflict and triggers equidistance mechanics to prevent recency/loudness bias.
Architecture:
Per-user keying:
{channel_id}:{user_id}composite keysDual vectors:
genuine_vector(long-term relationship) andgame_vector(KoTH / roleplay context, does not pollute genuine)Relational baselines: slow-updating snapshots of “how Star normally is with this person,” stored in Redis for persistence across sessions
Conflict detection: when 2+ high-trust users have opposing emotional bids, sets
conflict_detectedflag and triggers equidistance rules
- class user_limbic_mirror.ContextMode(*values)[source]
Bases:
EnumClassification of which relational layer a turn belongs to.
Distinguishes genuine emotional expression (which should mutate the long-term relationship model) from in-game / roleplay context (KoTH and similar, tracked separately so it never pollutes the genuine baseline), with an ambiguous fallback for turns that cannot yet be confidently placed. The chosen mode decides which shadow vector
analyzewrites to and whether the slow relational baseline is updated.Members are persisted by their string
valueinside Redis profile hashes via_save_profile_to_redisand rehydrated by_load_profile_from_redis. Referenced throughout this module and by the test suite (tests/core/test_ulm_redis.py).- GENUINE = 'genuine'
- GAME = 'game'
- AMBIGUOUS = 'ambiguous'
- class user_limbic_mirror.TurnRecord(timestamp, user_msg_len, star_reply_len, sentiment, deltas, context_mode, dominant_signals)[source]
Bases:
objectOne analyzed exchange, retained for rolling-window relational analysis.
Captures the metadata of a single user/Star turn — its timing, message sizes, quick sentiment, the per-node vector deltas applied, the
ContextModeit was scored under, and a short list of the dominant signals — so the recent slice of a conversation can be inspected without re-reading the raw text. Instances are appended toUserProfile.historybyUserLimbicMirror.analyzeand round-tripped through Redis as JSON by_save_profile_to_redis/_load_profile_from_redis. Construction is also exercised directly bytests/core/test_ulm_redis.py. Note this is distinct from the unrelatedTurnRecorddefined ingame_session.py.- Parameters:
- context_mode: ContextMode
- class user_limbic_mirror.UserProfile(user_id, channel_id, genuine_vector=<factory>, game_vector=<factory>, relational_baseline=<factory>, history=<factory>, timestamps=<factory>, prev_message='', context_mode=ContextMode.GENUINE, total_turns=0, last_active=0.0, recent_turns=<factory>, revision_count=0)[source]
Bases:
objectComplete per-user relational state, keyed by
{channel}:{user}.Holds everything
UserLimbicMirrorneeds to model one person in one channel: the dual shadow vectors (genuine_vectorfor the long-term relationship andgame_vectorfor KoTH / roleplay context), the slow-movingrelational_baseline, boundedhistoryandtimestampsdeques for windowed analysis, the previous message, the activeContextMode, turn counters, last-active time, a smallrecent_turnsbuffer feeding the Flash-Lite dyadic mirror, and arevision_countused for optimistic-lock checks on save.Instances live in the mirror’s in-memory LRU cache, are created lazily by
_get_profile, persisted whole to a Redis hash by_save_profile_to_redis, and rehydrated by_load_profile_from_redis. Resonance and entrainment results are attached dynamically as_resonance_cacheand_entrainment_phaseattributes duringanalyze. Also constructed directly across the test suite (e.g.tests/core/test_ulm_redis.py,tests/adversarial/).- Parameters:
- context_mode: ContextMode = 'genuine'
- class user_limbic_mirror.ChannelConflictState(detected=False, parties=<factory>, severity=0.0, started_at=0.0, description='')[source]
Bases:
objectSnapshot of inter-user conflict detected within a single channel.
Records whether two or more high-trust users are currently at odds, who the involved parties are, an estimated severity in
[0, 1], when the conflict began, and a short human-readable description. The mirror uses this to trigger equidistance mechanics so Star does not side with whoever spoke last or loudest.Instances are produced and refreshed by
UserLimbicMirror._detect_conflict, surfaced to callers viaget_conflict_state(read bylimbic_system/coordinator.py) andget_channel_summary, and persisted to Redis by_persist_conflict_to_redis. Also constructed directly intests/core/test_ulm_redis.py.- Parameters:
- class user_limbic_mirror.UserLimbicMirror(redis_client=None)[source]
Bases:
objectPer-user relational model with conflict detection and game/genuine split.
Maintains separate shadow vectors per user per channel, detects inter-user conflict, and applies equidistance mechanics to prevent Star from siding with whoever is loudest.
- __init__(redis_client=None)[source]
Construct an empty mirror, optionally bound to a Redis client.
Sets up the in-memory caches that hold all relational state: the
_profilesLRU dict (keyed{channel}:{user}), per-channel_conflictsstates, the_channel_usersmembership sets, the_game_channelsset, and the_pending_baseline_loadsqueue for lazy legacy-baseline hydration. When a Redis client is supplied, profiles, baselines, conflict states, game-channel membership, and resonance spells are persisted to and read back from Redis; without one the mirror runs purely in memory.Instantiated by
limbic_system/coordinator.py(the live integration), by the resonance toolstools/inject_ncm.pyandtools/loopcast.py, byweb/ncm_chart_api.pyfor charting, and across the test suite.- Parameters:
redis_client – Async Redis client for persistence, or
Noneto run in-memory only.- Return type:
None
- async set_game_mode(channel_id, active=True)[source]
Toggle whether a channel is in active KoTH / game context.
Adding a channel to the
_game_channelsset makesanalyzeroute subsequent turns there into each user’sgame_vectorinstead of thegenuine_vector, keeping roleplay swings out of the long-term relationship model; clearing it returns the channel to genuine mode. The set is mirrored to theulm:game_channelsRedis set (SADD / SREM) so the mode survives restarts, with Redis failures logged but non-fatal.Mutates
_game_channelsand touches Redis. Exercised bytests/core/test_ulm_redis.py; no production caller invokes it directly today (game mode is otherwise inferred per channel).
- check_mimetic_pull(user_msg, star_desire_text)[source]
Score how much the user is echoing Star’s stated desire.
Implements the “desire osmosis” signal: lowercases and stop-word-filters both the user’s message and Star’s current desire text, then sizes their token overlap. Three or more shared content words yield a strong
0.20pull, one or two a mild0.08, and no overlap0.0— the idea being that a user unconsciously mirroring Star’s wanting is leaning into the relationship. Pure computation with no side effects.Called by
analyzeto produce theU_MIMETIC_PULLdelta (a high value there can trigger mimetic-melt behavior in the desire engine).
- async analyze(channel_id, user_id, user_msg, star_reply='', star_desire_text='', context_mode=None)[source]
Analyze a user message and update their shadow vector.
- async get_vector(channel_id, user_id, layer='genuine')[source]
Return a copy of a user’s shadow vector for the requested layer.
Public read accessor: fetches the profile via
_get_profile(which may read through from Redis) and hands back a defensive copy of either thegame_vector(whenlayer == "game") or thegenuine_vectorotherwise, so callers can read node values without mutating internal state.No known production caller invokes this today; it exists as part of the public surface for external/diagnostic reads.
- get_conflict_state(channel_id)[source]
Return the current cached conflict state for a channel.
Synchronous read of the
_conflictscache (populated by_detect_conflictduringanalyze); returns a fresh, emptyChannelConflictStatewhen nothing has been recorded for the channel yet. Does not recompute — it reports whatever the lastanalyzeturn left behind.Called by
limbic_system/coordinator.pyto set theconflict_detectedflag on the merged vector, and internally byget_channel_summary.- Parameters:
channel_id (
str) – Channel to look up.- Returns:
The cached state, or a default empty one.
- Return type:
- async get_read_summary(channel_id, user_id)[source]
Render a short natural-language read of a user for prompt injection.
Fetches the profile via
_get_profile, ranks its genuine-vector nodes by how far they deviate from the default baseline, and verbalizes the few most elevated and most suppressed dimensions (using friendly labels) into a compact line Star can read. It also appends an equidistance warning when an inter-user conflict names this user, a game-context marker, an active-resonance summary (via_get_resonance_summary), and any cached entrainment-phase / egg status — giving the model a one-glance relational read of the person it is replying to.Reads cached profile and conflict state only; no Redis write. Called by
limbic_system/coordinator.pyto build the per-user read for the prompt, and internally byget_channel_summary.
- async get_channel_summary(channel_id)[source]
Summarize every tracked user and the conflict state for a channel.
Builds a
user_readsmap by callingget_read_summaryfor each cached, tracked user in the channel, and attaches aconflictblock (severity, parties, description) whenget_conflict_statereports an active conflict — giving an admin or tool a single channel-wide snapshot of how Star currently reads the room.Reads
_channel_usersand the profile cache; no Redis write. Reached from theget_channel_summarytool handler intools/channel_summary_tools.py.
- async inject_resonance(user_id, deltas, reason='', ttl_seconds=86400)[source]
Write resonance deltas to a global per-user key in Redis.
These deltas merge into the user’s shadow vector during analyze(), modulating how Star perceives and responds to this user across ALL channels. Spells decay after ttl_seconds.
- Parameters:
- Return type:
- async save_baseline(channel_id, user_id)[source]
Persist relational baseline to Redis for cross-session persistence.
Also appends a time-series sample to
ulm:ledger:{user_id}so the admin chart can show how the relational baseline evolves over time.
- async load_baseline(channel_id, user_id)[source]
Restore a user’s relational baseline from Redis into their profile.
The read counterpart of
save_baseline: GETs theulm:baseline:{channel}:{user}string and, for each known node, copies the stored value into bothrelational_baselineandgenuine_vector(warming the live vector toward the remembered relationship) and restorestotal_turns. This is the legacy backup path used to seed a freshly created profile before the full-hash persistence takes over.No-op without a Redis client; missing keys and decode errors are logged and ignored. Fetches the profile via
_get_profileand mutates it in place. Called lazily byanalyzefor profiles queued in_pending_baseline_loadson first access, and directly bytests/core/test_ulm_redis.py.
- async user_limbic_mirror.update_limbic_vector_occ(redis, user_id, vector_updater_func)[source]
Atomically update a user’s limbic vector under optimistic concurrency.
Standalone helper (distinct from the
UserLimbicMirrorclass) that safely mutates theuser:limbic:{user_id}JSON state in Redis. It WATCHes the key, reads the current vector and version, applies the caller-suppliedvector_updater_functo produce the new vector, bumps the version, and commits inside a MULTI/EXEC pipeline. A concurrent writer between the WATCH and EXEC raisesWatchError, which is caught and retried with a small backoff up to five attempts before giving up.Records
limbic_occ_success/limbic_occ_collision/limbic_occ_failurecounters on the sharedobservabilitysink and logs each outcome. No production caller invokes it today; it is exercised bytests/test_limbic_concurrency.py.- Parameters:
- Returns:
The persisted state
{"vector": [...], "version": n}after a successful commit.- Return type:
- Raises:
RuntimeError – If all retry attempts collide and the update cannot be committed.