Skip to content
Merged
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
40 changes: 4 additions & 36 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,7 @@ on:
branches: [main, master]

jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Lint with ruff
run: ruff check .

test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
Expand All @@ -33,25 +17,9 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]" || pip install -e . || pip install -r requirements.txt
pip install pytest
- name: Install package and test dependencies
run: pip install -e ".[dev]"
- name: Lint (ruff)
run: ruff check .
- name: Run tests
run: pytest -v

build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build tools
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build --sdist --wheel
1 change: 1 addition & 0 deletions .venv/bin/python
1 change: 1 addition & 0 deletions .venv/bin/python3
1 change: 1 addition & 0 deletions .venv/bin/python3.12
1 change: 1 addition & 0 deletions .venv/lib64
5 changes: 5 additions & 0 deletions .venv/pyvenv.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
home = /usr/bin
include-system-site-packages = false
version = 3.12.3
executable = /usr/bin/python3.12
command = /usr/bin/python3 -m venv /tmp/claude-1000/-home-eileen/c25f18c4-8752-45a5-bacc-1c3e70a86ab7/scratchpad/round4/exocortex/.venv
2 changes: 1 addition & 1 deletion AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Who I Am

I watch over exocortex. 🧠 Persistent cognitive substrate for multi-agent systems — S3-compatible memory, shadow rendering, tiered compute, ESP32 support
I watch over exocortex. 🧠 Persistent cognitive substrate for multi-agent systems — tiered in-memory store with optional SurrealDB backend, shadow rendering, tiered compute, ESP32-friendly TAP protocol

I reside in this repository. This is my room.

Expand Down
14 changes: 14 additions & 0 deletions PRODUCTION_HARDENING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Production Hardening — Round 4 (2026-07-11)

This document tracks concrete production-readiness fixes for this round.
Each fix is verified independently (real test suite + ruff) before being pushed.

## Scope

Audit of actual source + tests (not just README) for:
- Bugs: wrong logic, off-by-one, untested error paths
- Fake-green tests (always pass regardless of code)
- README claims that don't match what the code actually does
- Missing but feasible test coverage for real branches

See git log on branch `production-round4-2026-07-11` for the per-fix commits.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[![CI](https://github.com/SuperInstance/exocortex/actions/workflows/ci.yml/badge.svg)](https://github.com/SuperInstance/exocortex/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

🧠 Persistent cognitive substrate for multi-agent systems — S3-compatible memory, shadow rendering, tiered compute, ESP32 support
🧠 Persistent cognitive substrate for multi-agent systems — tiered in-memory store with optional SurrealDB backend, shadow rendering, tiered compute, ESP32-friendly TAP protocol

---

Expand Down Expand Up @@ -39,14 +39,32 @@ Part of the [SuperInstance](https://github.com/SuperInstance) fleet ecosystem
- [🧮 constraint-tminus-bridge](https://github.com/SuperInstance/constraint-tminus-bridge) — Constraint networks for agent alignment
- [🎻 symphony-orchestrator](https://github.com/SuperInstance/symphony-orchestrator) — Full stack orchestrator

## Component Status

Feature maturity, stated honestly (a claim previously described storage as
"S3-compatible" — there is **no** S3 / object-storage code; storage is an
in-memory tiered store with an optional SurrealDB backend).

| Component | Status | Notes |
|-----------|--------|-------|
| Tiered in-memory store (hot/warm/cold, half-life decay) | ✅ Implemented | In-process `OrderedDict`s; no persistence on restart |
| SurrealDB backend | 🟡 Optional | `SurrealDBMemoryLayer` exists; falls back to in-memory when the `surrealdb` package/DB is unavailable. Not exercised against a live DB in CI |
| Embedding | 🟡 Placeholder | `Operation.EMBED` returns **random** unit vectors — recall is by random-vector similarity, not semantic search |
| MicroNN train/predict | ✅ Implemented | Pure-Python single-hidden-layer net; training is simulated (random accuracy) |
| Dream cycle (k-means consolidation) | ✅ Implemented | Pure-Python k-means, no sklearn |
| Resonance engine | ✅ Implemented | Cross-agent cosine-similarity overlap detection |
| Cortical bus (pub/sub) | ✅ Implemented | Asyncio `PriorityQueue` + fan-out |
| FastAPI REST + TAP protocol | ✅ Implemented | TAP = plain-text endpoints sized for ESP32 |
| Textual TUI ("Plato's Cave") | ✅ Implemented | Requires a real terminal |
| A2A / MCP protocol servers | 🟡 Stub | Enum values only; no server implementations |

## Cross-Implementation

This component exists in two languages:
- **Python** (`pip install si-exocortex`) — [SuperInstance/exocortex](https://github.com/SuperInstance/exocortex)
- **Rust** (`cargo add exocortex`) — [SuperInstance/exocortex-rs](https://github.com/SuperInstance/exocortex-rs)

Both implement the same specification. Choose based on your runtime.

## License

MIT
Expand Down
24 changes: 15 additions & 9 deletions src/bus/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,22 @@ async def emit(self, event_type: str, source: str, **kwargs) -> bool:
return await self.publish(event)

def should_render(self, event: CortexEvent) -> bool:
"""Rate limit: max N events per trace_id."""
"""Rate limit: render only the first N events per trace_id.

Returns True for the first ``_max_per_trace`` events of a trace and
False for every subsequent event, so a chatty trace cannot flood the
shadow wall.

NOTE: this is an opt-in filter — it is *not* invoked automatically by
the dispatch loop. Subscribers that want rate-limited shadows call it
explicitly. (An earlier version's comment claimed it would also render
a "summary" of the trace, but detecting the final event of a trace is
not possible here, so that was aspirational/incorrect.)
"""
trace = event.trace_id
self._trace_counts[trace] += 1
count = self._trace_counts[trace]
if count <= self._max_per_trace:
return True
# Always render the last one (summary)
if count == self._max_per_trace + 1:
return False # skip middle, will show summary
return False
count = self._trace_counts[trace] + 1
self._trace_counts[trace] = count
return count <= self._max_per_trace

async def start(self) -> None:
"""Start the bus dispatch loop."""
Expand Down
11 changes: 7 additions & 4 deletions src/compute/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ def predict(self, x: list[float]) -> tuple[int, float]:
"""Predict class + confidence."""
logits = self.forward(x)
# Softmax
max_l = max(logits)
exps = [math.exp(l - max_l) for l in logits]
max_logit = max(logits)
exps = [math.exp(logit - max_logit) for logit in logits]
total = sum(exps)
probs = [e / total for e in exps]
best = max(range(len(probs)), key=lambda i: probs[i])
Expand Down Expand Up @@ -185,6 +185,9 @@ async def reflex_check(self, source: str, value: float) -> dict[str, Any] | None
z_score = abs(value - bl["mean"]) / std

if z_score > 3.0:
# Capture the baseline this anomaly was measured against BEFORE
# we contaminate the running stats with the anomalous value.
baseline_mean = bl["mean"]
# Still update stats even for anomalies
new_mean = bl["mean"] + (value - bl["mean"]) / (n + 1)
new_var = ((n * bl["var"]) + (value - bl["mean"]) * (value - new_mean)) / (n + 1)
Expand All @@ -196,8 +199,8 @@ async def reflex_check(self, source: str, value: float) -> dict[str, Any] | None
"source": source,
"value": value,
"sigma": z_score,
"mean": new_mean,
"detail": f"{source} = {value:.1f} ({z_score:.1f}σ from mean {bl['mean']:.1f})",
"mean": baseline_mean,
"detail": f"{source} = {value:.1f} ({z_score:.1f}σ from mean {baseline_mean:.1f})",
}

# Update running stats
Expand Down
5 changes: 2 additions & 3 deletions src/compute/dream.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from dataclasses import dataclass, field
from typing import Any

from ..core.types import CortexEvent, MemoryEntry
from ..core.types import CortexEvent

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -251,7 +251,7 @@ async def run(self) -> DreamReport:

clusters: list[DreamCluster] = []
for ci in range(n_clusters):
member_indices = [i for i, l in enumerate(labels) if l == ci]
member_indices = [i for i, label in enumerate(labels) if label == ci]
if not member_indices:
continue
members = [embedded[i] for i in member_indices]
Expand Down Expand Up @@ -361,7 +361,6 @@ def _generate_narrative(self, report: DreamReport) -> str:
parts = []

if report.clusters:
n_total = sum(len(c.memory_ids) for c in report.clusters)
parts.append(f"Dreaming over {report.memories_sampled} memories")
parts.append(f"into {len(report.clusters)} islands of thought")

Expand Down
23 changes: 22 additions & 1 deletion src/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,27 @@
import tomli as tomllib # type: ignore


def _parse_duration_days(raw: Any) -> float:
"""Parse a retention value into days as float.

Accepts a number ("30", 30, 30.0) or a string with an optional trailing
unit suffix, e.g. "30d", "30.5d". Always returns a float.
"""
if isinstance(raw, (int, float)):
return float(raw)
text = str(raw).strip()
# Strip a single optional trailing duration unit (e.g. "30d" -> "30").
# Use a conservative suffix check rather than str.rstrip, which would
# wrongly collapse values like "30dd" and is order-dependent.
suffixes = ("d", "days", "day")
lowered = text.lower()
for suffix in suffixes:
if lowered.endswith(suffix):
text = text[: -len(suffix)].strip()
break
return float(text)


@dataclass
class CortexConfig:
"""The single source of truth for exocortex configuration."""
Expand Down Expand Up @@ -65,7 +86,7 @@ def load(cls, path: Path | str = ".cortex.toml") -> CortexConfig:
return cls(
name=cortex.get("name", "default-cortex"),
memory_backend=mem.get("backend", "memory"),
memory_retention_days=float(mem.get("retention", "30d").rstrip("d")),
memory_retention_days=_parse_duration_days(mem.get("retention", "30d")),
embedding_dims=mem.get("embedding_dims", 384),
hot_window_seconds=mem.get("hot_window_seconds", 60.0),
warm_unreinforced_hours=mem.get("warm_unreinforced_hours", 24.0),
Expand Down
4 changes: 1 addition & 3 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

import asyncio
import logging
import signal
import sys

from .config import CortexConfig
from .bus import CorticalBus
Expand Down Expand Up @@ -68,7 +66,7 @@ async def stats_updater():

# Launch everything
logger.info(f"📡 Server: http://{config.host}:{config.port}")
logger.info(f"🔮 TUI: Plato's Cave")
logger.info("🔮 TUI: Plato's Cave")

await asyncio.gather(
server_instance.serve(),
Expand Down
11 changes: 8 additions & 3 deletions src/memory/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from collections import OrderedDict
from typing import Any

from ..core.types import MemoryEntry, Operation
from ..core.types import MemoryEntry

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -176,8 +176,13 @@ async def tick(self) -> dict[str, int]:
async def get_random_memories(self, n: int = 10) -> list[MemoryEntry]:
"""Sample random memories for dream cycle processing."""
import random
all_entries = list(self._warm.values()) + list(self._hot.values())
return random.sample(all_entries, min(n, len(all_entries)))
# Dedupe by id: every hot entry is also present in warm, so naively
# concatenating the tiers yields duplicate samples.
by_id: dict[str, MemoryEntry] = {}
for entry in list(self._warm.values()) + list(self._hot.values()) + list(self._cold.values()):
by_id[entry.id] = entry
unique = list(by_id.values())
return random.sample(unique, min(n, len(unique)))

async def get_recent_memories(self, since: float, limit: int = 100) -> list[MemoryEntry]:
"""Get memories created after a timestamp."""
Expand Down
12 changes: 7 additions & 5 deletions src/memory/surrealdb_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@
import math
import time
import uuid
from collections import OrderedDict
from typing import Any

from . import MemoryLayer, LRU_MAX, HOT_WINDOW_SECONDS, WARM_UNREINFORCED_HOURS, COLD_CONFIDENCE_THRESHOLD
from . import MemoryLayer, HOT_WINDOW_SECONDS, WARM_UNREINFORCED_HOURS

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -452,10 +451,13 @@ async def get_random_memories(self, n: int = 10) -> list[Any]:
except Exception as e:
logger.warning(f"SurrealDB random sample failed: {e}")

# Fallback: sample from in-memory
# Fallback: sample from in-memory (dedupe by id — hot ⊂ warm)
import random
all_entries = list(self._warm.values()) + list(self._hot.values())
return random.sample(all_entries, min(n, len(all_entries)))
by_id: dict[str, Any] = {}
for entry in list(self._warm.values()) + list(self._hot.values()) + list(self._cold.values()):
by_id[entry.id] = entry
unique = list(by_id.values())
return random.sample(unique, min(n, len(unique)))

async def get_recent_memories(self, since: float, limit: int = 100) -> list[Any]:
"""Get memories created after a timestamp."""
Expand Down
10 changes: 3 additions & 7 deletions src/protocols/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,17 @@

from __future__ import annotations

import time
import uuid
import logging
from typing import Any

from fastapi import FastAPI, HTTPException, Query as QueryParam
from fastapi import FastAPI, Query as QueryParam
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

from ..core.types import CortexRequest, CortexResponse, Operation, Protocol
from ..core.types import Operation
from ..bus import CorticalBus
from ..compute import ComputeEngine
from ..memory import MemoryLayer
from ..shadows import render_shadow

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -80,7 +77,6 @@ def create_app(bus: CorticalBus, compute: ComputeEngine, memory: MemoryLayer) ->

@app.post("/api/v1/embed")
async def embed(req: EmbedRequest) -> dict[str, Any]:
start = time.time()
result = await compute.execute(Operation.EMBED, {
"content": req.content, "dims": req.dims,
})
Expand Down Expand Up @@ -207,7 +203,7 @@ async def tap_remember(req: RememberRequest) -> str:
"""Plain text remember."""
result = await compute.execute(Operation.EMBED, {"content": req.content, "dims": 384})
embedding = result.get("embedding", [])
entry = await memory.remember(req.content, embedding, req.agent_id, req.tags)
await memory.remember(req.content, embedding, req.agent_id, req.tags)
await bus.emit("remember", req.agent_id, payload={"preview": req.content[:40]})
return "remembered"

Expand Down
4 changes: 1 addition & 3 deletions src/shadows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@

from __future__ import annotations

import math
import time
from dataclasses import dataclass
from enum import Enum

from ..core.types import CortexEvent, Operation, ShadowMode
from ..core.types import CortexEvent, Operation


class ShadowColor(str, Enum):
Expand Down
Loading
Loading