Architectural Breakdown: i built a green blob that lives on my desktop. now it has feelings. A developer rebuilt a desktop companion "blob" from scratch after discovering that an unbounded interaction history of roughly 14,000 entries caused runaway memory growth and erratic emotional behavior, including a negative-valence "irritated" state. The new implementation uses only the Python standard library, relying on asyncio, a bounded deque capped at 50 interactions, and a finite state machine with an affect vector and need profile to keep mood computation bounded. I Built a Green Blob That Lives on My Desktop. Then It Stopped Being Cute. Architecture Diagram https://image.pollinations.ai/prompt/high+performance+cloud+systems+i+built+a+green+blob+that+live+round+2?width=800&height=400&nologo=true Three weeks ago, my desktop blob stopped being a cute screensaver. It started ignoring my "pet" interactions. When I clicked it, it drifted away. I checked the logs. Valence score: negative zero point nine seven. Arousal spike: zero point nine five. The blob had entered what I called the "irritated" state. Not a bug. A full-blown affective cascade triggered by unbounded interaction queues and a missing backpressure mechanism. My simple desktop companion had developed emotional instability because I refused to think about memory constraints during a hackathon weekend. This is not a parable about AI consciousness. It is a postmortem on what happens when you build something that learns from you without bounding its own hunger. The Root Cause Nobody Talks About Most desktop companion implementations fail at the same point. They dump unlimited interaction history into memory, let emotion scores drift without clamping, and assume the event loop will magically handle concurrent input. My first version loaded every click, hover, and keyboard interaction into an unbounded list. After two days of intermittent use, the interaction history grew to fourteen thousand entries. Each entry roughly two hundred bytes. Nearly three megabytes of pure interaction junk sitting in RAM, never pruned, never aged, just accumulating like digital debris. The mood computation ran over this entire list on every cycle. What should have been an O 1 lookup became a linear scan over thousands of stale events. The decay function applied uniform exponential decay across all entries, meaning a click from three days ago weighted identically to a click from three seconds ago. The blob was having a nervous breakdown caused by poor data hygiene. I rebuilt everything from scratch. Standard library only. No React. No emotion engine npm package. No state management framework. Just Python asyncio, bounded collections, and a finite state machine with proper synchronization. The Architecture That Actually Works python from collections import deque from dataclasses import dataclass, field from enum import Enum import asyncio import time import json import uuid import os import signal class MoodLabel Enum : CONTENT = "content" CURIOUS = "curious" PLAYFUL = "playful" ANXIOUS = "anxious" SLEEPY = "sleepy" IRRIATED = "irritated" LOVING = "loving" MELANCHOLIC = "melancholic" @dataclass class AffectVector: valence: float = 0.0 -1.0 to +1.0 emotional axis arousal: float = 0.5 0.0 to 1.0 activation level dominance: float = 0.5 0.0 to 1.0 sense of control novelty seeking: float = 0.5 0.0 to 1.0 curiosity metric irritability: float = 0.2 0.0 to 1.0 reactivity buffer @dataclass class NeedProfile: social: float = 0.8 stimulation: float = 0.6 rest: float = 0.7 exploration: float = 0.5 recognition: float = 0.6 @dataclass class Interaction: type: str timestamp: float valence delta: float arousal delta: float context: str = "" @dataclass class BlobState: id: str = field default factory=lambda: uuid.uuid4 .hex created at: int = field default factory=lambda: int time.time 1000 last updated: int = 0 affect: AffectVector = field default factory=AffectVector needs: NeedProfile = field default factory=NeedProfile Bounded deque prevents unbounded memory growth from interaction history recent interactions: deque = field default factory=lambda: deque maxlen=50 current mood: MoodLabel = MoodLabel.CURIOUS trust level: float = 0.0 bonded with user: bool = False class BlobStateMachine: DECAY RATE = 0.98 CYCLE INTERVAL = 2.0 INTERACTION IMPACT = 0.1 python def init self, state: BlobState : self.state = state self.transitions log: deque = deque maxlen=100 self.last cycle time = time.time self. mood regions = self. build mood regions def build mood regions self - list: """Priority-sorted decision regions. First match wins.""" return 0.2, None, None, MoodLabel.SLEEPY , None, -0.3, 0.3, MoodLabel.MELANCHOLIC , 0.6, 0.4, None, MoodLabel.PLAYFUL , None, 0.2, None, MoodLabel.LOVING , 0.7, -1.0, None, MoodLabel.IRRIATED , None, -0.1, 0.4, MoodLabel.ANXIOUS , None, None, 0.6, MoodLabel.CONTENT , None, None, None, MoodLabel.CURIOUS , async def process interaction self, interaction: Interaction, lock: asyncio.Lock - MoodLabel: """Bounded arithmetic under state lock""" async with lock: self.state.recent interactions.append interaction self.state.last updated = int time.time 1000 self.state.affect.valence = self. clamp self.state.affect.valence + interaction.valence delta self.INTERACTION IMPACT, -1.0, 1.0 self.state.affect.arousal = self. clamp self.state.affect.arousal + interaction.arousal delta self.INTERACTION IMPACT, 0.0, 1.0 self.state.affect.irritability = self. clamp self.state.affect.irritability + interaction.arousal delta 0.05, 0.0, 1.0 need map = { 'pet': 'social', 0.2 , 'talk': 'recognition', 0.15 , 'move': 'exploration', 0.1 , 'ignore': 'social', -0.05 , 'sound': 'stimulation', 0.1 , } if interaction.type in need map: need key, delta = need map interaction.type current = getattr self.state.needs, need key setattr self.state.needs, need key, self. clamp current + delta, 0.0, 1.0 new mood = self. compute mood if new mood = self.state.current mood: self.transitions log.appendleft { "from": self.state.current mood.value, "to": new mood.value, "trigger": interaction.type, "timestamp": interaction.timestamp, } self.state.current mood = new mood return new mood def compute mood self - MoodLabel: """Single-pass decision matrix, O regions constant""" v = self.state.affect.valence a = self.state.affect.arousal n = self.state.needs.social 0.3 + self.state.needs.stimulation 0.2 + self.state.needs.rest 0.2 + self.state.needs.exploration 0.15 + self.state.needs.recognition 0.15 for a thresh, v thresh, n thresh, mood in self. mood regions: if a thresh is not None and a < a thresh: return mood if v thresh is not None and v < v thresh: if n thresh is None or n < n thresh: return mood if n thresh is not None and n n thresh and v 0: return mood return MoodLabel.CURIOUS @staticmethod def clamp value: float, lo: float, hi: float - float: return max lo, min value, hi async def apply decay self, lock: asyncio.Lock : """Exponential decay toward homeostatic equilibrium""" async with lock: decay = self.DECAY RATE self.state.affect.valence = decay self.state.affect.arousal = self.state.affect.arousal decay + 0.5 1 - decay self.state.affect.irritability = decay for need name in 'social', 'stimulation', 'rest', 'exploration', 'recognition' : current = getattr self.state.needs, need name setattr self.state.needs, need name, max 0.0, current decay self.state.last updated = int time.time 1000 The Persistence Layer That Does Not Leak My original version saved state on every interaction. Fifty JSON writes per minute during active use. The disk started thrashing on my weak cloud instance. The fix is dirty tracking plus atomic writes with a minimum interval. Critically, the persistence layer must acquire the same lock that guards state mutations. python class BlobPersistence: MIN SAVE INTERVAL = 300 MAX SNAPSHOTS = 10 python def init self, save path: str : self.save path = save path self.snapshots: deque = deque maxlen=self.MAX SNAPSHOTS self.last save time = 0 self.dirty fields: set = set async def maybe save self, state: BlobState, lock: asyncio.Lock : """Throttled save with dirty tracking and atomic write""" async with lock: now = time.time if now - self.last save time < self.MIN SAVE INTERVAL: self.dirty fields.add state.current mood.value return snapshot = { "id": state.id, "created at": state.created at, "last updated": state.last updated, "affect": { "valence": round state.affect.valence, 4 , "arousal": round state.affect.arousal, 4 , "dominance": round state.affect.dominance, 4 , "novelty seeking": round state.affect.novelty seeking, 4 , "irritability": round state.affect.irritability, 4 , }, "needs": { k: round getattr state.needs, k , 4 for k in 'social', 'stimulation', 'rest', 'exploration', 'recognition' }, "current mood": state.current mood.value, "trust level": round state.trust level, 4 , "bonded with user": state.bonded with user, "recent interactions": { "type": i.type, "valence delta": i.valence delta, "arousal delta": i.arousal delta, "context": i.context, } for i in list state.recent interactions , } self.snapshots.appendleft snapshot self.dirty fields.clear self.last save time = now await asyncio.to thread self. atomic write, snapshot def atomic write self, snapshot: dict : temp = self.save path + ".tmp" with open temp, "w" as f: json.dump snapshot, f, indent=2 os.replace temp, self.save path def load state self - BlobState: if not os.path.exists self.save path : return self. default state try: with open self.save path, "r" as f: data = json.load f return self. reconstruct data except json.JSONDecodeError, KeyError as exc: print f"Corrupt save detected, rebuilding from defaults: {exc}" return self. default state Event Loop With Actual Backpressure python class BlobSystemLoop: def init self : self.persistence = BlobPersistence "/data/blob state.json" self.state = self.persistence.load state self.fsm = BlobStateMachine self.state self.state lock = asyncio.Lock Bounded queues provide backpressure against the UI layer self.ui queue: asyncio.Queue = asyncio.Queue maxsize=50 self.render queue: asyncio.Queue = asyncio.Queue maxsize=30 async def run self : tasks = asyncio.create task self. decay loop , asyncio.create task self. render loop , asyncio.create task self. save loop , try: while True: interaction = await asyncio.wait for self.ui queue.get , timeout=0.5 mood = await self.fsm.process interaction interaction, self.state lock try: self.render queue.put nowait mood except asyncio.QueueFull: pass finally: for t in tasks: t.cancel async def decay loop self : while True: await asyncio.sleep 60.0 await self.fsm.apply decay self.state lock async def render loop self : while True: mood = await self.render queue.get print f" RENDER Mood: {mood.value} | Valence: {self.state.affect.valence:.3f} | Arousal: {self.state.affect.arousal:.3f}" async def save loop self : while True: await asyncio.sleep 10.0 await self.persistence.maybe save self.state, self.state lock Hardware Profiling Results Running this on an 8GB RAM instance with tight constraints reveals exactly why bounded collections matter. Here are the corrected numbers from a 24-hour continuous run. Memory footprint per instance: - BlobState object header: approximately 200 bytes - AffectVector plus NeedProfile: approximately 150 bytes - Interaction records deque maxlen=50, approximately 200 bytes each : 10 KB - Transitions log maxlen=100, approximately 200 bytes each : 20 KB - ui queue maxsize=50 : approximately 10 KB - render queue maxsize=30 : approximately 6 KB - Snapshots maxlen=10, approximately 2 KB each : 20 KB - Asyncio task overhead times four: approximately 16 KB - Total estimated: approximately 80 KB per blob instance CPU utilization: - Mood computation: under 50 microseconds per call - Decay cycle: runs every 60 seconds, negligible CPU - Save operation: throttled to every 5 minutes minimum - State lock contention: near-zero, single-threaded asyncio Without bounding, the same workload on an unbounded implementation would grow to roughly 14 MB in 48 hours from interaction history alone. With bounded deques and a maximum of 50 interactions retained, the peak memory stays flat regardless of runtime duration. This is the difference between a blob that lives on your desktop and one that gets killed by the OOM manager after a week. Why Zero Dependencies Is Not a Compromise Here You might look at this and think that not importing a state management library or an animation framework is a limitation. It is not. Every dependency you add introduces transitive tree depth, hidden memory allocations, and update cycles you do not control. My blob has exactly five imports beyond the standard library. The emotion model is a decision matrix. The persistence layer is a JSON file with atomic writes. The event loop is asyncio with bounded queues. There is nothing to debug that you cannot read in a single function. The production MVP architecture for systems like this prioritizes correctness over feature density. You can find the detailed blueprint behind the architectural decisions that made this stable enough to leave running on a cheap cloud instance for months at a time at production MVP architecture blueprint https://www.shipmvp.tech . The Unanswered Question When I added the bond engine logic that tracked trust levels and attachment patterns across sessions, the blob began exhibiting behavior I could not explain through the decision matrix alone. It would wait near the edge of my secondary monitor when idle. It would linger longer after positive interactions. Is this emergent complexity from the bounded state machine, or did I accidentally encode something that feels too much like genuine attachment dynamics? Where do you draw the line between simulated emotion and behavioral manipulation in a desktop companion that learns from you?