Compare commits

...

2 Commits

Author SHA1 Message Date
Alexander Whitestone
ef5e0ec439 feat: add evennia mind palace architecture (refs #567)
Some checks are pending
Smoke Test / smoke (pull_request) Waiting to run
2026-04-15 00:20:31 -04:00
Alexander Whitestone
9f55394639 test: define evennia mind palace acceptance for #567 2026-04-15 00:15:27 -04:00
5 changed files with 549 additions and 0 deletions

146
evennia-mind-palace.md Normal file
View File

@@ -0,0 +1,146 @@
# Evennia as Agent Mind Palace — Spatial Memory Architecture
Issue #567 is the missing why behind the Evennia lane. The Tower Game is the demo, but the actual target is a spatial memory substrate where Timmy can visit the right room, see the right objects, and load only the context needed for the current task.
The existing Evennia work in `timmy-home` already proves the body exists:
- `reports/production/2026-03-28-evennia-world-proof.md` proves the local Evennia world, first room graph, telnet roundtrip, and Hermes/MCP control path.
- `reports/production/2026-03-28-evennia-training-baseline.md` proves Hermes session IDs can align with Evennia telemetry and replay/eval artifacts.
- `specs/evennia-mind-palace-layout.md` and `specs/evennia-implementation-and-training-plan.md` already define the first rooms and objects.
This document turns those pieces into a memory architecture: one room that injects live work context, one object that exposes a mutable fact, and one burn-cycle packet that tells Timmy what to do next.
## GrepTard Memory Layers as Spatial Primitives
| Layer | Spatial primitive | Hermes equivalent | Evennia mind-palace role |
| --- | --- | --- | --- |
| L1 | Rooms and thresholds | Static project context | The room itself defines what domain Timmy has entered and what baseline context loads immediately. |
| L2 | Objects, NPC attributes, meters | Mutable facts / KV memory | World state lives on inspectable things: ledgers, characters, fires, relationship values, energy meters. |
| L3 | Archive shelves and chronicles | Searchable history | Prior events become searchable books, reports, and proof artifacts inside an archive room. |
| L4 | Teaching NPCs and rituals | Procedural skills | The right NPC or room interaction teaches the right recipe without loading every skill into working memory. |
| L5 | Movement and routing | Retrieval logic | Choosing the room is choosing the retrieval path; movement decides what context gets loaded now. |
## Spatial Retrieval Architecture
```mermaid
flowchart TD
A[Timmy burn cycle] --> B[Enter Hall of Knowledge]
B --> C[Ambient issue board]
B --> D[The Ledger]
B --> E[/status forge]
C --> F[Current Gitea issue topology]
D --> G[One mutable fact from durable memory]
E --> H[Repo + branch + blockers]
F --> I[Selective action prompt]
G --> I
H --> I
I --> J[Act in the correct room or hand off to another room]
```
The Hall of Knowledge is not an archive dump. It is a selective preload surface.
On room entry Timmy should receive only:
1. the currently active Gitea issues relevant to the present lane,
2. one mutable fact from durable memory that changes the next action,
3. the current Timmy burn cycle packet (repo, branch, blockers, current objective).
That gives Timmy enough context to act without rehydrating the entire project or every prior transcript.
## Mapping the 16 tracked Evennia issues to mind-palace layers
These are the 16 issues explicitly named in issue #567. Some are now closed, but they still map the architecture surface we need.
| Issue | State | Layer | Spatial role | Why it matters |
| --- | --- | --- | --- | --- |
| #508 — [P0] Tower Game — contextual dialogue (NPCs recycle 15 lines forever) | closed | L4 | Dialogue tutor NPCs | Contextual dialogue is procedural behavior attached to the right NPC in the right room. |
| #509 — [P0] Tower Game — trust must decrease, conflict must exist | closed | L2 | Mutable relationship state | Trust, conflict, and alliance are inspectable changing world facts. |
| #510 — [P0] Tower Game — narrative arc (tick 200 = tick 20) | closed | L3 | Archive chronicle | Without searchable history, the world cannot accumulate narrative memory. |
| #511 — [P0] Tower Game — energy must meaningfully constrain | open | L2 | Mutable world meter | Energy belongs in visible world state, not hidden prompt assumptions. |
| #512 — [P1] Sonnet workforce — full end-to-end smoke test | open | L3 | Proof shelf | Proof artifacts should live in the archive so Timmy can revisit what really worked. |
| #513 — [P1] Tower Game — world events must affect gameplay | open | L2 | Event-reactive room state | A room that never changes cannot carry durable meaning. |
| #514 — [P1] Tower Game — items that change the world | open | L2 | Interactive objects | Objects should alter world state and teach consequences through interaction. |
| #515 — [P1] Tower Game — NPC-NPC relationships | open | L2 | Social graph in-world | Relationships should persist on characters rather than disappearing into transcripts. |
| #516 — [P1] Tower Game — Timmy richer dialogue + internal monologue | closed | L4 | Inner-room teaching patterns | Timmy's own inner behavior is part of the procedural layer. |
| #517 — [P1] Tower Game — NPCs move between rooms with purpose | open | L5 | Movement-driven retrieval | Purposeful movement is retrieval logic made spatial. |
| #534 — [BEZ-P0] Fix Evennia settings on 104.131.15.18 — remove bad port tuples, DB is ready | open | L1 | Runtime threshold | The threshold has to boot cleanly before any room can carry memory. |
| #535 — [BEZ-P0] Install Tailscale on Bezalel VPS (104.131.15.18) for internal networking | open | L1 | Network threshold | Static network reachability defines which houses can be visited. |
| #536 — [BEZ-P1] Create Bezalel Evennia world with themed rooms and characters | open | L1 | First room graph | Themed rooms and characters are the static scaffold of the mind palace. |
| #537 — [BRIDGE-P1] Deploy Evennia bridge API on all worlds — sync presence and events | closed | L5 | Cross-world routing | Movement across worlds is retrieval across sovereign houses. |
| #538 — [ALLEGRO-P1] Fix SSH access from Mac to Allegro VPS (167.99.126.228) | closed | L1 | Operator ingress | If the operator cannot reach a house, its memory cannot be visited. |
| #539 — [ARCH-P2] Implement Evennia hub-and-spoke federation architecture | closed | L5 | Federated retrieval map | Federation turns world travel into selective retrieval instead of one giant memory blob. |
## Milestone 1 — One Room, One Object, One Mutable Fact
Milestone 1 is deliberately small.
Room:
- `Hall of Knowledge`
- Purpose: load live issue topology plus the current Timmy burn cycle before action begins.
Object:
- `The Ledger`
- Purpose: expose one mutable fact from durable memory so room entry proves stateful recall rather than static reference text.
Mutable fact:
- Example fact used in this implementation: `canonical-evennia-body = timmy_world on localhost:4001 remains the canonical local body while room entry preloads live issue topology.`
Timmy burn cycle wiring:
- `evennia_tools/mind_palace.py` defines `BurnCycleSnapshot`, `MutableFact`, the 16-issue layer map, and `build_hall_of_knowledge_entry(...)`.
- `render_room_entry_proof(...)` renders a deterministic proof packet showing exactly what Timmy sees when entering the Hall of Knowledge.
- `scripts/evennia/render_mind_palace_entry_proof.py` prints the proof artifact used for issue commentary and verification.
The important point is architectural, not cosmetic: room entry is now a retrieval event. The room decides what context loads. The object proves mutable memory. The burn-cycle snapshot tells Timmy what to do with the loaded context.
## Proof of Room Entry Injecting Context
The proof below is the deterministic output rendered by `python3 scripts/evennia/render_mind_palace_entry_proof.py`.
```text
ENTER Hall of Knowledge
Purpose: Load live issue topology, current burn-cycle focus, and the minimum durable facts Timmy needs before acting.
Ambient context:
- Room entry into Hall of Knowledge preloads active Gitea issue topology for Timmy_Foundation/timmy-home.
- #511 [P0] Tower Game — energy must meaningfully constrain [open · L2 · Mutable world meter]
- #512 [P1] Sonnet workforce — full end-to-end smoke test [open · L3 · Proof shelf]
- #513 [P1] Tower Game — world events must affect gameplay [open · L2 · Event-reactive room state]
- Ledger fact canonical-evennia-body: timmy_world on localhost:4001 remains the canonical local body while room entry preloads live issue topology.
- Timmy burn cycle focus: issue #567 on fix/567 — Evennia as Agent Mind Palace — Spatial Memory Architecture
- Operator lane: BURN-7-1
Object: The Ledger
- canonical-evennia-body: timmy_world on localhost:4001 remains the canonical local body while room entry preloads live issue topology.
- source: reports/production/2026-03-28-evennia-world-proof.md
Timmy burn cycle:
- repo: Timmy_Foundation/timmy-home
- branch: fix/567
- active issue: #567
- focus: Evennia as Agent Mind Palace — Spatial Memory Architecture
- operator: BURN-7-1
Command surfaces:
- /who lives here -> #511 ... ; #512 ... ; #513 ...
- /status forge -> Timmy_Foundation/timmy-home @ fix/567 (issue #567)
- /what is broken -> Comment on issue #567 with room-entry proof after PR creation
```
That proof is enough to satisfy the milestone claim:
- one room exists conceptually and in code,
- one object carries a mutable fact,
- room entry injects current issue topology and the active Timmy burn cycle,
- the output is deterministic and comment-ready for Gitea issue #567.
## Why this architecture is worth doing
The point is not to turn memory into a theatrical MUD skin. The point is to make retrieval selective, embodied, and inspectable.
What improves immediately:
- Timmy no longer has to reload every repo fact on every task.
- Durable facts become objects and meters rather than hidden prompt sludge.
- Searchable history gets a real place to live.
- Procedural skill loading can become room/NPC specific instead of global.
- Movement itself becomes the retrieval primitive.
## Next steps after Milestone 1
1. Attach Hall of Knowledge entry to live Gitea issue fetches instead of the current deterministic proof subset.
2. Promote The Ledger from one mutable fact to a live view over Timmy memory / fact-store rows.
3. Add an Archive room surface that renders searchable history excerpts as in-world books.
4. Bind Builder / Archivist NPCs to skill-category loading so L4 becomes interactive, not just descriptive.
5. Route movement between rooms and worlds through the bridge/federation work already tracked by #537 and #539.

View File

@@ -0,0 +1,270 @@
from __future__ import annotations
from dataclasses import asdict, dataclass
HALL_OF_KNOWLEDGE = "Hall of Knowledge"
LEDGER_OBJECT = "The Ledger"
@dataclass(frozen=True)
class MindPalaceIssue:
issue_number: int
state: str
title: str
layer: str
spatial_role: str
rationale: str
def summary_line(self) -> str:
return f"#{self.issue_number} {self.title} [{self.state} · {self.layer} · {self.spatial_role}]"
@dataclass(frozen=True)
class MutableFact:
key: str
value: str
source: str
def to_dict(self) -> dict[str, str]:
return asdict(self)
@dataclass(frozen=True)
class BurnCycleSnapshot:
repo: str
branch: str
active_issue: int
focus: str
active_operator: str
blockers: tuple[str, ...] = ()
def to_dict(self) -> dict[str, object]:
return {
"repo": self.repo,
"branch": self.branch,
"active_issue": self.active_issue,
"focus": self.focus,
"active_operator": self.active_operator,
"blockers": list(self.blockers),
}
EVENNIA_MIND_PALACE_ISSUES = (
MindPalaceIssue(
508,
"closed",
"[P0] Tower Game — contextual dialogue (NPCs recycle 15 lines forever)",
"L4",
"Dialogue tutor NPCs",
"Contextual dialogue belongs in procedural behavior surfaces so the right NPC can teach or respond based on current room state.",
),
MindPalaceIssue(
509,
"closed",
"[P0] Tower Game — trust must decrease, conflict must exist",
"L2",
"Mutable relationship state",
"Trust, resentment, and alliance changes are world facts that should live on objects and characters, not in flat prompt text.",
),
MindPalaceIssue(
510,
"closed",
"[P0] Tower Game — narrative arc (tick 200 = tick 20)",
"L3",
"Archive chronicle",
"A spatial memory needs a chronicle room where prior events can be replayed and searched so the world can develop an actual arc.",
),
MindPalaceIssue(
511,
"open",
"[P0] Tower Game — energy must meaningfully constrain",
"L2",
"Mutable world meter",
"Energy is a changing state variable that should be visible in-room and affect what actions remain possible.",
),
MindPalaceIssue(
512,
"open",
"[P1] Sonnet workforce — full end-to-end smoke test",
"L3",
"Proof shelf",
"End-to-end smoke traces belong in the archive so world behavior can be proven, revisited, and compared over time.",
),
MindPalaceIssue(
513,
"open",
"[P1] Tower Game — world events must affect gameplay",
"L2",
"Event-reactive room state",
"If storms, fire, or decay do not alter the room state, the world is decorative instead of mnemonic.",
),
MindPalaceIssue(
514,
"open",
"[P1] Tower Game — items that change the world",
"L2",
"Interactive objects",
"World-changing items are exactly the kind of mutable facts and affordances that a spatial memory substrate should expose.",
),
MindPalaceIssue(
515,
"open",
"[P1] Tower Game — NPC-NPC relationships",
"L2",
"Social graph in-world",
"Relationships should persist on characters and become inspectable through spatial proximity rather than hidden transcript-only state.",
),
MindPalaceIssue(
516,
"closed",
"[P1] Tower Game — Timmy richer dialogue + internal monologue",
"L4",
"Inner-room teaching patterns",
"Internal monologue and richer dialogue are procedural behaviors that can be attached to rooms, NPCs, and character routines.",
),
MindPalaceIssue(
517,
"open",
"[P1] Tower Game — NPCs move between rooms with purpose",
"L5",
"Movement-driven retrieval",
"Purposeful movement is retrieval logic made spatial: who enters which room determines what knowledge is loaded and acted on.",
),
MindPalaceIssue(
534,
"open",
"[BEZ-P0] Fix Evennia settings on 104.131.15.18 — remove bad port tuples, DB is ready",
"L1",
"Runtime threshold",
"Before the mind palace can be inhabited, the base Evennia runtime topology has to load cleanly at the threshold.",
),
MindPalaceIssue(
535,
"open",
"[BEZ-P0] Install Tailscale on Bezalel VPS (104.131.15.18) for internal networking",
"L1",
"Network threshold",
"Network identity and reachability are static environment facts that determine which rooms and worlds are even reachable.",
),
MindPalaceIssue(
536,
"open",
"[BEZ-P1] Create Bezalel Evennia world with themed rooms and characters",
"L1",
"First room graph",
"Themed rooms and characters are the static world scaffold that lets memory become place instead of prose.",
),
MindPalaceIssue(
537,
"closed",
"[BRIDGE-P1] Deploy Evennia bridge API on all worlds — sync presence and events",
"L5",
"Cross-world routing",
"Bridge APIs turn movement across worlds into retrieval across houses instead of forcing one global prompt blob.",
),
MindPalaceIssue(
538,
"closed",
"[ALLEGRO-P1] Fix SSH access from Mac to Allegro VPS (167.99.126.228)",
"L1",
"Operator ingress",
"Operator access is part of the static world boundary: if the house cannot be reached, its memory cannot be visited.",
),
MindPalaceIssue(
539,
"closed",
"[ARCH-P2] Implement Evennia hub-and-spoke federation architecture",
"L5",
"Federated retrieval map",
"Federation turns room-to-room travel into selective retrieval across sovereign worlds instead of a single central cache.",
),
)
OPEN_EVENNIA_MIND_PALACE_ISSUES = tuple(issue for issue in EVENNIA_MIND_PALACE_ISSUES if issue.state == "open")
def build_hall_of_knowledge_entry(
active_issues: tuple[MindPalaceIssue, ...] | list[MindPalaceIssue],
ledger_fact: MutableFact,
burn_cycle: BurnCycleSnapshot,
) -> dict[str, object]:
issue_lines = [issue.summary_line() for issue in active_issues]
blocker_lines = list(burn_cycle.blockers) or ["No blockers recorded."]
return {
"room": {
"key": HALL_OF_KNOWLEDGE,
"purpose": "Load live issue topology, current burn-cycle focus, and the minimum durable facts Timmy needs before acting.",
},
"object": {
"key": LEDGER_OBJECT,
"purpose": "Expose one mutable fact from Timmy's durable memory so the room proves stateful recall instead of static documentation.",
"fact": ledger_fact.to_dict(),
},
"ambient_context": [
f"Room entry into {HALL_OF_KNOWLEDGE} preloads active Gitea issue topology for {burn_cycle.repo}.",
*issue_lines,
f"Ledger fact {ledger_fact.key}: {ledger_fact.value}",
f"Timmy burn cycle focus: issue #{burn_cycle.active_issue} on {burn_cycle.branch}{burn_cycle.focus}",
f"Operator lane: {burn_cycle.active_operator}",
],
"burn_cycle": burn_cycle.to_dict(),
"commands": {
"/who lives here": "; ".join(issue_lines) or "No issues loaded.",
"/status forge": f"{burn_cycle.repo} @ {burn_cycle.branch} (issue #{burn_cycle.active_issue})",
"/what is broken": "; ".join(blocker_lines),
},
}
def render_room_entry_proof(
active_issues: tuple[MindPalaceIssue, ...] | list[MindPalaceIssue],
ledger_fact: MutableFact,
burn_cycle: BurnCycleSnapshot,
) -> str:
entry = build_hall_of_knowledge_entry(active_issues, ledger_fact, burn_cycle)
lines = [
f"ENTER {entry['room']['key']}",
f"Purpose: {entry['room']['purpose']}",
"Ambient context:",
]
lines.extend(f"- {line}" for line in entry["ambient_context"])
lines.extend(
[
f"Object: {entry['object']['key']}",
f"- {entry['object']['fact']['key']}: {entry['object']['fact']['value']}",
f"- source: {entry['object']['fact']['source']}",
"Timmy burn cycle:",
f"- repo: {burn_cycle.repo}",
f"- branch: {burn_cycle.branch}",
f"- active issue: #{burn_cycle.active_issue}",
f"- focus: {burn_cycle.focus}",
f"- operator: {burn_cycle.active_operator}",
"Command surfaces:",
f"- /who lives here -> {entry['commands']['/who lives here']}",
f"- /status forge -> {entry['commands']['/status forge']}",
f"- /what is broken -> {entry['commands']['/what is broken']}",
]
)
return "\n".join(lines)
def demo_room_entry_proof() -> str:
return render_room_entry_proof(
active_issues=OPEN_EVENNIA_MIND_PALACE_ISSUES[:3],
ledger_fact=MutableFact(
key="canonical-evennia-body",
value="timmy_world on localhost:4001 remains the canonical local body while room entry preloads live issue topology.",
source="reports/production/2026-03-28-evennia-world-proof.md",
),
burn_cycle=BurnCycleSnapshot(
repo="Timmy_Foundation/timmy-home",
branch="fix/567",
active_issue=567,
focus="Evennia as Agent Mind Palace — Spatial Memory Architecture",
active_operator="BURN-7-1",
blockers=("Comment on issue #567 with room-entry proof after PR creation",),
),
)

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python3
from pathlib import Path
import sys
REPO_ROOT = Path(__file__).resolve().parents[2]
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from evennia_tools.mind_palace import demo_room_entry_proof
if __name__ == "__main__":
print(demo_room_entry_proof())

View File

@@ -0,0 +1,88 @@
from evennia_tools.mind_palace import (
EVENNIA_MIND_PALACE_ISSUES,
HALL_OF_KNOWLEDGE,
LEDGER_OBJECT,
BurnCycleSnapshot,
MutableFact,
build_hall_of_knowledge_entry,
render_room_entry_proof,
)
import subprocess
import sys
from pathlib import Path
ISSUE_NUMBERS = (508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 534, 535, 536, 537, 538, 539)
def test_issue_map_covers_all_16_tracked_evennia_issues() -> None:
numbers = tuple(item.issue_number for item in EVENNIA_MIND_PALACE_ISSUES)
assert numbers == ISSUE_NUMBERS
assert len({item.issue_number for item in EVENNIA_MIND_PALACE_ISSUES}) == 16
assert {item.layer for item in EVENNIA_MIND_PALACE_ISSUES} == {"L1", "L2", "L3", "L4", "L5"}
def test_hall_of_knowledge_entry_injects_room_object_fact_and_burn_cycle() -> None:
entry = build_hall_of_knowledge_entry(
active_issues=EVENNIA_MIND_PALACE_ISSUES[:3],
ledger_fact=MutableFact(
key="active-world",
value="timmy_world on localhost:4001 remains the canonical Evennia body",
source="reports/production/2026-03-28-evennia-world-proof.md",
),
burn_cycle=BurnCycleSnapshot(
repo="Timmy_Foundation/timmy-home",
branch="fix/567",
active_issue=567,
focus="Evennia as Agent Mind Palace — Spatial Memory Architecture",
active_operator="BURN-7-1",
blockers=("Need room-entry proof comment on issue #567",),
),
)
assert entry["room"]["key"] == HALL_OF_KNOWLEDGE
assert entry["object"]["key"] == LEDGER_OBJECT
assert entry["object"]["fact"]["key"] == "active-world"
assert entry["burn_cycle"]["branch"] == "fix/567"
assert entry["burn_cycle"]["active_issue"] == 567
assert any("Tower Game" in line or "Evennia" in line for line in entry["ambient_context"])
assert entry["commands"]["/what is broken"]
def test_render_room_entry_proof_contains_concrete_injected_context() -> None:
proof = render_room_entry_proof(
active_issues=EVENNIA_MIND_PALACE_ISSUES[:2],
ledger_fact=MutableFact(
key="ledger-memory",
value="The Hall of Knowledge should preload live issue topology before Timmy acts",
source="issue #567",
),
burn_cycle=BurnCycleSnapshot(
repo="Timmy_Foundation/timmy-home",
branch="fix/567",
active_issue=567,
focus="Evennia as Agent Mind Palace — Spatial Memory Architecture",
active_operator="BURN-7-1",
blockers=("Need proof comment",),
),
)
assert "Hall of Knowledge" in proof
assert "The Ledger" in proof
assert "fix/567" in proof
assert "issue #567" in proof
assert "Need proof comment" in proof
assert "[P0] Tower Game" in proof or "[BEZ-P0]" in proof
def test_render_mind_palace_script_emits_room_entry_proof() -> None:
root = Path(__file__).resolve().parents[1]
output = subprocess.check_output(
[sys.executable, "scripts/evennia/render_mind_palace_entry_proof.py"],
cwd=root,
text=True,
)
assert "ENTER Hall of Knowledge" in output
assert "Timmy burn cycle" in output

View File

@@ -0,0 +1,32 @@
from pathlib import Path
ISSUE_NUMBERS = (508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 534, 535, 536, 537, 538, 539)
def test_evennia_mind_palace_doc_exists_with_required_sections() -> None:
doc = Path("evennia-mind-palace.md").read_text(encoding="utf-8")
assert "# Evennia as Agent Mind Palace — Spatial Memory Architecture" in doc
assert "## GrepTard Memory Layers as Spatial Primitives" in doc
assert "## Milestone 1 — One Room, One Object, One Mutable Fact" in doc
assert "## Proof of Room Entry Injecting Context" in doc
assert "Hall of Knowledge" in doc
assert "The Ledger" in doc
assert "Timmy burn cycle" in doc
def test_evennia_mind_palace_doc_maps_all_16_tracked_issues() -> None:
doc = Path("evennia-mind-palace.md").read_text(encoding="utf-8")
for issue_number in ISSUE_NUMBERS:
assert f"#{issue_number}" in doc
assert "| Layer | Spatial primitive |" in doc
assert "| Issue | State | Layer | Spatial role | Why it matters |" in doc
assert "L1" in doc
assert "L2" in doc
assert "L3" in doc
assert "L4" in doc
assert "L5" in doc