Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Added — Habenula Phase 1 (anti-reward / negative-RPE)

Lateral habenula subsystem. Records negative-reward-prediction-error
events as a dedicated channel separate from `bg_td_events`. Fires on
reward omission, aversive valences, repeated failures. Pairs
antisymmetrically with LC (PR #121): LC fires on +surprise, Hb fires
on −surprise / omission.

Phase 1 is inspection-only / additive. Does NOT yet damp
`bg_modulators.tonic_da`. That's Phase 3.

- **Migration 070** — 3 tables (`habenula_triggers`, `habenula_firings`,
`habenula_state`). 5 seed triggers (reward_omission, retrieval_failure,
repeated_low_utility, aversive_valence, task_abandoned). Single-row
state seed.
- **`agentmemory.mcp_tools_habenula`** — 5 MCP tools (`habenula_status`,
`habenula_fire`, `habenula_register_trigger`, `habenula_history`,
`habenula_reset`).
- **10 tests** covering migration, empty state, fire-via-trigger,
explicit-pe fire, positive-PE rejection, missing-args rejection,
unknown-trigger rejection, idempotent registration, history filters,
reset semantics.
- **Design proposal** at `docs/proposals/habenula.md`.

Phase 2 wires `habenula_fire` into outcome_annotate's negative path.
Phase 3 lets `suggested_da_damp` actually subtract from
`bg_modulators.tonic_da`. Phase 4 enforces.

### Added — issue #116 Phase 1-A: retrieval pathway log

External architecture memo (issue #116, "Thalamus, Basal Ganglia, and
Expand Down
3 changes: 2 additions & 1 deletion MCP_SERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ docker run -v ~/.agentmemory:/data -e BRAIN_DB=/data/brain.db brainctl
The `CMD` defaults to `brainctl-mcp`, so the container runs the MCP
server over stdio.

## Available Tools (260)
## Available Tools (265)

| Tool | Description |
|------|-------------|
Expand Down Expand Up @@ -164,6 +164,7 @@ server over stdio.
| Insula (Phase 1, interoception) | `insula_sample`, `insula_state`, `insula_subscribe`, `insula_check_triggers` | Self-state vector (write_pressure, retrieval_strain, consolidation_debt, embedding_health, attention_load, certainty) with EMA baseline + deviation. Subscriber registry routes signals to subsystems |
| PFC sub-regions (Phase 1, named slots) | `pfc_slot_set`, `pfc_slot_get`, `pfc_status` | 4 named slots per agent: dlPFC (active task), vmPFC (outcome-utility), OFC (realized-outcome log), frontopolar (meta-monitor). Mostly aggregation |
| Entorhinal grid (Phase 1, conceptual indexing) | `entorhinal_activate`, `entorhinal_lookup`, `entorhinal_status` | 48 grid cells across 3 scales (fine/medium/coarse). Deterministic hash maps content → cell activations; sub-linear pattern lookup |
| Habenula (Phase 1, anti-reward) | `habenula_status`, `habenula_fire`, `habenula_register_trigger`, `habenula_history`, `habenula_reset` | Lateral habenula — negative-RPE source. Pairs antisymmetrically with LC. Records reward-omission / aversive / repeated-failure events; computes `suggested_da_damp` for Phase 3 DA suppression (see `docs/proposals/habenula.md`) |

### Tier 3: Specialist (~150 tools)

Expand Down
71 changes: 71 additions & 0 deletions db/migrations/070_habenula.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-- Migration 070: lateral habenula subsystem — Phase 1 schema
--
-- The "anti-reward" / negative-RPE source. Pairs antisymmetrically
-- with LC (LC = positive surprise → NE; Hb = negative surprise /
-- reward omission / aversion → DA suppression in Phase 3).
--
-- Phase 1 is inspection-only / additive: schema + read+CRUD tools.
-- Does NOT yet damp bg_modulators.tonic_da. That's Phase 3.
--
-- Four invariants encoded:
-- 1. Negative-RPE coding: signed_pe always <= 0.
-- 2. Reward omission distinct from punishment (event_kind).
-- 3. Tonic vs phasic separation.
-- 4. DA-suppression effect proportional to integrated activity
-- (Phase 3 will use EWMA; Phase 1 just records events).
--
-- Rollback:
-- DROP TABLE IF EXISTS habenula_state;
-- DROP TABLE IF EXISTS habenula_firings;
-- DROP TABLE IF EXISTS habenula_triggers;
-- DELETE FROM schema_version WHERE version = 70;
--
-- IDEMPOTENT.

CREATE TABLE IF NOT EXISTS habenula_triggers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
event_kind TEXT NOT NULL CHECK(event_kind IN ('omission', 'aversive', 'repeated_failure', 'other')),
default_pe REAL NOT NULL DEFAULT -0.1 CHECK(default_pe <= 0.0),
description TEXT,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_hb_triggers_kind ON habenula_triggers(event_kind);

CREATE TABLE IF NOT EXISTS habenula_firings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fired_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now')),
agent_id TEXT,
trigger_id INTEGER,
event_kind TEXT NOT NULL CHECK(event_kind IN ('omission', 'aversive', 'repeated_failure', 'other')),
signed_pe REAL NOT NULL CHECK(signed_pe <= 0.0),
context_hash TEXT,
source_event_id INTEGER,
notes TEXT,
FOREIGN KEY (trigger_id) REFERENCES habenula_triggers(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_hb_firings_recent ON habenula_firings(fired_at);
CREATE INDEX IF NOT EXISTS idx_hb_firings_agent ON habenula_firings(agent_id, fired_at);
CREATE INDEX IF NOT EXISTS idx_hb_firings_kind ON habenula_firings(event_kind, fired_at);

CREATE TABLE IF NOT EXISTS habenula_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
tonic_activity REAL NOT NULL DEFAULT 0.0,
phasic_burst REAL NOT NULL DEFAULT 0.0,
rolling_disappointment_24h INTEGER NOT NULL DEFAULT 0,
last_firing_at TEXT,
suggested_da_damp REAL NOT NULL DEFAULT 0.0,
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%S', 'now'))
);
INSERT OR IGNORE INTO habenula_state (id) VALUES (1);

INSERT OR IGNORE INTO habenula_triggers (name, event_kind, default_pe, description) VALUES
('reward_omission', 'omission', -0.15, 'expected positive outcome did not arrive'),
('retrieval_failure', 'omission', -0.10, 'memory_search returned no useful candidates'),
('repeated_low_utility', 'repeated_failure', -0.20, 'same query pattern failed 3+ times in 24h'),
('aversive_valence', 'aversive', -0.30, 'amygdala flagged content with strong negative valence'),
('task_abandoned', 'repeated_failure', -0.25, 'agent abandoned a task after failure cascade');

INSERT OR IGNORE INTO schema_version (version, description, applied_at)
VALUES (70, 'habenula Phase 1: 3 tables (triggers, firings, state) + 5 seed trigger classes',
strftime('%Y-%m-%dT%H:%M:%S', 'now'));
108 changes: 108 additions & 0 deletions docs/proposals/habenula.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Proposal: The Habenula Subsystem for brainctl

**Status:** Phase 1 design + implementation, branch `brain-regions-habenula-phase-1`.
**Author:** Claude Opus 4.7 (overnight chain continuation)
**Date:** 2026-05-20
**Scope:** New subsystem. Pairs antisymmetrically with LC + the BG's TD-error bus.

---

## TL;DR

Lateral habenula (LHb) is the brain's **negative-reward-prediction-error** source. It fires when an expected positive outcome **fails to materialize** (omission) or when an aversive outcome arrives. Its primary projection target is the rostromedial tegmental nucleus (RMTg), which then **suppresses** dopaminergic VTA/SNc neurons. The functional consequence: habenula activity damps dopamine, disengages exploration, and drives task switching.

In brainctl, the BG's `bg_td_events` already broadcasts a TD-error signal (`δ = utility + γ·V(s') − V(s)`) that can be either sign. But there's no first-class structure for tracking **systematic negative outcomes** — repeated retrieval failures, repeated aversive valences, expected-good-results that didn't pan out. Issue #116's audit memo noted that brainctl learns from positive outcomes through bg_striatal_weights but doesn't have a dedicated "anti-reward" channel that drives disengagement, task abandonment, or exploration cessation.

This proposal codifies habenula as a thin first-class subsystem that:

- Logs negative outcome events specifically (separately from the general `bg_td_events` stream)
- Tracks running disappointment / aversive-event counters per (agent, context)
- Provides a `tonic_da` damping signal that the existing BG-thalamus modulator cascade can read in Phase 2
- Pairs antisymmetrically with LC: LC fires on positive surprise (high |+δ|), habenula fires on negative surprise (high |−δ|) or expected-positive omission

Phase 1 ships schema + 5 MCP tools + tests. **No behavior change** — does not yet damp `bg_modulators.tonic_da`. That's Phase 3.

## Architectural placement

```
┌────── LC (PR #121) ─────────┐ ┌────── Habenula (this PR) ──────┐
│ fires on +surprise / NE │ │ fires on −surprise / aversion │
│ → bg_modulators.lc_ne │ │ → Phase 3 damps tonic_da │
└────────────────────────────┘ └────────────────────────────────┘
│ │
│ │
└────────────┬─────────────────────┘
┌────────────────────┐
│ bg_td_events bus │
│ (sign-agnostic δ) │
└────────────────────┘
```

Habenula is NOT the same as a negative δ in bg_td_events. The TD signal already supports negative δ. What habenula adds is:

1. **Expected-positive omission detection** — δ ≈ 0 isn't enough; we need "the prediction said *positive*, the observation gave *neutral or worse*"
2. **Aggregation across events** — sustained disappointment looks different from one bad TD
3. **A dedicated channel for disengagement triggers** — agents/contexts where the agent should *stop trying that retrieval pattern*
4. **Asymmetric coupling to LC** — habenula and LC together cover the full ±PE space; together they're the candidate signal for the Phase 4 enforcement flip

## Biological invariants encoded

1. **Negative-RPE coding.** LHb neurons phasically activate on negative-RPE events (Matsumoto & Hikosaka 2007). brainctl schema: `habenula_firings.signed_pe` is the source of truth, always ≤ 0.
2. **Reward omission ≠ punishment.** Both fire habenula but with different downstream consequences. `habenula_firings.event_kind` distinguishes `omission` from `aversive`.
3. **Tonic vs phasic.** Like LC and ARAS, habenula has tonic baseline and phasic bursts. Tracked in `habenula_state`.
4. **DA-suppression effect proportional to integrated activity.** A single bad event doesn't kill the whole reward circuit — sustained or extreme activity does. Phase 3 implementation will use an exponentially-decayed running average; Phase 1 just records the events.

## Phase 1 schema (migration 070)

```sql
CREATE TABLE habenula_triggers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
event_kind TEXT NOT NULL CHECK(event_kind IN ('omission', 'aversive', 'repeated_failure', 'other')),
default_pe REAL NOT NULL DEFAULT -0.1 CHECK(default_pe <= 0.0),
description TEXT,
created_at TEXT NOT NULL DEFAULT (...)
);

CREATE TABLE habenula_firings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
fired_at TEXT NOT NULL DEFAULT (...),
agent_id TEXT,
trigger_id INTEGER REFERENCES habenula_triggers(id),
event_kind TEXT NOT NULL CHECK(event_kind IN ('omission', 'aversive', 'repeated_failure', 'other')),
signed_pe REAL NOT NULL CHECK(signed_pe <= 0.0),
context_hash TEXT,
source_event_id INTEGER,
notes TEXT
);

CREATE TABLE habenula_state (
id INTEGER PRIMARY KEY CHECK (id = 1),
tonic_activity REAL NOT NULL DEFAULT 0.0,
phasic_burst REAL NOT NULL DEFAULT 0.0,
rolling_disappointment_24h INTEGER NOT NULL DEFAULT 0,
last_firing_at TEXT,
suggested_da_damp REAL NOT NULL DEFAULT 0.0,
updated_at TEXT NOT NULL DEFAULT (...)
);
```

Seeded triggers: `reward_omission`, `retrieval_failure`, `repeated_low_utility`, `aversive_valence`, `task_abandoned`.

## Phase 1 MCP tools

- `habenula_status` — current state + last 5 firings + 24h aggregate
- `habenula_fire(trigger_name, signed_pe, agent_id, context_hash, ...)` — record a negative event
- `habenula_register_trigger` — idempotent UPSERT
- `habenula_history(limit, since, agent_id, event_kind)` — paginated firings
- `habenula_reset(agent_id)` — manually zero out tonic/phasic for an agent (admin-mode disengagement-cooldown)

## DoD

- Migration 070 applies cleanly to /tmp + live (with backup)
- Schema-version 70 row present
- 5 seed triggers + single state row
- 5 MCP tools registered + discoverable
- ≥7 tests passing
- Branch pushed, PR open, NOT merged to main
2 changes: 2 additions & 0 deletions src/agentmemory/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
mcp_tools_entorhinal_grid,
mcp_tools_expertise,
mcp_tools_federation,
mcp_tools_habenula,
mcp_tools_health,
mcp_tools_hippocampal_subfields,
mcp_tools_immunity,
Expand Down Expand Up @@ -97,6 +98,7 @@
mcp_tools_entorhinal_grid,
mcp_tools_expertise,
mcp_tools_federation,
mcp_tools_habenula,
mcp_tools_health,
mcp_tools_hippocampal_subfields,
mcp_tools_immunity,
Expand Down
Loading
Loading