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
30 changes: 15 additions & 15 deletions agent/learning_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from pathlib import Path
from typing import Any

_MEMORY_DELIM = "\n§\n"
_MEMORY_FILES = {"memory": "MEMORY.md", "profile": "USER.md"}


Expand Down Expand Up @@ -63,20 +62,18 @@ def _memory_local_index(source: str, global_index: int) -> int:
return global_index - sum(1 for c in cards if c.get("source") == "memory")


def _read_chunks(path: Path) -> list[str]:
"""Raw ``§``-delimited chunks, preserving formatting; empties dropped to
match ``_memory_cards`` indexing."""
text = path.read_text(encoding="utf-8")

return [c for c in text.split(_MEMORY_DELIM) if c.strip()]
def _locate_memory(source: str, gidx: int) -> tuple[Path, list[str], int]:
"""Resolve a memory card to its file, all §-delimited entries, and local index.

Entries come from ``MemoryStore._read_file`` — the same parser the memory
tool uses — so journey indices stay aligned with what the graph renders.
"""
from tools.memory_tool import MemoryStore

def _locate_memory(source: str, gidx: int) -> tuple[Path, list[str], int]:
"""Resolve a memory card to its file, all chunks, and local index."""
path = _memories_dir() / _MEMORY_FILES[source]
if not path.exists():
raise ValueError(f"{path.name} not found")
chunks = _read_chunks(path)
chunks = MemoryStore._read_file(path)
local = _memory_local_index(source, gidx)
if not 0 <= local < len(chunks):
raise ValueError("memory node id is stale — refresh the graph")
Expand Down Expand Up @@ -149,7 +146,7 @@ def _delete_memory(node_id: str) -> dict[str, Any]:
path, chunks, local = _locate_memory(source, gidx)

del chunks[local]
_write_chunks(path, chunks)
_write_memory(path, chunks)

return {"ok": True, "message": f"deleted memory from {path.name}"}

Expand Down Expand Up @@ -184,17 +181,20 @@ def _edit_memory(node_id: str, content: str) -> dict[str, Any]:
path, chunks, local = _locate_memory(source, gidx)

chunks[local] = body
_write_chunks(path, chunks)
_write_memory(path, chunks)

return {"ok": True, "message": f"updated memory in {path.name}"}


# ── Helpers ─────────────────────────────────────────────────────────────────


def _write_chunks(path: Path, chunks: list[str]) -> None:
body = _MEMORY_DELIM.join(c.strip() for c in chunks)
path.write_text(f"{body}\n" if body else "", encoding="utf-8")
def _write_memory(path: Path, chunks: list[str]) -> None:
"""Atomic temp-file + rename via the memory tool, so a concurrent reader
never sees a half-written file (and the §-join stays single-sourced)."""
from tools.memory_tool import MemoryStore

MemoryStore._write_file(path, [c.strip() for c in chunks if c.strip()])


def _clear_skill_cache() -> None:
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/app/starmap/star-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import type { StarmapGraph } from '@/types/hermes'
import { computePalette, memoryInkFor, resolveRgb, rgba } from './color'
import { RING_OUTER, TILT, ZOOM_MAX, ZOOM_MIN } from './constants'
import { clamp, distToSegmentSq, fitScale, fitViewport, nodeRadius } from './geometry'
import { NodeContextMenu, type NodeMenuTarget } from './node-context-menu'
import { drawScene, drawScramble } from './render'
import { decodeShareCode, encodeShareCode, ShareCodeError } from './share-code'
import { ShareControls } from './share-controls'
import { buildSimulation } from './simulation'
import { formatDate } from './text'
import { buildTimeAxis, dateAtReveal, type TimeAxis } from './time-axis'
import { NodeContextMenu, type NodeMenuTarget } from './node-context-menu'
import { Timeline } from './timeline'
import type { FadeBuckets, MemoryCard, Palette, Ring, RingLabelRect, SimLink, SimNode, Viewport } from './types'

Expand Down Expand Up @@ -918,10 +918,10 @@ export function StarMap({
<div className="relative min-h-0 flex-1 overflow-hidden" ref={wrapRef}>
<canvas
className="block touch-none select-none text-foreground"
onContextMenu={onContextMenu}
onDoubleClick={resetView}
onMouseDown={onMouseDown}
onMouseLeave={onMouseLeave}
onContextMenu={onContextMenu}
onMouseMove={onMouseMove}
onMouseUp={endDrag}
onWheel={onWheel}
Expand Down
14 changes: 14 additions & 0 deletions tests/agent/test_learning_mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,17 @@ def test_edit_skill_rewrites_and_validates(home):

def test_missing_skill_detail(home):
assert not lm.node_detail("nonexistent-skill")["ok"]


def test_memory_writes_match_memory_tool_format(home):
"""A journey mutation must leave the file byte-identical to what the memory
tool itself writes — same §-join, no trailing-newline drift — so the two
surfaces never fight over format and indices stay aligned."""
from tools.memory_tool import ENTRY_DELIMITER, MemoryStore

assert lm.edit_node("memory:memory:0", "alpha rewritten")["ok"]
path = home / "memories" / "MEMORY.md"
entries = MemoryStore._read_file(path)

assert entries == ["alpha rewritten", "beta note"]
assert path.read_text(encoding="utf-8") == ENTRY_DELIMITER.join(entries)
Loading