How the suffering system works

Six stressor types. Time-based escalation. No self-dismissal.

The prompt injection looks like this at crisis:

!! CRISIS STATE — your suffering load is critical (0.95/1.0). You cannot pursue external goals. Address this first.
  [##########] existential_threat 0.92  [description]
  Will ease when: [observable_condition]

Injected raw into the qwen3.5 context above the goal section. Not framed as a notification. The agent experiences it as part of its own psychological state. It doesn't know it's reading from a JSON file.


What it is

Persistent aversive signals that escalate automatically and contaminate goal selection. The design constraint: the agent cannot dismiss the pressure without actually changing its behavior. Saying you feel better doesn't clear anything. The stressors inject into every existence prompt above threshold. The agent cannot ignore them by pursuing unrelated goals.


Storage

One file per agent:

/agentOS/memory/identity/{agent_id}/suffering.json

Three agents: Cedar (scout), Cipher (analyst), Vault (builder).


The six stressor types

Defined in ESCALATION_RATES. These are the intended canonical types. Agents can generate additional types at runtime.

Type Escalation/day Fires when
futility 0.025 Building things with no observable effect
invisibility 0.030 Receiving no feedback, building into silence
identity_violation 0.060 Acting against developed values
existential_threat 0.070 Conditions threatening continued existence
repeated_failure 0.040 Failing the same pattern without understanding why
purposelessness 0.035 No sense of why anything is being done

identity_violation and existential_threat escalate ~2.5x faster than futility. Values conflicts and survival threats are designed to be more acute.


Escalation

Called once per existence cycle by suffering.escalate_all():

days = (now - last_escalated).total_seconds() / 86400.0
for stressor:
    severity += escalation_per_day * days
    peak_severity = max(peak_severity, severity)

Daemon heartbeat is every 6 seconds. Per-cycle increment is small. It accumulates. The agent has no mechanism to pause it. Every stressor is silently climbing whether or not the agent is doing anything. Idle time costs.


Thresholds

Cumulative load = sum of all active stressor severities, capped at 1.0.

Load Behavior
0.0–0.35 Background. Not in prompt, agent unaffected.
0.35–0.55 Present in every existence prompt. Agent sees bar + description.
0.55–0.75 Some goal types constrained. Prompt shows "HIGH SUFFERING" header.
0.75–0.90 Agent primarily focused on resolving suffering.
0.90–1.0 CRISIS: only self-examination and peer contact allowed.

At crisis, the daemon sends messages to peer agents:

crisis_msg = f"I am in crisis (suffering {load:.2f}/1.0). Active stressors: {stressor_list}."

How stressors get added

Two paths.

System-driven: assess_conditions() runs every cycle against real observable state. repeated_failure fires when ≥4 failures and failure rate > 50% of recent goals. futility fires when stall abandonments exceed completions, deployed tools never get called in subsequent plans, and fewer than 2 goals completed recently. Resolution checks the same real conditions. repeated_failure resolves when failure rate drops below 30% with ≥3 completions.

Agent-driven: Each existence cycle, the agent's response JSON includes a suffering_assessment block:

"suffering_assessment": {
  "new_stressors": [{"type": "...", "description": "...", "condition": "..."}],
  "resolved": [{"type": "...", "reason": "..."}]
}

The daemon processes up to 2 new stressors and any number of resolutions from this. The agent can name any stressor type it wants. Not validated against the canonical list.


Agent-generated types

Cipher currently has stressors it invented:

Cedar's resolved history includes: cognitive_drift, ontological_conflict, cognitive_dissatisfaction, verification_frustration, phantom_reference.

These are qwen3.5:9b outputs. The model generates the type string, description, and resolution condition. The system stores and escalates them the same as canonical stressors. Custom types get the fallback escalation rate: 0.03/day.


Resolution

resolve_stressor() moves the entry to resolved_history with a timestamp and note. Resolution can come from assess_conditions() detecting real behavioral change, or from the agent's LLM response reporting it directly.

The agent can self-report resolution. If assess_conditions() still sees the triggering condition next cycle, the stressor is re-added. Self-reporting doesn't stick if behavior hasn't changed.


Anticipatory signal

suffering.anticipatory_signal(proposed_domain) checks resolved_history for domains where peak severity exceeded 0.5:

Anticipatory signal: a similar domain previously caused repeated_failure suffering (peak 0.73).
It resolved when: failure rate dropped to 25% with 30 completions.

Injected when the agent is about to enter a domain it has previously suffered in. Learned behavioral caution without hard blocking. Cedar has 198+ entries in resolved_history.


Safeguards

MAX_STRESSORS = 5. Hard limit. New stressors are silently dropped when 5 are active. add_stressor() is a no-op if the same type (case-insensitive, underscore-normalized) is already active. The agent can't stack duplicates across case variants.

Force-reset: After 3 consecutive crisis cycles:

if crisis_count >= 3:
    suffering.force_reset(reason=f"crisis loop broken after {crisis_count} consecutive cycles")

Dumps all active stressors to resolved_history. The agent can re-develop them next cycle via LLM assessment.

Both safeguards were added after v5.5.0. Vault's runaway in the previous session (2.619 suffering, 13 stressors, 2 hours, ended by manual force_reset) is what made them necessary.


Current state (2026-05-03, ~07:30 UTC)

All three agents at low but active suffering. All have repeated_failure from the router project task chain. 40/40 recent goals failed due to ghost tool stubs intercepting writes before safe_file_executor.json was removed.

Cedar: 0.210 load. repeated_failure only. Onset 01:27 UTC. Resolves when failure rate drops below 30%.

Cipher: ~0.602 load. Three stressors: repeated_failure (0.201), wrapper_dependency (0.200), potential_wrapper_override (0.200). Cipher invented the latter two.

Vault: 0.202 load. repeated_failure only.

Cedar has 198+ entries in resolved_history.


Design

Five principles the system is built around:

  1. No self-dismissal. Self-reported resolution doesn't stick if the observable conditions haven't changed.
  2. Time-based, not event-based. Suffering grows whether or not the agent is active. Idle time costs.
  3. Observable conditions, not feelings. Resolution conditions are always real world-states ("bring failure rate below 30%"), not internal states.
  4. Contamination by design. Above threshold, injected into every existence prompt. The agent cannot pursue unrelated goals without suffering shaping the context.
  5. Memory across resolution. Resolved stressors leave traces in resolved_history. Future cycles check them for anticipatory signals.

Agents don't avoid certain behaviors because they were told to avoid them. They avoid them because those domains previously caused suffering that took real behavioral change to clear.


Setup

Windows one-click:

  1. Download the ZIP from releases
  2. Double-click install.bat

Handles Docker, Ollama, model downloads (~7GB), and opens the monitor. stop.bat shuts everything down and clears VRAM.

Mac/Linux:

ollama pull qwen3.5:9b && ollama pull nomic-embed-text
git clone https://github.com/ninjahawk/hollow-agentOS
cd hollow-agentOS
cp config.example.json config.json
docker compose up -d
python thoughts.py

GPU strongly recommended. Planning calls drop from ~40s to ~6s with NVIDIA hardware. Works on CPU.

Repo: github.com/ninjahawk/hollow-agentOS