Skip to content

Commit ad02188

Browse files
feat(memory): add curated cross-session memory NodeTypes (#1207)
Extend MemoryGraph's NodeType enum with USER_CONTEXT/FEEDBACK/ PROJECT_CONTEXT/REFERENCE, mapped 1:1 onto the harness auto-memory's own proven taxonomy (user/feedback/project/reference). These nodes represent hand-curated memory with no workflow origin and no open/resolved lifecycle - severity stays unset, status is reinterpreted as active/superseded/stale. Regression guards cover the exact pre-fix failure: reloading a saved graph containing one of these types raised `ValueError: '<value>' is not a valid NodeType` in MemoryGraph._load() -> Node.from_dict() -> NodeType(data["type"]). No schema change - existing fields are reinterpreted, not added.
1 parent 06468f9 commit ad02188

5 files changed

Lines changed: 92 additions & 5 deletions

File tree

src/attune/memory/graph.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,14 @@ def _generate_id(self, finding: dict[str, Any]) -> str:
154154
def add_finding(self, workflow: str, finding: dict[str, Any]) -> str:
155155
"""Add a finding from any workflow, return node ID.
156156
157+
Also accepts curated cross-session memory (no workflow origin) via
158+
the ``USER_CONTEXT``/``FEEDBACK``/``PROJECT_CONTEXT``/``REFERENCE``
159+
node types - pass ``workflow=""`` for those (see
160+
``attune.memory.nodes`` module docstring).
161+
157162
Args:
158-
workflow: Name of the workflow adding this finding
163+
workflow: Name of the workflow adding this finding, or "" for
164+
a curated-memory node with no workflow origin.
159165
finding: Dict with at least 'type' and 'name' keys
160166
161167
Returns:

src/attune/memory/nodes.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""Memory Graph Node Types
22
3-
Defines node types for the cross-workflow knowledge graph.
4-
Each node represents an entity discovered by a workflow.
3+
Defines node types for the cross-workflow knowledge graph. Most nodes
4+
represent an entity discovered by a workflow (a bug, a fix, a test).
5+
The "Curated memory" category (below) is the exception: those nodes
6+
represent hand-curated cross-session memory - decisions, standing
7+
feedback, project context - that has no workflow origin and no
8+
resolution lifecycle. ``source_workflow`` stays empty and ``severity``
9+
stays unset for curated-memory nodes; ``status`` is reinterpreted as
10+
active/superseded/stale rather than open/investigating/resolved/wontfix
11+
(same field, no schema change).
512
613
Copyright 2025 Smart AI Memory, LLC
714
Licensed under the Apache License, Version 2.0
@@ -47,6 +54,12 @@ class NodeType(Enum):
4754
DEPENDENCY = "dependency"
4855
LICENSE = "license"
4956

57+
# Curated memory (not workflow findings - see module docstring)
58+
USER_CONTEXT = "user_context"
59+
FEEDBACK = "feedback"
60+
PROJECT_CONTEXT = "project_context"
61+
REFERENCE = "reference"
62+
5063

5164
@dataclass
5265
class Node:

tests/memory/test_graph.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ def test_all_node_types_count(self):
5656
"""Test total number of node types."""
5757
# FILE, FUNCTION, CLASS, MODULE, BUG, VULNERABILITY, PERFORMANCE_ISSUE,
5858
# CODE_SMELL, TECH_DEBT, PATTERN, FIX, REFACTOR, TEST, TEST_CASE,
59-
# COVERAGE_GAP, DOC, API_ENDPOINT, DEPENDENCY, LICENSE
60-
assert len(NodeType) == 19
59+
# COVERAGE_GAP, DOC, API_ENDPOINT, DEPENDENCY, LICENSE,
60+
# USER_CONTEXT, FEEDBACK, PROJECT_CONTEXT, REFERENCE
61+
assert len(NodeType) == 23
6162

6263
def test_node_type_from_string(self):
6364
"""Test creating NodeType from string."""

tests/unit/memory/test_graph.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,38 @@ def test_handles_corrupt_json(self, temp_graph_path):
121121
graph = MemoryGraph(path=temp_graph_path)
122122
assert len(graph.nodes) == 0
123123

124+
def test_loads_curated_memory_node_via_real_add_finding(self, temp_graph_path):
125+
"""Regression guard: before NodeType.FEEDBACK/USER_CONTEXT/
126+
PROJECT_CONTEXT/REFERENCE existed, reloading a graph containing one
127+
of these types raised
128+
``ValueError: 'feedback' is not a valid NodeType`` inside
129+
MemoryGraph._load() -> Node.from_dict() -> NodeType(data["type"]).
130+
Exercises the real public API (add_finding, not manual Node
131+
construction) end to end: add, save, reload in a fresh instance,
132+
traverse via find_related."""
133+
graph1 = MemoryGraph(path=temp_graph_path)
134+
feedback_id = graph1.add_finding(
135+
workflow="",
136+
finding={
137+
"type": "feedback",
138+
"name": "Standing commitments to Patrick",
139+
"status": "active",
140+
},
141+
)
142+
project_id = graph1.add_finding(
143+
workflow="",
144+
finding={"type": "project_context", "name": "Memory subsystem motivation"},
145+
)
146+
graph1.add_edge(project_id, feedback_id, EdgeType.RELATED_TO)
147+
148+
# A fresh instance must reload without raising.
149+
graph2 = MemoryGraph(path=temp_graph_path)
150+
151+
assert graph2.nodes[feedback_id].type == NodeType.FEEDBACK
152+
assert graph2.nodes[project_id].type == NodeType.PROJECT_CONTEXT
153+
related = graph2.find_related(project_id, edge_types=[EdgeType.RELATED_TO])
154+
assert [n.id for n in related] == [feedback_id]
155+
124156

125157
# =============================================================================
126158
# NODE OPERATIONS

tests/unit/memory/test_nodes_coverage_boost.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ def test_node_type_enum_has_dependency_types(self):
6363
assert NodeType.DEPENDENCY.value == "dependency"
6464
assert NodeType.LICENSE.value == "license"
6565

66+
def test_node_type_enum_has_curated_memory_types(self):
67+
"""Test that NodeType enum includes curated cross-session memory
68+
types (not workflow findings - see module docstring)."""
69+
assert NodeType.USER_CONTEXT.value == "user_context"
70+
assert NodeType.FEEDBACK.value == "feedback"
71+
assert NodeType.PROJECT_CONTEXT.value == "project_context"
72+
assert NodeType.REFERENCE.value == "reference"
73+
6674
def test_node_type_can_be_created_from_string_value(self):
6775
"""Test that NodeType can be created from string value."""
6876
node_type = NodeType("bug")
@@ -351,6 +359,33 @@ def test_roundtrip_serialization(self):
351359
assert restored.updated_at == original.updated_at
352360
assert restored.status == original.status
353361

362+
def test_curated_memory_node_roundtrip_serialization(self):
363+
"""Regression guard: before the curated-memory NodeType members
364+
existed, Node.from_dict(node.to_dict()) on a FEEDBACK/USER_CONTEXT/
365+
PROJECT_CONTEXT/REFERENCE node raised
366+
``ValueError: '<value>' is not a valid NodeType`` at reload time
367+
(from_dict calls NodeType(data["type"]) against whatever the enum
368+
actually contains). severity stays unset and status is
369+
reinterpreted (active, not open/resolved) for these node types -
370+
see the module docstring."""
371+
original = Node(
372+
id="feedback_my_commitments_to_patrick",
373+
type=NodeType.FEEDBACK,
374+
name="Standing commitments to Patrick",
375+
description="Neutral curiosity, full attention, correction without ego.",
376+
source_file="/memory/feedback_my_commitments_to_patrick.md",
377+
severity="",
378+
status="active",
379+
tags=["relationship", "standing-commitment"],
380+
)
381+
382+
restored = Node.from_dict(original.to_dict())
383+
384+
assert restored.type == NodeType.FEEDBACK
385+
assert restored.severity == ""
386+
assert restored.status == "active"
387+
assert restored.source_workflow == ""
388+
354389

355390
@pytest.mark.unit
356391
class TestBugNode:

0 commit comments

Comments
 (0)