From 4886190ca3fa061f8d142b2107661781b94544ef Mon Sep 17 00:00:00 2001 From: Adam Cuculich <46691282+cucupac@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:25:53 -0700 Subject: [PATCH 1/2] fix: preserve memory relationships through recall --- .../retrieval/deterministic_graph_recall.py | 46 +++++++++- app/core/use_cases/retrieval/read/execute.py | 1 + .../repos/relational/read_policy_repo.py | 6 ++ .../host_apps/inner_agents/prompt.py | 6 ++ .../test_context_pack_json_shape.py | 2 + .../test_deterministic_graph_recall.py | 86 +++++++++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) diff --git a/app/core/use_cases/retrieval/deterministic_graph_recall.py b/app/core/use_cases/retrieval/deterministic_graph_recall.py index ddf2995..bf31f23 100644 --- a/app/core/use_cases/retrieval/deterministic_graph_recall.py +++ b/app/core/use_cases/retrieval/deterministic_graph_recall.py @@ -125,10 +125,12 @@ def build_deterministic_graph_pack( ) ) + memory_relations: dict[tuple[str, str, str], dict[str, Any]] = {} structural_trace = _expand_structural_memory_relations( request=request, memory_candidates=memory_candidates, uow=uow, + memory_relations=memory_relations, ) concept_candidates = _discover_concepts( request=request, @@ -153,6 +155,12 @@ def build_deterministic_graph_pack( request=request, memory_candidates=memory_candidates, ) + selected_ids = {str(item["memory"].id) for item in selected_memories} + relations_payload = [ + relation + for (subject, _, target), relation in sorted(memory_relations.items()) + if subject in selected_ids and target in selected_ids + ] concepts_payload = [ _compact_concept_payload(entry["bundle"], why_selected=entry["why"]) for entry in selected_concepts @@ -172,6 +180,7 @@ def build_deterministic_graph_pack( for lane in lanes ], "memories": [_memory_payload(item) for item in selected_memories], + "memory_relations": relations_payload, "concepts": concepts_payload, "relation_neighbors": neighbor_payload, "anchors": anchors[:16], @@ -186,6 +195,7 @@ def build_deterministic_graph_pack( "pack_composition": _pack_composition(selected_memories, anchors), "pack_budget": _pack_budget( selected_memories=selected_memories, + memory_relations=relations_payload, concepts=concepts_payload, neighbors=neighbor_payload, ranking_trace=ranking_trace, @@ -254,7 +264,7 @@ def deterministic_brief_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]: link_roles={"failed_tactic_for", "warns_about"}, ) known_traps.extend(_claim_texts(concept_items, {"failure_mode"})) - prior_cases = _brief_memory_texts( + prior_cases = _brief_case_texts(pack) + _brief_memory_texts( memories, kinds={"solution"}, link_roles={"solution_for", "example_of"}, @@ -458,6 +468,7 @@ def _expand_structural_memory_relations( request: MemoryReadRequest, memory_candidates: dict[str, dict[str, Any]], uow: IUnitOfWork, + memory_relations: dict[tuple[str, str, str], dict[str, Any]], ) -> dict[str, Any]: """Add canonical structural memory-relation neighbors to recall candidates.""" @@ -476,6 +487,16 @@ def _expand_structural_memory_relations( kinds=request.kinds or list(MATURE_MEMORY_KIND_VALUES), predicates=predicates, ) + for row in rows: + key = (row["subject_memory_id"], row["predicate"], row["object_memory_id"]) + memory_relations[key] = { + "subject_memory_id": key[0], + "predicate": key[1], + "object_memory_id": key[2], + "status": row["status"], + "confidence": row["confidence"], + "validated_at": _iso(row["validated_at"]), + } for neighbor in select_structural_memory_relation_neighbors( rows, anchor_memory_id=anchor_memory_id ): @@ -1127,12 +1148,14 @@ def _pack_composition( def _pack_budget( *, selected_memories: Sequence[dict[str, Any]], + memory_relations: Sequence[dict[str, Any]], concepts: Sequence[dict[str, Any]], neighbors: Sequence[dict[str, Any]], ranking_trace: dict[str, Any], ) -> dict[str, Any]: payload = { "memories": [_memory_payload(item) for item in selected_memories], + "memory_relations": memory_relations, "concepts": concepts, "relation_neighbors": neighbors, } @@ -1155,6 +1178,7 @@ def synthesis_pack_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]: "strategy": pack.get("strategy"), "request": pack.get("request") if isinstance(pack.get("request"), dict) else {}, "memories": _dict_items(pack.get("memories")), + "memory_relations": _dict_items(pack.get("memory_relations")), "concepts": _dict_items(pack.get("concepts")), "relation_neighbors": _dict_items(pack.get("relation_neighbors")), "anchors": _dict_items(pack.get("anchors")), @@ -1187,6 +1211,26 @@ def _brief_memory_texts( return rendered +def _brief_case_texts(pack: dict[str, Any]) -> list[str]: + """Keep each recorded outcome attached to its selected source memory.""" + + memories = {item["id"]: item for item in pack["memories"]} + cases = [] + for relation in pack.get("memory_relations", []): + subject = memories[relation["subject_memory_id"]] + target = memories[relation["object_memory_id"]] + qualifiers = [relation["status"]] + for name in ("confidence", "validated_at"): + if relation[name] is not None: + qualifiers.append(f"{name}={relation[name]}") + cases.append( + f"{subject['kind']} ({subject['currentness']}): {_truncate(subject['text'], 300)} " + f"--{relation['predicate']} [{'; '.join(qualifiers)}]--> " + f"{target['kind']} ({target['currentness']}): {_truncate(target['text'], 300)}" + ) + return cases + + def _claim_texts( concepts: Sequence[dict[str, Any]], claim_types: set[str] ) -> list[str]: diff --git a/app/core/use_cases/retrieval/read/execute.py b/app/core/use_cases/retrieval/read/execute.py index e2bee43..faa38af 100644 --- a/app/core/use_cases/retrieval/read/execute.py +++ b/app/core/use_cases/retrieval/read/execute.py @@ -57,6 +57,7 @@ def execute_read_memory( "items": selected["concepts"], }, "relation_neighbors": selected["relation_neighbors"], + "memory_relations": selected.get("memory_relations", []), "conflicts": selected["conflicts"], } ) diff --git a/app/infrastructure/db/runtime/repos/relational/read_policy_repo.py b/app/infrastructure/db/runtime/repos/relational/read_policy_repo.py index def4545..65c12d6 100644 --- a/app/infrastructure/db/runtime/repos/relational/read_policy_repo.py +++ b/app/infrastructure/db/runtime/repos/relational/read_policy_repo.py @@ -38,6 +38,9 @@ def list_structural_memory_relation_rows( structural_memory_relations.c.subject_memory_id, structural_memory_relations.c.predicate, structural_memory_relations.c.object_memory_id, + structural_memory_relations.c.status, + structural_memory_relations.c.confidence, + structural_memory_relations.c.validated_at, ).where( structural_memory_relations.c.repo_id == repo_id, structural_memory_relations.c.predicate.in_(list(predicates)), @@ -60,6 +63,9 @@ def list_structural_memory_relation_rows( "subject_memory_id": str(row["subject_memory_id"]), "predicate": str(row["predicate"]), "object_memory_id": str(row["object_memory_id"]), + "status": str(row["status"]), + "confidence": row["confidence"], + "validated_at": row["validated_at"], "visible_memory_ids": tuple( sorted( self._visible_memory_ids( diff --git a/app/infrastructure/host_apps/inner_agents/prompt.py b/app/infrastructure/host_apps/inner_agents/prompt.py index 7c3c0f0..78de9f5 100644 --- a/app/infrastructure/host_apps/inner_agents/prompt.py +++ b/app/infrastructure/host_apps/inner_agents/prompt.py @@ -33,6 +33,12 @@ - `preference` records user or team guidance. - `change` revises or replaces older knowledge. +`memory_relations` connect specific records by subject, predicate, and object. +`solved_by` and `failed_with` bind an outcome to its problem; keep that pairing and its stated conditions together. +`superseded_by` and `explained_by_change` bind old guidance to its replacement or explanation. +Preserve link direction and lifecycle qualifications when describing a case. +Do not transfer a solution or failure to another problem because their text is similar. + Concept claims give orientation: - `definition` and `behavior` explain a concept. - Active, relevant `invariant` and `usage_note` claims become constraints. diff --git a/tests/operations/read/execution/context_pack/test_context_pack_json_shape.py b/tests/operations/read/execution/context_pack/test_context_pack_json_shape.py index 9f0e901..42d7417 100644 --- a/tests/operations/read/execution/context_pack/test_context_pack_json_shape.py +++ b/tests/operations/read/execution/context_pack/test_context_pack_json_shape.py @@ -17,6 +17,7 @@ def test_read_preserves_grouped_evidence_and_display_order(monkeypatch): "implicit_related", "concepts", "relation_neighbors", + "memory_relations", "conflicts", ] assert pack["meta"] == { @@ -51,3 +52,4 @@ def test_empty_read_keeps_evidence_envelope(monkeypatch): ).data["pack"] assert sum(pack["meta"]["counts"].values()) == 0 assert pack["concepts"] == {"mode": "none", "items": []} + assert pack["memory_relations"] == [] diff --git a/tests/operations/recall/execution/test_deterministic_graph_recall.py b/tests/operations/recall/execution/test_deterministic_graph_recall.py index 291482f..6d61d89 100644 --- a/tests/operations/recall/execution/test_deterministic_graph_recall.py +++ b/tests/operations/recall/execution/test_deterministic_graph_recall.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import replace +from datetime import datetime, timezone import pytest @@ -31,7 +32,9 @@ _select_concepts, _select_final_memories, build_deterministic_graph_pack, + deterministic_brief_from_graph_pack, source_items_from_graph_pack, + synthesis_pack_from_graph_pack, ) from app.core.use_cases.retrieval.read.request import MemoryReadRequest from app.core.use_cases.retrieval.recall.request import MemoryRecallRequest @@ -144,6 +147,86 @@ def test_graph_pack_expands_canonical_structural_memory_relations() -> None: assert sources_by_id["mem-change"]["input_section"] == "explicit_related" +@pytest.mark.parametrize("seed_both_endpoints", [False, True]) +@pytest.mark.parametrize("excluded", [None, "hidden", "budget"]) +def test_recall_preserves_case_pairings(monkeypatch, seed_both_endpoints, excluded): + uow = _FakeUow() + uow.concepts = _NoFakeConcepts() + uow.concept_semantic_retrieval = _NoFakeConceptSemanticRetrieval() + template = uow.memories._memories["mem-direct"] + uow.memories._memories = { + key: replace(template, id=MemoryId(key), kind=MemoryKind(kind), text=text) + for key, kind, text in ( + ("p1", "problem", "Writes time out while a lock is held."), + ("p2", "problem", "Writes time out while the disk is full."), + ("s1", "solution", "Release the lock."), + ("s2", "solution", "Free disk space."), + ("f1", "failed_tactic", "Increasing the timeout failed."), + ) + } + if excluded == "hidden": + uow.memories._memories["s2"] = replace( + uow.memories._memories["s2"], repo_id=RepoId("other-repo") + ) + relations = [ + { + "subject_memory_id": subject, + "predicate": predicate, + "object_memory_id": target, + "status": status, + "confidence": 0.8, + "validated_at": datetime(2026, 9, 1, tzinfo=timezone.utc), + } + for subject, predicate, target, status in ( + ("p1", "solved_by", "s1", "active"), + ("p2", "solved_by", "s2", "maybe_stale"), + ("p1", "failed_with", "f1", "active"), + ) + ] + monkeypatch.setattr( + uow.read_policy, + "list_structural_memory_relation_rows", + lambda **kw: [ + { + **row, + "visible_memory_ids": (row["subject_memory_id"], row["object_memory_id"]), + } + for row in relations + if kw["anchor_memory_id"] in (row["subject_memory_id"], row["object_memory_id"]) + and row["predicate"] in kw["predicates"] + ], + ) + seeds = list(uow.memories._memories) if seed_both_endpoints else ["s1", "s2", "f1"] + monkeypatch.setattr( + uow.semantic_retrieval, + "query_semantic", + lambda **kw: [{"memory_id": key, "score": 0.92} for key in seeds], + ) + request = _request(query="prior writes timeout") + if excluded == "budget": + request = request.model_copy(update={"limit": 1}) + pack = build_deterministic_graph_pack(request=request, uow=uow) + expected = [ + {**row, "validated_at": row["validated_at"].isoformat()} + for row in relations + if excluded != "budget" and (excluded != "hidden" or row["object_memory_id"] != "s2") + ] + assert sorted(pack["memory_relations"], key=str) == sorted(expected, key=str) + synthesis = synthesis_pack_from_graph_pack(pack) + read = execute_read_memory(request, uow).data["pack"] + assert synthesis["memory_relations"] == read["memory_relations"] == pack["memory_relations"] + cases = deterministic_brief_from_graph_pack(pack)["prior_cases"] + for row in expected: + subject = uow.memories._memories[row["subject_memory_id"]].text + target = uow.memories._memories[row["object_memory_id"]].text + assert any( + subject in case and target in case and row["predicate"] in case + and row["status"] in case and row["validated_at"] in case + for case in cases + ) + assert not any("lock is held" in case and "Free disk" in case for case in cases) + + def _request(*, query: str) -> MemoryReadRequest: return MemoryReadRequest.model_validate( { @@ -415,6 +498,9 @@ def list_structural_memory_relation_rows(self, **kwargs): "subject_memory_id": "mem-direct", "predicate": "explained_by_change", "object_memory_id": "mem-change", + "status": "active", + "confidence": None, + "validated_at": None, "visible_memory_ids": ("mem-direct", "mem-change"), } ] From 61d5ce1f24a2e307a9770ebff8bea51ec32e9712 Mon Sep 17 00:00:00 2001 From: Adam Cuculich <46691282+cucupac@users.noreply.github.com> Date: Mon, 21 Sep 2026 22:05:26 -0700 Subject: [PATCH 2/2] fix: preserve case priority without duplicate recall entries --- .../retrieval/deterministic_graph_recall.py | 58 +++++++++---------- app/core/use_cases/retrieval/read/execute.py | 2 +- .../operations/_shared/read_pipeline_stubs.py | 1 + .../recall/execution/test_build_context.py | 2 + .../test_deterministic_graph_recall.py | 29 ++++++++++ .../test_recall_summary_record_writes.py | 2 + 6 files changed, 64 insertions(+), 30 deletions(-) diff --git a/app/core/use_cases/retrieval/deterministic_graph_recall.py b/app/core/use_cases/retrieval/deterministic_graph_recall.py index bf31f23..88c54b6 100644 --- a/app/core/use_cases/retrieval/deterministic_graph_recall.py +++ b/app/core/use_cases/retrieval/deterministic_graph_recall.py @@ -264,11 +264,7 @@ def deterministic_brief_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]: link_roles={"failed_tactic_for", "warns_about"}, ) known_traps.extend(_claim_texts(concept_items, {"failure_mode"})) - prior_cases = _brief_case_texts(pack) + _brief_memory_texts( - memories, - kinds={"solution"}, - link_roles={"solution_for", "example_of"}, - ) + prior_cases = _brief_case_texts(memories, pack["memory_relations"]) return { "summary": _summary(memories=memories, concepts=concept_items), "constraints": _truncate_list(constraints, 6), @@ -489,14 +485,8 @@ def _expand_structural_memory_relations( ) for row in rows: key = (row["subject_memory_id"], row["predicate"], row["object_memory_id"]) - memory_relations[key] = { - "subject_memory_id": key[0], - "predicate": key[1], - "object_memory_id": key[2], - "status": row["status"], - "confidence": row["confidence"], - "validated_at": _iso(row["validated_at"]), - } + memory_relations[key] = row | {"validated_at": _iso(row["validated_at"])} + del memory_relations[key]["visible_memory_ids"] for neighbor in select_structural_memory_relation_neighbors( rows, anchor_memory_id=anchor_memory_id ): @@ -1178,7 +1168,7 @@ def synthesis_pack_from_graph_pack(pack: dict[str, Any]) -> dict[str, Any]: "strategy": pack.get("strategy"), "request": pack.get("request") if isinstance(pack.get("request"), dict) else {}, "memories": _dict_items(pack.get("memories")), - "memory_relations": _dict_items(pack.get("memory_relations")), + "memory_relations": pack["memory_relations"], "concepts": _dict_items(pack.get("concepts")), "relation_neighbors": _dict_items(pack.get("relation_neighbors")), "anchors": _dict_items(pack.get("anchors")), @@ -1211,23 +1201,33 @@ def _brief_memory_texts( return rendered -def _brief_case_texts(pack: dict[str, Any]) -> list[str]: - """Keep each recorded outcome attached to its selected source memory.""" +def _brief_case_texts( + memories: Sequence[dict[str, Any]], relations: Sequence[dict[str, Any]] +) -> list[str]: + """Render cases in selected memory order, keeping outcomes with their sources.""" - memories = {item["id"]: item for item in pack["memories"]} + memories_by_id = {item["id"]: item for item in memories} + relations_by_target = defaultdict(list) + for relation in relations: + relations_by_target[relation["object_memory_id"]].append(relation) cases = [] - for relation in pack.get("memory_relations", []): - subject = memories[relation["subject_memory_id"]] - target = memories[relation["object_memory_id"]] - qualifiers = [relation["status"]] - for name in ("confidence", "validated_at"): - if relation[name] is not None: - qualifiers.append(f"{name}={relation[name]}") - cases.append( - f"{subject['kind']} ({subject['currentness']}): {_truncate(subject['text'], 300)} " - f"--{relation['predicate']} [{'; '.join(qualifiers)}]--> " - f"{target['kind']} ({target['currentness']}): {_truncate(target['text'], 300)}" - ) + for target in memories: + linked = relations_by_target[target["id"]] + if not linked: + cases.extend(_brief_memory_texts( + [target], kinds={"solution"}, link_roles={"solution_for", "example_of"} + )) + for relation in linked: + subject = memories_by_id[relation["subject_memory_id"]] + qualifiers = [relation["status"]] + for name in ("confidence", "validated_at"): + if relation[name] is not None: + qualifiers.append(f"{name}={relation[name]}") + cases.append( + f"{subject['kind']} ({subject['currentness']}): {_truncate(subject['text'], 300)} " + f"--{relation['predicate']} [{'; '.join(qualifiers)}]--> " + f"{target['kind']} ({target['currentness']}): {_truncate(target['text'], 300)}" + ) return cases diff --git a/app/core/use_cases/retrieval/read/execute.py b/app/core/use_cases/retrieval/read/execute.py index faa38af..50ef82e 100644 --- a/app/core/use_cases/retrieval/read/execute.py +++ b/app/core/use_cases/retrieval/read/execute.py @@ -57,7 +57,7 @@ def execute_read_memory( "items": selected["concepts"], }, "relation_neighbors": selected["relation_neighbors"], - "memory_relations": selected.get("memory_relations", []), + "memory_relations": selected["memory_relations"], "conflicts": selected["conflicts"], } ) diff --git a/tests/operations/_shared/read_pipeline_stubs.py b/tests/operations/_shared/read_pipeline_stubs.py index 97aec93..d9fc9d6 100644 --- a/tests/operations/_shared/read_pipeline_stubs.py +++ b/tests/operations/_shared/read_pipeline_stubs.py @@ -39,6 +39,7 @@ def stub_read_pipeline(monkeypatch: pytest.MonkeyPatch, *, zero_results: bool) - "app.core.use_cases.retrieval.read.execute.build_deterministic_graph_pack", lambda **kwargs: { "memories": memories, + "memory_relations": [], "concepts": [], "relation_neighbors": [], "conflicts": [], diff --git a/tests/operations/recall/execution/test_build_context.py b/tests/operations/recall/execution/test_build_context.py index ec5b902..a59855e 100644 --- a/tests/operations/recall/execution/test_build_context.py +++ b/tests/operations/recall/execution/test_build_context.py @@ -279,6 +279,7 @@ def _graph_pack() -> dict: return { "strategy": "deterministic_graph", "request": {"query": "migration timeout"}, + "memory_relations": [], "query_lanes": [{"lane": "original", "query": "migration timeout"}], "memories": [ { @@ -336,6 +337,7 @@ def _empty_graph_pack() -> dict: return { "strategy": "deterministic_graph", "request": {"query": "nothing"}, + "memory_relations": [], "query_lanes": [], "memories": [], "concepts": [], diff --git a/tests/operations/recall/execution/test_deterministic_graph_recall.py b/tests/operations/recall/execution/test_deterministic_graph_recall.py index 6d61d89..f1bfce3 100644 --- a/tests/operations/recall/execution/test_deterministic_graph_recall.py +++ b/tests/operations/recall/execution/test_deterministic_graph_recall.py @@ -238,6 +238,35 @@ def _request(*, query: str) -> MemoryReadRequest: ) +@pytest.mark.parametrize("linked_solutions", [(0, 1, 2), (5,)]) +def test_fast_brief_preserves_case_order_without_duplicate_solutions(linked_solutions): + memories = [ + {"id": f"s{i}", "kind": "solution", "text": f"Fix {i}.", "currentness": "current"} + for i in range(6) + ] + [ + {"id": f"p{i}", "kind": "problem", "text": f"Problem {i}.", "currentness": "current"} + for i in linked_solutions + ] + relations = [ + {"subject_memory_id": f"p{i}", "predicate": "solved_by", "object_memory_id": f"s{i}", + "status": "active", "confidence": None, "validated_at": None} + for i in reversed(linked_solutions) + ] + cases = deterministic_brief_from_graph_pack( + {"memories": memories, "memory_relations": relations} + )["prior_cases"] + assert len(cases) == 6 + for i, case in enumerate(cases): + assert f"Fix {i}." in case + if i in linked_solutions: + assert f"Problem {i}." in case and "solved_by" in case + + +def test_synthesis_rejects_missing_memory_relations(): + with pytest.raises(KeyError, match="memory_relations"): + synthesis_pack_from_graph_pack({"memories": []}) + + class _FakeVectorSearch: model_name = "fake-embedding" diff --git a/tests/operations/telemetry/execution/recall_summaries/test_recall_summary_record_writes.py b/tests/operations/telemetry/execution/recall_summaries/test_recall_summary_record_writes.py index 23ae594..0512d00 100644 --- a/tests/operations/telemetry/execution/recall_summaries/test_recall_summary_record_writes.py +++ b/tests/operations/telemetry/execution/recall_summaries/test_recall_summary_record_writes.py @@ -350,6 +350,7 @@ def _candidate_pack() -> dict: return { "strategy": "deterministic_graph", "request": {"query": "recall telemetry"}, + "memory_relations": [], "query_lanes": [{"lane": "original", "query": "recall telemetry"}], "memories": [ { @@ -402,6 +403,7 @@ def _empty_pack() -> dict: "request": {"query": "nothing matches"}, "query_lanes": [], "memories": [], + "memory_relations": [], "concepts": [], "relation_neighbors": [], "anchors": [],