From 0b3db9afa8fc25cc7d26203b290c2287832f66ec Mon Sep 17 00:00:00 2001 From: jbergner Date: Wed, 26 Aug 2026 22:59:36 +0200 Subject: [PATCH] 1.4.4 --- MANIFEST.sha256 | 28 +- README.md | 2 +- RELEASE-NOTES-v1.4.4.md | 48 ++ VERSION | 2 +- docs/VALIDATION.md | 19 +- mega-project.json | 2 +- patches/SHA256SUMS | 2 + patches/v1.4.3-to-v1.4.4.diff | 422 ++++++++++++++++++ .../neuroforge/internal/httpapi/index.html | 2 +- .../neuroforge/internal/store/knowledge.go | 56 ++- .../internal/store/knowledge_test.go | 27 ++ platform/neuroforge/internal/store/segment.go | 8 +- .../neuroforge/internal/store/segment_test.go | 48 ++ .../neuroforge/internal/store/source_index.go | 4 +- platform/neuroforge/internal/store/store.go | 16 +- platform/neuroforge/internal/store/v3.go | 13 +- 16 files changed, 653 insertions(+), 46 deletions(-) create mode 100644 RELEASE-NOTES-v1.4.4.md create mode 100644 patches/v1.4.3-to-v1.4.4.diff diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index df3f9f8..70aae07 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -3,7 +3,7 @@ ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag.yml e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore 9c18555764b03bdb004098bf6b8ca7f3eaebdccb8c17f15a11d2cebbab28cbd3 ./Makefile -4ce9d3f1bd0c4c95a3783d38fb69e1a6f302c88af3c71eea4980c6f722cce163 ./README.md +bcb3b35b556a4f1435351d03c56839352bbd0df1104888a1b5078e5241d19fde ./README.md 4858caa52c0fb6cf302a1c581d07d448e5e90e0daa797e5819610fd6223bd348 ./RELEASE-NOTES-v1.1.0.md 01163462f46314f57660677fdef407c6c2884412ea850aab13a4f650e8c29f50 ./RELEASE-NOTES-v1.2.0.md 4da388ce660aa3b7a0d8075ec066a025b5437960973397360fcb9a5d4cb58c96 ./RELEASE-NOTES-v1.3.0.md @@ -11,7 +11,8 @@ e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore 0723a0151b72d492899a5c5409e1fef37de1fe4c458c3053f2f7eccd4b6e70c5 ./RELEASE-NOTES-v1.4.1.md 58923d3292f11487526b8dc4d6c0992025c0f946df8435fde5c91ef4581d46fd ./RELEASE-NOTES-v1.4.2.md 548f3168f8f29add4f510c4c8048a74465f311dbc3ba2436e5b8db5cc898c552 ./RELEASE-NOTES-v1.4.3.md -bd31e8d9cf8d14a1e9e3fdf3b9f6a524819aed0d1b14627523c6765767c7d8f1 ./VERSION +61ceebe5a891388336795fda2fd0d1ad10373b4ebaae7d2e670f35766115c604 ./RELEASE-NOTES-v1.4.4.md +d03d8b268b4171328ac2458ce8ca82d69a1fde38469d4e4faae4f8fa8d8a5e35 ./VERSION e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./backups/.gitkeep 8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deploy/searxng/settings.yml f95897d6e96afbbcc230f56ae1968a046f76cf24ec78d9c18aa2b22bd8335e34 ./docker-compose.yml @@ -33,7 +34,7 @@ a71d28353529906cee08cf90f35e0b0cf97bbc94ccfcef830145a2d561b21a9c ./docs/MIGRATI 69ea49bc76690ac489aea908f4e784a14293e92d62478a240eabea53c5f90820 ./docs/QUALITY-REPLAY-example.json bd2d3c43a09a89fefd0e844433064d7d7384a414179a754c6de94232d738fb04 ./docs/QUALITY-REPLAY.md be749a09ddbcd4cf427316c6fe531f138e9bfaa6c104f4c11a08f7555231e8b2 ./docs/UNIFIED-GRAPH.md -25396a0a80f232d6073530e1ef3222e565efd0a24e2c66ea0866285d03332d70 ./docs/VALIDATION.md +2cb377f06c50a642d968bbc3fe978ea429d08483c53ed68bd6f1d675808e933b ./docs/VALIDATION.md be4810451750abb676eee0edcc6f164d86f372f32b185f6f730bf84538cef4a8 ./exports/knowledge-obsidian-snapshot.zip dfa65e55e9ccf642ae5ef8ef91c62f4220b31ce4304ea4e1061f2e7d8c1b0fb4 ./exports/knowledge-obsidian-snapshot.zip.sha256 99ffd5f497239a5e17b4e4bc79c7a1b5e1e41fdaf9b3f07beb7179050bbddcc4 ./go.work @@ -140,8 +141,8 @@ cb3cde795233c0d4842dc7451c5855d1c071e459efbde3be0cabac32a837ba1b ./knowledge/14 7f1d67faf4a6cea0c41c84d7b275d8a8979b19420b52db4925d8e2cd71ead3e0 ./knowledge/16_office-aktivierung-und-lizenzierung.json f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./knowledge/17_serienbriefe-und-dokumentfunktionen.json 5a0b3d5d5bc712e30a67a4de3432f69070f7e43dd99931f675e72c213006b363 ./knowledge/example-vpn.json -e94bbbfe2769b3d30c69bdf4239b030d23423249cb3fa7f5de3171656cd3a908 ./mega-project.json -c02d6d702401343e6d7002c3db117935964e2abd18db966b604871e59715ee05 ./patches/SHA256SUMS +b662742d1787a5616835f8dadc9f4306b2ba25c778816333421d86c5aa82488a ./mega-project.json +fde0c9a8fdcced3c4fc35519c560e9a3e45f6f5237a70bb91495311b869381b5 ./patches/SHA256SUMS 47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 ./patches/glpi-agent-mega.diff 9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 ./patches/glpi-knowledge-mega.diff f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 ./patches/neuroforge-mega.diff @@ -151,6 +152,7 @@ f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 ./patches/neur 654de4aac7cf318680872897c3de192afbafb61c7169acb8e7514116df86efde ./patches/v1.4.0-envfix-to-v1.4.1.diff 1ebc306bd6870389500c86cf07dcd677b5bd0448c762812c7e1c0f8732f36eb1 ./patches/v1.4.1-to-v1.4.2.diff 651c883facddfd0316f89e3cf3c24fd913371fcec2a5f447b8d105fa5dec463e ./patches/v1.4.2-to-v1.4.3.diff +d90be604acf6ecf5d821890d5864307853caf0e0678a3249eeaa1d29748a6ada ./patches/v1.4.3-to-v1.4.4.diff 564817f8edabde0c4e4a1a427a3aa5418aae7bf12e9463044a7e6e0f13973657 ./platform/neuroforge/.env.example 39319b6f2058e4c8d6656a9cf01675374f81a04075b956b093a2befb5e05ada4 ./platform/neuroforge/.gitignore 189486a885c7fb78e0eb878d93cda0c70ca6d7ff9bfdfb3f5f32487cf03a9688 ./platform/neuroforge/BENCHMARK-v0.5.0.md @@ -207,7 +209,7 @@ edfedb67b1f55b2fa8a4a4d29b809b53083b36752e8dc087fa8603f4ec26154e ./platform/neu b9bb2934e01ed2bfb6b16e4139e10e51f1387112eb8654f7f692779a2fd2d273 ./platform/neuroforge/internal/cost/cost_test.go fb66ce4ab760b979eacd4f7f17dedc41916f93c53ba582be572d250f99654695 ./platform/neuroforge/internal/httpapi/admin_app_auth_test.go 4b8555feb594fb71aac1c868eca00cc952eb4e998fbea10b442389f7d5e8160b ./platform/neuroforge/internal/httpapi/httpapi.go -b6df45ac405afddcd68d21a7f1cd7a5a2b4e2379d350eafa2589de6b1dc2540c ./platform/neuroforge/internal/httpapi/index.html +94d2594d1186418858c06e2a7c1c5ba18b666e4fc87f7ee8fcbb21f28cd30c93 ./platform/neuroforge/internal/httpapi/index.html 20f4a6cc30d5ce84ceed4fbdfc9f5c57ab5f82274e0c54464817d571e0e881b0 ./platform/neuroforge/internal/httpapi/integration.go 7f6e4767dfcb948570e05baadca6917f27a406fe57ef82eeb63e3b3979ec94b6 ./platform/neuroforge/internal/httpapi/integration_api_test.go f8cfcc7a2bc781394231c25e36e75c41563f5250a1387e5687880d2591efd93b ./platform/neuroforge/internal/httpapi/integration_graph.go @@ -243,8 +245,8 @@ e890897c662b070997c3b18134bafbee5bf9b3fa5220045ac09dc6e3dfd4ac41 ./platform/neu 1858ed8045b16282de815a6668ff6027c7a3a3795e38659c753c504d0f2a8383 ./platform/neuroforge/internal/store/diskann_test.go d49925fda3cbe3ceeffde3369073bce5a5bc67a23defb6827e27cd4a7f4c59f5 ./platform/neuroforge/internal/store/index_segments.go 478efaeb260cb5e970af8b3bb99f232ef846f54c6e4ff9deb5499df6e008e1c1 ./platform/neuroforge/internal/store/index_segments_test.go -bf39c642b88d28ee03c7bcd7103b46cf3da2f1f70a1105618753229ad7159165 ./platform/neuroforge/internal/store/knowledge.go -0689a337688ce9ab8a1c2713b6ad9f2213e4846d17b2baee3ed9117009d94dc1 ./platform/neuroforge/internal/store/knowledge_test.go +bea84ff9bd7812249ada186b97be58766a37d319f4d986252a0fd43e399a3dc2 ./platform/neuroforge/internal/store/knowledge.go +e9aaedbf4d5e9a43f4a4a5dbe2e9e0f0257b952fb59e82bf729aa5e761955c1e ./platform/neuroforge/internal/store/knowledge_test.go 43de0f541fb47633b02b25686544d44887c17b47e8d01cb23f8e57567cebdcd7 ./platform/neuroforge/internal/store/mmap_linux.go 9ce033d691157037fc6df719b5ecd8fb29d6d881d1862e2d50dc0c6c573b5f66 ./platform/neuroforge/internal/store/mmap_linux_test.go ef6135a9039fb45361e57107d3968272b4b365ac833e9e2e58b0875566bf5a07 ./platform/neuroforge/internal/store/mmap_other.go @@ -253,15 +255,15 @@ fe98dca9f919cb52140a916de08e76b88bd0d08b6b83fc8b4e5220411998b0fc ./platform/neu 5df97ac71adcea7627756a52d295d682900d3b812a977dbe9a4911e4c4118809 ./platform/neuroforge/internal/store/raftlog.go fd6769bdcf1f7d22ced9ea426fd4dbc52b50b5483b4be42ac3b6adf637c926ab ./platform/neuroforge/internal/store/raftstate.go f8d1a8c913a10ab2a658416bee7e94c2a08d5805e7329e459c3fac3055f57e19 ./platform/neuroforge/internal/store/research_runs.go -6a40b93c094b69af94e608d0dab777551d58091d07e1c48a8b6d1c5d77557fa5 ./platform/neuroforge/internal/store/segment.go -ebd58daf6dab70d87c7591903355d65716b323157c39454d8c34a4d9dd0733dc ./platform/neuroforge/internal/store/segment_test.go -18f4f033ec9c65cef88a235905bfcb69d89ff3d0a2c27e70d2aa3c8b78bea553 ./platform/neuroforge/internal/store/source_index.go +c5c976d068317c688611bce153453133ebd7cf19b835955c77115b450793fe9b ./platform/neuroforge/internal/store/segment.go +ea968456538367ed536c212707209a173098ec65823152a0813422bc8071c952 ./platform/neuroforge/internal/store/segment_test.go +1d8071889b7a2be9faf029a1dd2981c69a9914798869bfe217f6db105340323e ./platform/neuroforge/internal/store/source_index.go 4d41586c15bfdd13139e19bfa102613f18ddc6120e4214a1f45dd9e2eaa195af ./platform/neuroforge/internal/store/sources.go 0ff464ef7fab01324d77ee4e406a8c15451b3333b75d4afa444be3c3c94f016b ./platform/neuroforge/internal/store/sources_test.go 61cd82b106e74f2aa0a7591d757a0872491a635dc1fccc78c66d41c052d0c9d2 ./platform/neuroforge/internal/store/sqar_vector.go -ae157a764f4970c6d8c5935b6370f682de79cee6c751e476b936aaa05dbb59f7 ./platform/neuroforge/internal/store/store.go +4faa443230f87326449d80b731ce27a6590256dbdd2f1209e8faaab8fce20610 ./platform/neuroforge/internal/store/store.go f86b648e8b4845ab83e672ae3fcfb0bcb0dc1bf891abf4bd30d380304f0b259a ./platform/neuroforge/internal/store/tiering.go -2fac412008f68b4fe98b2a433a0bd669ba108ce4c894c8bc81151664a5c318e6 ./platform/neuroforge/internal/store/v3.go +55f8e410258a37b4da1175753342017b94481a77142f9015a8b1f9a99b34d36a ./platform/neuroforge/internal/store/v3.go 091e4c9cacfc76786a76f6afc4eb2b697e5a641e611475b6dab97e99371360ea ./platform/neuroforge/internal/store/v3_test.go 7f0bca5419a895258b7921e940c3fd69cce04dedcb0b9d8f1127cd24b9c9e201 ./platform/neuroforge/internal/store/v5_test.go b3a0115907719a8a7aa9e6b244eec55be71a2535ba20db65907987d5fb78dc77 ./platform/neuroforge/internal/store/vector_journal.go diff --git a/README.md b/README.md index aef1427..5f6f3ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GLPI NeuroForge Mega v1.4.0 -> Release: **v1.4.3** · Dashboard-Status-Kompatibilitätsfix für korrekte Memory-/Synapsen-/Revision-Anzeige. +> Release: **v1.4.4** · Legacy-Memory-ID-Reparatur und robuster Wissensraum-Graph für segmentbasierte Bestandsdaten. Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. diff --git a/RELEASE-NOTES-v1.4.4.md b/RELEASE-NOTES-v1.4.4.md new file mode 100644 index 0000000..775c996 --- /dev/null +++ b/RELEASE-NOTES-v1.4.4.md @@ -0,0 +1,48 @@ +# GLPI NeuroForge Mega v1.4.4 + +## Wissensraum / Legacy-Segment-ID Fix + +Diese Patch-Version behebt einen Bestandsdatenfehler, der sich als **leerer Wissensraum trotz vorhandener semantischer Memories** zeigt. + +### Symptom + +- Übersicht/Knowledge-Summary meldet vorhandene semantische Memories. +- Research lernt weiter und die Store-Revision steigt. +- Der Tab **Wissensraum** zeigt dennoch `0 Knoten`. +- Bei betroffenen Altbeständen können zusätzlich HNSW-/Detail-/Retention-Pfade inkonsistent mit dem Memory-Katalog arbeiten. + +### Ursache + +NeuroForge verwendet in segmentbasierten Bestandsdaten zwei Identitätsträger: + +1. den kanonischen Schlüssel bzw. `segmentRecord.ID`, und +2. die eingebettete `Memory.ID` im Segment-Payload. + +Aktuelle Writer setzen beide Werte. Historische/legacy Segment-Payloads können jedoch eine gültige äußere ID besitzen, während `Memory.ID` leer ist. Die Summary zählt den Katalogeintrag korrekt, der bisherige Knowledge-Graph verwendete beim Seed-Aufbau aber `Memory.ID`. Dadurch wurde ein leerer Seed (`""`) gewählt und kein Knoten aufgelöst. + +Das Fehlerbild wurde mit einem historischen Segment-Fixture auf v1.4.3 reproduziert. + +### Lösung + +- Der **Katalog-/Segment-Schlüssel ist kanonisch** und repariert beim Öffnen eine fehlende oder abweichende eingebettete `Memory.ID`. +- `SegmentStore.Get()` stellt die kanonische ID auch beim Hydratisieren alter Bodies wieder her. +- Knowledge-Graph, Knowledge-Listen, Provenance-Lookups, Child-/Supersession-Navigation, Retention und Hot-HNSW verwenden robuste kanonische IDs. +- Der Knowledge-Graph liefert zusätzlich: + - `total_memories` + - `total_synapses` + - `truncated` +- Das WebUI zeigt dadurch beispielsweise `64 / 163 Knoten · 0 Kanten` statt den Graph-Sample mit dem Gesamtbestand zu verwechseln. +- Solange noch **keine Synapsen** existieren, zeigt der Wissensraum einen gebundenen Start-Sample von bis zu 64 isolierten Memories. Sobald Synapsen entstehen, übernimmt wieder die graphbasierte Expansion. + +### Datenmigration + +Keine manuelle Datenmigration ist erforderlich. Die Identitätsnormalisierung erfolgt beim Öffnen des Stores. Bestehende Segmentdateien werden nicht destruktiv umgeschrieben. + +### Validierung + +- Legacy-Segment mit leerer eingebetteter `Memory.ID`: vorher Graph leer, nach Fix Graph + Retrieval korrekt. +- `go test ./...`, `go vet ./...`, `go build ./...` für alle vier Module: OK. +- NeuroForge Store/HTTP API Race-Checks: OK. +- NeuroForge WebUI JavaScript Syntaxcheck: OK. + +Nach dem Update genügt ein Neustart des NeuroForge-Containers. diff --git a/VERSION b/VERSION index 428b770..1c99cf0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.3 +1.4.4 diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index 1e17961..b1702b6 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -1,13 +1,13 @@ # Validierung -Stand: 26.08.2026 — Release v1.4.0 +Stand: 26.08.2026 — Release v1.4.4 ## Umfang - 4 Go-Module im gemeinsamen `go.work` -- 158 Go-Dateien -- 47.260 Go-Codezeilen inklusive Tests -- 276 `Test...`-Testfunktionen +- 159 Go-Dateien +- 47.489 Go-Codezeilen inklusive Tests +- 281 `Test...`-Testfunktionen - 103 produktive Knowledge-JSON-Dateien - 8 Compose-Services inklusive optionalem SearXNG-Profil - reproduzierbarer Engineering-Snapshot: 1.652 Knoten / 6.450 Kanten @@ -70,3 +70,14 @@ Docker/Podman sind in der Prüfungsumgebung nicht installiert. Deshalb wurden ni - historischer Quality-Replay mit echten Betreiber-Tickets Vor Produktivfreigabe bleiben Container-Smoke-Test, echte GLPI-/Research-Konnektivität und der historische Quality-Replay Betreiber-Gates. + +## v1.4.4-spezifische Prüfungen + +- Historischer Segmentdatensatz mit kanonischer `segmentRecord.ID`, aber leerer eingebetteter `Memory.ID`: **reproduziert auf v1.4.3, behoben auf v1.4.4** +- Legacy-ID wird beim Segment-Scan, beim vollständigen Segment-Read und beim Store-Migrationspfad aus dem kanonischen Katalogschlüssel wiederhergestellt: **OK** +- Derselbe Legacy-Memory erscheint anschließend wieder im Knowledge Graph und in der Vektorsuche: **OK** +- Knowledge-Graph liefert `total_memories`, `total_synapses` und `truncated` für transparente Sampling-Anzeige: **OK** +- Ohne Synapsen zeigt der Graph einen gebundenen Start-Sample von bis zu 64 Memories statt leer zu wirken: **OK** +- NeuroForge `go test ./...`, `go vet ./...`, `go build ./...`: **OK** +- NeuroForge `go test -race ./internal/store ./internal/httpapi`: **OK** +- eingebettetes NeuroForge-JavaScript `node --check`: **OK** diff --git a/mega-project.json b/mega-project.json index 1a449fb..14fbfd7 100644 --- a/mega-project.json +++ b/mega-project.json @@ -28,7 +28,7 @@ "schema": "Wiki/Schema.md", "glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI" }, - "version": "1.4.3", + "version": "1.4.4", "controlled_learning": { "raw_chat_auto_learning": false, "validated_outcomes": [ diff --git a/patches/SHA256SUMS b/patches/SHA256SUMS index 2f1095f..26f8198 100644 --- a/patches/SHA256SUMS +++ b/patches/SHA256SUMS @@ -6,3 +6,5 @@ f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 patches/neurof 163a8a990aacd986bacb385494c2b2062ac10a9a8f0a456d19a6d1746c2fc500 patches/v1.3.0-to-v1.4.0.diff 654de4aac7cf318680872897c3de192afbafb61c7169acb8e7514116df86efde patches/v1.4.0-envfix-to-v1.4.1.diff 1ebc306bd6870389500c86cf07dcd677b5bd0448c762812c7e1c0f8732f36eb1 patches/v1.4.1-to-v1.4.2.diff +651c883facddfd0316f89e3cf3c24fd913371fcec2a5f447b8d105fa5dec463e patches/v1.4.2-to-v1.4.3.diff +d90be604acf6ecf5d821890d5864307853caf0e0678a3249eeaa1d29748a6ada patches/v1.4.3-to-v1.4.4.diff diff --git a/patches/v1.4.3-to-v1.4.4.diff b/patches/v1.4.3-to-v1.4.4.diff new file mode 100644 index 0000000..d73cd36 --- /dev/null +++ b/patches/v1.4.3-to-v1.4.4.diff @@ -0,0 +1,422 @@ +diff --git a/README.md b/README.md +index aef1427..5f6f3ae 100644 +--- a/README.md ++++ b/README.md +@@ -1,6 +1,6 @@ + # GLPI NeuroForge Mega v1.4.0 + +-> Release: **v1.4.3** · Dashboard-Status-Kompatibilitätsfix für korrekte Memory-/Synapsen-/Revision-Anzeige. ++> Release: **v1.4.4** · Legacy-Memory-ID-Reparatur und robuster Wissensraum-Graph für segmentbasierte Bestandsdaten. + + Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. + +diff --git a/VERSION b/VERSION +index 428b770..1c99cf0 100644 +--- a/VERSION ++++ b/VERSION +@@ -1 +1 @@ +-1.4.3 ++1.4.4 +diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md +index 1e17961..b1702b6 100644 +--- a/docs/VALIDATION.md ++++ b/docs/VALIDATION.md +@@ -1,13 +1,13 @@ + # Validierung + +-Stand: 26.08.2026 — Release v1.4.0 ++Stand: 26.08.2026 — Release v1.4.4 + + ## Umfang + + - 4 Go-Module im gemeinsamen `go.work` +-- 158 Go-Dateien +-- 47.260 Go-Codezeilen inklusive Tests +-- 276 `Test...`-Testfunktionen ++- 159 Go-Dateien ++- 47.489 Go-Codezeilen inklusive Tests ++- 281 `Test...`-Testfunktionen + - 103 produktive Knowledge-JSON-Dateien + - 8 Compose-Services inklusive optionalem SearXNG-Profil + - reproduzierbarer Engineering-Snapshot: 1.652 Knoten / 6.450 Kanten +@@ -70,3 +70,14 @@ Docker/Podman sind in der Prüfungsumgebung nicht installiert. Deshalb wurden ni + - historischer Quality-Replay mit echten Betreiber-Tickets + + Vor Produktivfreigabe bleiben Container-Smoke-Test, echte GLPI-/Research-Konnektivität und der historische Quality-Replay Betreiber-Gates. ++ ++## v1.4.4-spezifische Prüfungen ++ ++- Historischer Segmentdatensatz mit kanonischer `segmentRecord.ID`, aber leerer eingebetteter `Memory.ID`: **reproduziert auf v1.4.3, behoben auf v1.4.4** ++- Legacy-ID wird beim Segment-Scan, beim vollständigen Segment-Read und beim Store-Migrationspfad aus dem kanonischen Katalogschlüssel wiederhergestellt: **OK** ++- Derselbe Legacy-Memory erscheint anschließend wieder im Knowledge Graph und in der Vektorsuche: **OK** ++- Knowledge-Graph liefert `total_memories`, `total_synapses` und `truncated` für transparente Sampling-Anzeige: **OK** ++- Ohne Synapsen zeigt der Graph einen gebundenen Start-Sample von bis zu 64 Memories statt leer zu wirken: **OK** ++- NeuroForge `go test ./...`, `go vet ./...`, `go build ./...`: **OK** ++- NeuroForge `go test -race ./internal/store ./internal/httpapi`: **OK** ++- eingebettetes NeuroForge-JavaScript `node --check`: **OK** +diff --git a/mega-project.json b/mega-project.json +index 1a449fb..14fbfd7 100644 +--- a/mega-project.json ++++ b/mega-project.json +@@ -28,7 +28,7 @@ + "schema": "Wiki/Schema.md", + "glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI" + }, +- "version": "1.4.3", ++ "version": "1.4.4", + "controlled_learning": { + "raw_chat_auto_learning": false, + "validated_outcomes": [ +diff --git a/platform/neuroforge/internal/httpapi/index.html b/platform/neuroforge/internal/httpapi/index.html +index 50e566d..a0a2b3f 100644 +--- a/platform/neuroforge/internal/httpapi/index.html ++++ b/platform/neuroforge/internal/httpapi/index.html +@@ -172,7 +172,7 @@ function lod(){return cam.z<.62?0:cam.z<1.45?1:2}function drawGraph(){let r=canv + function drawClusters(){let groups={};for(let p of graphPos.values()){let t=p.n.memory_type||'other';(groups[t]??=[]).push(p)}for(let[t,xs]of Object.entries(groups)){let x=xs.reduce((a,p)=>a+p.x,0)/xs.length,y=xs.reduce((a,p)=>a+p.y,0)/xs.length,s=worldToScreen({x,y}),rad=Math.min(58,20+Math.sqrt(xs.length)*6);ctx.fillStyle=(typeColors[t]||'#bd8cff')+'44';ctx.strokeStyle=typeColors[t]||'#bd8cff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(s.x,s.y,rad,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#eef3fb';ctx.font='600 13px system-ui';ctx.textAlign='center';ctx.fillText(t,s.x,s.y-2);ctx.fillStyle='#a9b7cb';ctx.font='11px system-ui';ctx.fillText(xs.length+' nodes',s.x,s.y+15)}ctx.textAlign='left'} + function fitGraph(){if(!graphPos.size)return;let xs=[...graphPos.values()].map(p=>p.x),ys=[...graphPos.values()].map(p=>p.y),w=Math.max(...xs)-Math.min(...xs)+140,h=Math.max(...ys)-Math.min(...ys)+140,r=canvas.getBoundingClientRect();cam.z=Math.max(.25,Math.min(1.15,Math.min(r.width/w,r.height/h)));cam.x=cam.y=0;drawGraph()} + canvas.addEventListener('wheel',e=>{e.preventDefault();let before=screenToWorld(e.offsetX,e.offsetY),factor=Math.exp(-e.deltaY*.0012);cam.z=Math.max(.2,Math.min(5,cam.z*factor));let after=worldToScreen(before);cam.x+=e.offsetX-after.x;cam.y+=e.offsetY-after.y;drawGraph()},{passive:false});canvas.addEventListener('pointerdown',e=>{pointer.down=true;pointer.x=e.clientX;pointer.y=e.clientY;pointer.moved=false;canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!pointer.down)return;let dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;if(Math.hypot(dx,dy)>2)pointer.moved=true;cam.x+=dx;cam.y+=dy;pointer.x=e.clientX;pointer.y=e.clientY;drawGraph()});canvas.addEventListener('pointerup',e=>{pointer.down=false;if(pointer.moved)return;let rect=canvas.getBoundingClientRect(),x=e.clientX-rect.left,y=e.clientY-rect.top;if(lod()===0){cam.z=.8;drawGraph();return}let best=null,bd=15;for(let p of graphPos.values()){let s=worldToScreen(p),d=Math.hypot(s.x-x,s.y-y);if(d{cam.z=Math.min(5,cam.z*1.3);drawGraph()});$('zoomOutBtn').addEventListener('click',()=>{cam.z=Math.max(.2,cam.z/1.3);drawGraph()});$('fitBtn').addEventListener('click',fitGraph); +-async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'¢er='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};$('graphCount').textContent=fmtN(graphData.nodes.length)+' Knoten · '+fmtN(graphData.edges.length)+' Kanten';layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory)); ++async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'¢er='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};let tn=Number(d?.total_memories??graphData.nodes.length),te=Number(d?.total_synapses??graphData.edges.length),ns=fmtN(graphData.nodes.length)+(tn>graphData.nodes.length?' / '+fmtN(tn):''),es=fmtN(graphData.edges.length)+(te>graphData.edges.length?' / '+fmtN(te):'');$('graphCount').textContent=ns+' Knoten · '+es+' Kanten';if(tn>0&&graphData.nodes.length===0)toast('Memory-Katalog enthält '+fmtN(tn)+' Einträge, aber der Graph konnte keine kanonischen IDs auflösen.',true);layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory)); + $('knowledgeSearchBtn').addEventListener('click',searchKnowledge);$('knowledgeQuery').addEventListener('keydown',e=>{if(e.key==='Enter')searchKnowledge()});async function searchKnowledge(){let q=$('knowledgeQuery').value.trim();if(!q)return;try{let d=await req('/admin/api/knowledge/search',{method:'POST',body:JSON.stringify({text:q,k:16})});$('knowledgeHits').innerHTML=(d.hits||[]).map(h=>`
${esc(h.memory.text.slice(0,120))}${Number(h.score||0).toFixed(3)}
${esc(h.candidate_source||'')}sim ${Number(h.similarity||0).toFixed(3)}base ${Number(h.base_score||0).toFixed(3)}graph +${Number(h.graph_boost||0).toFixed(3)}
`).join('')||'

Keine Treffer.

';$$('#knowledgeHits [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));if(d.hits?.[0]){selectedMemory=d.hits[0].memory.id;await loadKnowledgeGraph(selectedMemory);await openMemory(selectedMemory,false)}}catch(e){toast(e.message,true)}} + async function openMemory(id,recenter=true){selectedMemory=id;try{let d=await req('/admin/api/knowledge/memory/'+encodeURIComponent(id)),m=d.memory||{},p=m.provenance||{};$('knowledgeInspector').innerHTML=`
${esc(m.memory_type)}${esc(m.kind)}${esc(m.status||'active')}

${esc(m.truth_key||p.source_title||m.id)}

${esc(m.text)}

Qualität

Confidence
${Number(m.confidence||0).toFixed(3)}
Salience
${Number(m.salience||0).toFixed(3)}
Reward
${Number(m.reward||0).toFixed(3)}
Evidenzquellen
${fmtN(m.evidence_count||m.evidence_source_ids?.length||0)}
Aufrufe
${fmtN(m.access_count)}

Provenance

Quelle
${esc(p.source||'legacy/unknown')}
Source-ID
${esc(p.source_id||'–')}
Titel
${esc(p.source_title||'–')}
URI
${p.source_uri?`${esc(p.source_uri)}`:'–'}
Chunk
${p.chunk_index||'–'} / ${p.chunk_count||'–'}
Embedding
${esc([p.embedding_model,p.embedding_node_id].filter(Boolean).join(' · ')||'–')}
Generierung
${esc([p.generation_model,p.generation_node_id].filter(Boolean).join(' · ')||'–')}

Stärkste Synapsen

${(d.edges||[]).slice(0,12).map(e=>`
${esc(e.a===id?e.b:e.a)}w ${Number(e.weight).toFixed(2)}
`).join('')||'

Keine Synapsen.

'}

Timeline

${(d.events||[]).map(e=>`
${esc(e.type)}

${esc(e.summary)}

${new Date(e.created_at).toLocaleString()}
`).join('')||'

Keine Events.

'}`;$$('#knowledgeInspector [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));drawGraph();if(recenter)loadKnowledgeGraph(id)}catch(e){toast(e.message,true)}} + +diff --git a/platform/neuroforge/internal/store/knowledge.go b/platform/neuroforge/internal/store/knowledge.go +index 817175a..c6e4f34 100644 +--- a/platform/neuroforge/internal/store/knowledge.go ++++ b/platform/neuroforge/internal/store/knowledge.go +@@ -68,9 +68,12 @@ type KnowledgeEdge struct { + } + + type KnowledgeGraph struct { +- Center string `json:"center,omitempty"` +- Nodes []MemoryPreview `json:"nodes"` +- Edges []KnowledgeEdge `json:"edges"` ++ Center string `json:"center,omitempty"` ++ Nodes []MemoryPreview `json:"nodes"` ++ Edges []KnowledgeEdge `json:"edges"` ++ TotalMemories int `json:"total_memories"` ++ TotalSynapses int `json:"total_synapses"` ++ Truncated bool `json:"truncated"` + } + + type KnowledgeMemoryDetail struct { +@@ -216,7 +219,10 @@ func (s *Store) KnowledgeMemories(limit int, before time.Time, memoryType, statu + s.mu.RLock() + h := &previewHeap{} + heap.Init(h) +- for _, meta := range s.state.Memories { ++ for id, meta := range s.state.Memories { ++ if meta == nil { ++ continue ++ } + if !before.IsZero() && !meta.CreatedAt.Before(before) { + continue + } +@@ -237,6 +243,7 @@ func (s *Store) KnowledgeMemories(limit int, before time.Time, memoryType, statu + continue + } + p := memoryPreview(*meta) ++ p.ID = id + if h.Len() < limit { + heap.Push(h, p) + } else if p.CreatedAt.After((*h)[0].CreatedAt) { +@@ -285,16 +292,28 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap + selected[center] = true + frontier = []string{center} + } else { +- // Keep only a tiny top-K seed set; never allocate one preview per memory. ++ // Start from a bounded top-K seed set. With no synapses yet, show a ++ // broader sample so a freshly ingested knowledge space is still useful ++ // and visibly populated before associative links have formed. ++ seedLimit := 12 ++ if len(s.state.Synapses) == 0 { ++ seedLimit = 64 ++ } ++ if seedLimit > maxNodes { ++ seedLimit = maxNodes ++ } + type seedItem struct { + id string + score float64 + created time.Time + } +- seeds := make([]seedItem, 0, 12) +- for _, m := range s.state.Memories { +- x := seedItem{id: m.ID, score: m.Salience + float64(m.AccessCount)*0.01, created: m.CreatedAt} +- if len(seeds) < 12 { ++ seeds := make([]seedItem, 0, seedLimit) ++ for id, m := range s.state.Memories { ++ if m == nil { ++ continue ++ } ++ x := seedItem{id: id, score: m.Salience + float64(m.AccessCount)*0.01, created: m.CreatedAt} ++ if len(seeds) < seedLimit { + seeds = append(seeds, x) + continue + } +@@ -338,7 +357,10 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap + } + frontier = next + } +- out := KnowledgeGraph{Center: center, Nodes: make([]MemoryPreview, 0), Edges: make([]KnowledgeEdge, 0)} ++ out := KnowledgeGraph{ ++ Center: center, Nodes: make([]MemoryPreview, 0), Edges: make([]KnowledgeEdge, 0), ++ TotalMemories: len(s.state.Memories), TotalSynapses: len(s.state.Synapses), ++ } + for id := range selected { + if m, ok := s.fullMemoryForReadLocked(id); ok { + out.Nodes = append(out.Nodes, memoryPreview(m)) +@@ -353,6 +375,7 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap + if len(out.Edges) > maxNodes*4 { + out.Edges = out.Edges[:maxNodes*4] + } ++ out.Truncated = len(out.Nodes) < out.TotalMemories || len(out.Edges) < out.TotalSynapses + return out + } + +@@ -401,9 +424,9 @@ func (s *Store) KnowledgeMemoryDetail(id string) (KnowledgeMemoryDetail, bool) { + return nil + } + out.Parent = previewByID(m.ParentID) +- for _, mm := range s.state.Memories { +- if mm.ParentID == id { +- if full, ok := s.fullMemoryForReadLocked(mm.ID); ok { ++ for childID, mm := range s.state.Memories { ++ if mm != nil && mm.ParentID == id { ++ if full, ok := s.fullMemoryForReadLocked(childID); ok { + out.Children = append(out.Children, memoryPreview(full)) + } + } +@@ -419,10 +442,13 @@ func (s *Store) KnowledgeMemoryDetail(id string) (KnowledgeMemoryDetail, bool) { + out.Supersedes = append(out.Supersedes, *p) + } + } +- for _, mm := range s.state.Memories { ++ for successorID, mm := range s.state.Memories { ++ if mm == nil { ++ continue ++ } + for _, x := range mm.Supersedes { + if x == id { +- if full, ok := s.fullMemoryForReadLocked(mm.ID); ok { ++ if full, ok := s.fullMemoryForReadLocked(successorID); ok { + out.SupersededBy = append(out.SupersededBy, memoryPreview(full)) + } + } +diff --git a/platform/neuroforge/internal/store/knowledge_test.go b/platform/neuroforge/internal/store/knowledge_test.go +index 2ce942e..e5d6174 100644 +--- a/platform/neuroforge/internal/store/knowledge_test.go ++++ b/platform/neuroforge/internal/store/knowledge_test.go +@@ -75,4 +75,31 @@ func TestKnowledgeGraphEmptyUsesEmptySlices(t *testing.T) { + if len(g.Nodes) != 0 || len(g.Edges) != 0 { + t.Fatalf("unexpected graph contents: nodes=%d edges=%d", len(g.Nodes), len(g.Edges)) + } ++ if g.TotalMemories != 0 || g.TotalSynapses != 0 || g.Truncated { ++ t.Fatalf("unexpected empty graph totals: %+v", g) ++ } ++} ++ ++func TestKnowledgeGraphShowsBoundedOrphansBeforeSynapsesExist(t *testing.T) { ++ s, err := New(t.TempDir()) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer s.Close() ++ for i := 0; i < 100; i++ { ++ m := &core.Memory{ID: NewID("orphan"), Kind: "evidence", MemoryType: core.MemorySemantic, Text: "isolated evidence", Vector: []float32{1, float32(i + 1)}, Salience: float64(i) / 100, Confidence: .7} ++ if err := s.AddMemory(m); err != nil { ++ t.Fatal(err) ++ } ++ } ++ g := s.KnowledgeGraph("", 3, 600) ++ if g.TotalMemories != 100 || g.TotalSynapses != 0 { ++ t.Fatalf("unexpected totals: %+v", g) ++ } ++ if len(g.Nodes) != 64 { ++ t.Fatalf("nodes=%d want bounded orphan sample of 64", len(g.Nodes)) ++ } ++ if !g.Truncated { ++ t.Fatal("graph should report sampling/truncation") ++ } + } +diff --git a/platform/neuroforge/internal/store/segment.go b/platform/neuroforge/internal/store/segment.go +index e05590e..6fdaf60 100644 +--- a/platform/neuroforge/internal/store/segment.go ++++ b/platform/neuroforge/internal/store/segment.go +@@ -184,6 +184,9 @@ func (s *SegmentStore) scanFile(path string) error { + delete(s.scanMetadata, rec.ID) + } else if rec.Memory != nil { + m := cloneMemory(*rec.Memory) ++ // Historical segment records may have stored the canonical ID only ++ // in segmentRecord.ID. Restore the invariant Memory.ID == record ID. ++ m.ID = rec.ID + if m.VectorDim == 0 && len(m.Vector) > 0 { + m.VectorDim = len(m.Vector) + } +@@ -353,7 +356,10 @@ func (s *SegmentStore) Get(id string) (core.Memory, bool, bool, error) { + if rec.Memory == nil { + return core.Memory{}, true, false, errors.New("segment upsert has no memory body") + } +- return cloneMemory(*rec.Memory), true, false, nil ++ m := cloneMemory(*rec.Memory) ++ // The lookup key comes from the outer segment record and is authoritative. ++ m.ID = id ++ return m, true, false, nil + } + + type liveSegmentCursor struct { +diff --git a/platform/neuroforge/internal/store/segment_test.go b/platform/neuroforge/internal/store/segment_test.go +index 1fe248e..4a99b8c 100644 +--- a/platform/neuroforge/internal/store/segment_test.go ++++ b/platform/neuroforge/internal/store/segment_test.go +@@ -134,3 +134,51 @@ func TestIterateLiveVectorsSequentialUsesLatestSearchableRecords(t *testing.T) { + t.Fatal("deleted memory must not be encoded") + } + } ++ ++func TestLegacySegmentRecordRestoresCanonicalMemoryIDAndKnowledgeGraph(t *testing.T) { ++ dir := t.TempDir() ++ ss, err := openSegmentStore(filepath.Join(dir, "memory-segments"), 1<<20, false) ++ if err != nil { ++ t.Fatal(err) ++ } ++ // Historical compatibility fixture: the outer segment record owns the ID, ++ // while the embedded memory body has no ID. Older persisted data can have ++ // this shape even though current writers always populate both fields. ++ legacy := core.Memory{ ++ Kind: "evidence", ++ MemoryType: core.MemorySemantic, ++ Text: "legacy segment evidence", ++ Vector: []float32{1, 0}, ++ Salience: 1, ++ Confidence: .8, ++ } ++ if err := ss.appendRecord(segmentRecord{Revision: 1, Op: "upsert", ID: "legacy-id", Memory: &legacy}); err != nil { ++ t.Fatal(err) ++ } ++ ss.Close() ++ ++ s, err := New(dir) ++ if err != nil { ++ t.Fatal(err) ++ } ++ defer s.Close() ++ ++ if meta := s.state.Memories["legacy-id"]; meta == nil || meta.ID != "legacy-id" { ++ t.Fatalf("catalog identity was not repaired: %#v", meta) ++ } ++ got, ok := s.GetMemory("legacy-id") ++ if !ok || got.ID != "legacy-id" || got.Text != legacy.Text { ++ t.Fatalf("hydrated memory identity/body mismatch: ok=%v memory=%#v", ok, got) ++ } ++ g := s.KnowledgeGraph("", 3, 600) ++ if len(g.Nodes) != 1 || g.Nodes[0].ID != "legacy-id" { ++ t.Fatalf("legacy memory missing from knowledge graph: %#v", g.Nodes) ++ } ++ if g.TotalMemories != 1 || g.TotalSynapses != 0 || g.Truncated { ++ t.Fatalf("unexpected graph totals: %+v", g) ++ } ++ hits := s.SearchVector([]float32{1, 0}, 4, .1, 0) ++ if len(hits) == 0 || hits[0].Memory.ID != "legacy-id" { ++ t.Fatalf("legacy memory missing from vector retrieval: %#v", hits) ++ } ++} +diff --git a/platform/neuroforge/internal/store/source_index.go b/platform/neuroforge/internal/store/source_index.go +index 293d171..9ef6131 100644 +--- a/platform/neuroforge/internal/store/source_index.go ++++ b/platform/neuroforge/internal/store/source_index.go +@@ -57,11 +57,11 @@ func (s *Store) MemoryByProvenanceSourceID(sourceID string) (MemoryLookup, bool) + if sourceID == "" { + return MemoryLookup{}, false + } +- for _, meta := range s.state.Memories { ++ for id, meta := range s.state.Memories { + if meta == nil || strings.TrimSpace(meta.Provenance.SourceID) != sourceID { + continue + } +- m, ok := s.fullMemoryForReadLocked(meta.ID) ++ m, ok := s.fullMemoryForReadLocked(id) + if ok { + return MemoryLookup{Memory: cloneMemory(m)}, true + } +diff --git a/platform/neuroforge/internal/store/store.go b/platform/neuroforge/internal/store/store.go +index 5c16531..ad9d791 100644 +--- a/platform/neuroforge/internal/store/store.go ++++ b/platform/neuroforge/internal/store/store.go +@@ -481,7 +481,17 @@ func applyNewDefaults(c *core.Config) { + } + + func migrateMemories(memories map[string]*core.Memory, localShard string) { +- for _, m := range memories { ++ for id, m := range memories { ++ if m == nil { ++ continue ++ } ++ // The catalog map key is the canonical memory identity. Older segment ++ // records could carry the ID only in the outer segment record while the ++ // embedded Memory.ID was empty. Repair that legacy representation on ++ // every open so graph/index/retention code sees one stable identity. ++ if m.ID != id { ++ m.ID = id ++ } + if m.MemoryType == "" { + m.MemoryType = inferMemoryType(m.Kind) + } +@@ -711,7 +721,7 @@ func (s *Store) rebuildHotIndexesLocked() { + continue + } + dim := len(m.Vector) +- batches[dim] = append(batches[dim], vector.HNSWItem{ID: m.ID, Vector: m.Vector}) ++ batches[dim] = append(batches[dim], vector.HNSWItem{ID: c.id, Vector: m.Vector}) + } + for dim, items := range batches { + idx := s.newIndexLocked() +@@ -864,6 +874,8 @@ func (s *Store) fullMemoryForReadLocked(id string) (core.Memory, bool) { + if err != nil || !found || deleted { + return cloneMemory(*meta), true + } ++ // Segment record ID / catalog key is authoritative for legacy records. ++ m.ID = id + if s.pageCache != nil { + s.pageCache.Put(m) + } +diff --git a/platform/neuroforge/internal/store/v3.go b/platform/neuroforge/internal/store/v3.go +index a2651ef..f0e1cea 100644 +--- a/platform/neuroforge/internal/store/v3.go ++++ b/platform/neuroforge/internal/store/v3.go +@@ -29,7 +29,7 @@ func (s *Store) resolveConflictLocked(in *core.Memory) []core.Memory { + var changed []core.Memory + group := "conflict:" + strings.ToLower(key) + for id, meta := range s.state.Memories { +- if meta.ID == in.ID || !strings.EqualFold(strings.TrimSpace(meta.TruthKey), key) || meta.Status == core.MemoryArchived { ++ if id == in.ID || !strings.EqualFold(strings.TrimSpace(meta.TruthKey), key) || meta.Status == core.MemoryArchived { + continue + } + old, ok := s.materializeMemoryLocked(id) +@@ -359,19 +359,22 @@ func (s *Store) RunRetention(now time.Time) (RetentionResult, error) { + candidates := make([]retentionCandidate, 0, len(s.state.Memories)) + deleteSet := map[string]bool{} + changed := []core.Memory{} +- for _, m := range s.state.Memories { ++ for id, m := range s.state.Memories { ++ if m == nil { ++ continue ++ } + ageDays := now.Sub(m.CreatedAt).Hours() / 24 + if m.MemoryType == core.MemoryWorking && cfg.WorkingTTLHours > 0 && now.Sub(m.CreatedAt).Hours() >= cfg.WorkingTTLHours { +- deleteSet[m.ID] = true ++ deleteSet[id] = true + continue + } + utility := memoryUtility(m, now) +- candidates = append(candidates, retentionCandidate{id: m.ID, utility: utility, ageDays: ageDays}) ++ candidates = append(candidates, retentionCandidate{id: id, utility: utility, ageDays: ageDays}) + if ageDays < cfg.MinAgeDays || utility >= cfg.MinUtility || m.Status == core.MemoryArchived { + continue + } + if cfg.DeleteConsolidated && m.ConsolidatedInto != "" { +- deleteSet[m.ID] = true ++ deleteSet[id] = true + continue + } + full, ok := s.materializeMemoryLocked(m.ID) diff --git a/platform/neuroforge/internal/httpapi/index.html b/platform/neuroforge/internal/httpapi/index.html index 50e566d..a0a2b3f 100644 --- a/platform/neuroforge/internal/httpapi/index.html +++ b/platform/neuroforge/internal/httpapi/index.html @@ -172,7 +172,7 @@ function lod(){return cam.z<.62?0:cam.z<1.45?1:2}function drawGraph(){let r=canv function drawClusters(){let groups={};for(let p of graphPos.values()){let t=p.n.memory_type||'other';(groups[t]??=[]).push(p)}for(let[t,xs]of Object.entries(groups)){let x=xs.reduce((a,p)=>a+p.x,0)/xs.length,y=xs.reduce((a,p)=>a+p.y,0)/xs.length,s=worldToScreen({x,y}),rad=Math.min(58,20+Math.sqrt(xs.length)*6);ctx.fillStyle=(typeColors[t]||'#bd8cff')+'44';ctx.strokeStyle=typeColors[t]||'#bd8cff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(s.x,s.y,rad,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#eef3fb';ctx.font='600 13px system-ui';ctx.textAlign='center';ctx.fillText(t,s.x,s.y-2);ctx.fillStyle='#a9b7cb';ctx.font='11px system-ui';ctx.fillText(xs.length+' nodes',s.x,s.y+15)}ctx.textAlign='left'} function fitGraph(){if(!graphPos.size)return;let xs=[...graphPos.values()].map(p=>p.x),ys=[...graphPos.values()].map(p=>p.y),w=Math.max(...xs)-Math.min(...xs)+140,h=Math.max(...ys)-Math.min(...ys)+140,r=canvas.getBoundingClientRect();cam.z=Math.max(.25,Math.min(1.15,Math.min(r.width/w,r.height/h)));cam.x=cam.y=0;drawGraph()} canvas.addEventListener('wheel',e=>{e.preventDefault();let before=screenToWorld(e.offsetX,e.offsetY),factor=Math.exp(-e.deltaY*.0012);cam.z=Math.max(.2,Math.min(5,cam.z*factor));let after=worldToScreen(before);cam.x+=e.offsetX-after.x;cam.y+=e.offsetY-after.y;drawGraph()},{passive:false});canvas.addEventListener('pointerdown',e=>{pointer.down=true;pointer.x=e.clientX;pointer.y=e.clientY;pointer.moved=false;canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!pointer.down)return;let dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;if(Math.hypot(dx,dy)>2)pointer.moved=true;cam.x+=dx;cam.y+=dy;pointer.x=e.clientX;pointer.y=e.clientY;drawGraph()});canvas.addEventListener('pointerup',e=>{pointer.down=false;if(pointer.moved)return;let rect=canvas.getBoundingClientRect(),x=e.clientX-rect.left,y=e.clientY-rect.top;if(lod()===0){cam.z=.8;drawGraph();return}let best=null,bd=15;for(let p of graphPos.values()){let s=worldToScreen(p),d=Math.hypot(s.x-x,s.y-y);if(d{cam.z=Math.min(5,cam.z*1.3);drawGraph()});$('zoomOutBtn').addEventListener('click',()=>{cam.z=Math.max(.2,cam.z/1.3);drawGraph()});$('fitBtn').addEventListener('click',fitGraph); -async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'¢er='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};$('graphCount').textContent=fmtN(graphData.nodes.length)+' Knoten · '+fmtN(graphData.edges.length)+' Kanten';layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory)); +async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'¢er='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};let tn=Number(d?.total_memories??graphData.nodes.length),te=Number(d?.total_synapses??graphData.edges.length),ns=fmtN(graphData.nodes.length)+(tn>graphData.nodes.length?' / '+fmtN(tn):''),es=fmtN(graphData.edges.length)+(te>graphData.edges.length?' / '+fmtN(te):'');$('graphCount').textContent=ns+' Knoten · '+es+' Kanten';if(tn>0&&graphData.nodes.length===0)toast('Memory-Katalog enthält '+fmtN(tn)+' Einträge, aber der Graph konnte keine kanonischen IDs auflösen.',true);layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory)); $('knowledgeSearchBtn').addEventListener('click',searchKnowledge);$('knowledgeQuery').addEventListener('keydown',e=>{if(e.key==='Enter')searchKnowledge()});async function searchKnowledge(){let q=$('knowledgeQuery').value.trim();if(!q)return;try{let d=await req('/admin/api/knowledge/search',{method:'POST',body:JSON.stringify({text:q,k:16})});$('knowledgeHits').innerHTML=(d.hits||[]).map(h=>`
${esc(h.memory.text.slice(0,120))}${Number(h.score||0).toFixed(3)}
${esc(h.candidate_source||'')}sim ${Number(h.similarity||0).toFixed(3)}base ${Number(h.base_score||0).toFixed(3)}graph +${Number(h.graph_boost||0).toFixed(3)}
`).join('')||'

Keine Treffer.

';$$('#knowledgeHits [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));if(d.hits?.[0]){selectedMemory=d.hits[0].memory.id;await loadKnowledgeGraph(selectedMemory);await openMemory(selectedMemory,false)}}catch(e){toast(e.message,true)}} async function openMemory(id,recenter=true){selectedMemory=id;try{let d=await req('/admin/api/knowledge/memory/'+encodeURIComponent(id)),m=d.memory||{},p=m.provenance||{};$('knowledgeInspector').innerHTML=`
${esc(m.memory_type)}${esc(m.kind)}${esc(m.status||'active')}

${esc(m.truth_key||p.source_title||m.id)}

${esc(m.text)}

Qualität

Confidence
${Number(m.confidence||0).toFixed(3)}
Salience
${Number(m.salience||0).toFixed(3)}
Reward
${Number(m.reward||0).toFixed(3)}
Evidenzquellen
${fmtN(m.evidence_count||m.evidence_source_ids?.length||0)}
Aufrufe
${fmtN(m.access_count)}

Provenance

Quelle
${esc(p.source||'legacy/unknown')}
Source-ID
${esc(p.source_id||'–')}
Titel
${esc(p.source_title||'–')}
URI
${p.source_uri?`${esc(p.source_uri)}`:'–'}
Chunk
${p.chunk_index||'–'} / ${p.chunk_count||'–'}
Embedding
${esc([p.embedding_model,p.embedding_node_id].filter(Boolean).join(' · ')||'–')}
Generierung
${esc([p.generation_model,p.generation_node_id].filter(Boolean).join(' · ')||'–')}

Stärkste Synapsen

${(d.edges||[]).slice(0,12).map(e=>`
${esc(e.a===id?e.b:e.a)}w ${Number(e.weight).toFixed(2)}
`).join('')||'

Keine Synapsen.

'}

Timeline

${(d.events||[]).map(e=>`
${esc(e.type)}

${esc(e.summary)}

${new Date(e.created_at).toLocaleString()}
`).join('')||'

Keine Events.

'}`;$$('#knowledgeInspector [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));drawGraph();if(recenter)loadKnowledgeGraph(id)}catch(e){toast(e.message,true)}} diff --git a/platform/neuroforge/internal/store/knowledge.go b/platform/neuroforge/internal/store/knowledge.go index 817175a..c6e4f34 100644 --- a/platform/neuroforge/internal/store/knowledge.go +++ b/platform/neuroforge/internal/store/knowledge.go @@ -68,9 +68,12 @@ type KnowledgeEdge struct { } type KnowledgeGraph struct { - Center string `json:"center,omitempty"` - Nodes []MemoryPreview `json:"nodes"` - Edges []KnowledgeEdge `json:"edges"` + Center string `json:"center,omitempty"` + Nodes []MemoryPreview `json:"nodes"` + Edges []KnowledgeEdge `json:"edges"` + TotalMemories int `json:"total_memories"` + TotalSynapses int `json:"total_synapses"` + Truncated bool `json:"truncated"` } type KnowledgeMemoryDetail struct { @@ -216,7 +219,10 @@ func (s *Store) KnowledgeMemories(limit int, before time.Time, memoryType, statu s.mu.RLock() h := &previewHeap{} heap.Init(h) - for _, meta := range s.state.Memories { + for id, meta := range s.state.Memories { + if meta == nil { + continue + } if !before.IsZero() && !meta.CreatedAt.Before(before) { continue } @@ -237,6 +243,7 @@ func (s *Store) KnowledgeMemories(limit int, before time.Time, memoryType, statu continue } p := memoryPreview(*meta) + p.ID = id if h.Len() < limit { heap.Push(h, p) } else if p.CreatedAt.After((*h)[0].CreatedAt) { @@ -285,16 +292,28 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap selected[center] = true frontier = []string{center} } else { - // Keep only a tiny top-K seed set; never allocate one preview per memory. + // Start from a bounded top-K seed set. With no synapses yet, show a + // broader sample so a freshly ingested knowledge space is still useful + // and visibly populated before associative links have formed. + seedLimit := 12 + if len(s.state.Synapses) == 0 { + seedLimit = 64 + } + if seedLimit > maxNodes { + seedLimit = maxNodes + } type seedItem struct { id string score float64 created time.Time } - seeds := make([]seedItem, 0, 12) - for _, m := range s.state.Memories { - x := seedItem{id: m.ID, score: m.Salience + float64(m.AccessCount)*0.01, created: m.CreatedAt} - if len(seeds) < 12 { + seeds := make([]seedItem, 0, seedLimit) + for id, m := range s.state.Memories { + if m == nil { + continue + } + x := seedItem{id: id, score: m.Salience + float64(m.AccessCount)*0.01, created: m.CreatedAt} + if len(seeds) < seedLimit { seeds = append(seeds, x) continue } @@ -338,7 +357,10 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap } frontier = next } - out := KnowledgeGraph{Center: center, Nodes: make([]MemoryPreview, 0), Edges: make([]KnowledgeEdge, 0)} + out := KnowledgeGraph{ + Center: center, Nodes: make([]MemoryPreview, 0), Edges: make([]KnowledgeEdge, 0), + TotalMemories: len(s.state.Memories), TotalSynapses: len(s.state.Synapses), + } for id := range selected { if m, ok := s.fullMemoryForReadLocked(id); ok { out.Nodes = append(out.Nodes, memoryPreview(m)) @@ -353,6 +375,7 @@ func (s *Store) KnowledgeGraph(center string, depth, maxNodes int) KnowledgeGrap if len(out.Edges) > maxNodes*4 { out.Edges = out.Edges[:maxNodes*4] } + out.Truncated = len(out.Nodes) < out.TotalMemories || len(out.Edges) < out.TotalSynapses return out } @@ -401,9 +424,9 @@ func (s *Store) KnowledgeMemoryDetail(id string) (KnowledgeMemoryDetail, bool) { return nil } out.Parent = previewByID(m.ParentID) - for _, mm := range s.state.Memories { - if mm.ParentID == id { - if full, ok := s.fullMemoryForReadLocked(mm.ID); ok { + for childID, mm := range s.state.Memories { + if mm != nil && mm.ParentID == id { + if full, ok := s.fullMemoryForReadLocked(childID); ok { out.Children = append(out.Children, memoryPreview(full)) } } @@ -419,10 +442,13 @@ func (s *Store) KnowledgeMemoryDetail(id string) (KnowledgeMemoryDetail, bool) { out.Supersedes = append(out.Supersedes, *p) } } - for _, mm := range s.state.Memories { + for successorID, mm := range s.state.Memories { + if mm == nil { + continue + } for _, x := range mm.Supersedes { if x == id { - if full, ok := s.fullMemoryForReadLocked(mm.ID); ok { + if full, ok := s.fullMemoryForReadLocked(successorID); ok { out.SupersededBy = append(out.SupersededBy, memoryPreview(full)) } } diff --git a/platform/neuroforge/internal/store/knowledge_test.go b/platform/neuroforge/internal/store/knowledge_test.go index 2ce942e..e5d6174 100644 --- a/platform/neuroforge/internal/store/knowledge_test.go +++ b/platform/neuroforge/internal/store/knowledge_test.go @@ -75,4 +75,31 @@ func TestKnowledgeGraphEmptyUsesEmptySlices(t *testing.T) { if len(g.Nodes) != 0 || len(g.Edges) != 0 { t.Fatalf("unexpected graph contents: nodes=%d edges=%d", len(g.Nodes), len(g.Edges)) } + if g.TotalMemories != 0 || g.TotalSynapses != 0 || g.Truncated { + t.Fatalf("unexpected empty graph totals: %+v", g) + } +} + +func TestKnowledgeGraphShowsBoundedOrphansBeforeSynapsesExist(t *testing.T) { + s, err := New(t.TempDir()) + if err != nil { + t.Fatal(err) + } + defer s.Close() + for i := 0; i < 100; i++ { + m := &core.Memory{ID: NewID("orphan"), Kind: "evidence", MemoryType: core.MemorySemantic, Text: "isolated evidence", Vector: []float32{1, float32(i + 1)}, Salience: float64(i) / 100, Confidence: .7} + if err := s.AddMemory(m); err != nil { + t.Fatal(err) + } + } + g := s.KnowledgeGraph("", 3, 600) + if g.TotalMemories != 100 || g.TotalSynapses != 0 { + t.Fatalf("unexpected totals: %+v", g) + } + if len(g.Nodes) != 64 { + t.Fatalf("nodes=%d want bounded orphan sample of 64", len(g.Nodes)) + } + if !g.Truncated { + t.Fatal("graph should report sampling/truncation") + } } diff --git a/platform/neuroforge/internal/store/segment.go b/platform/neuroforge/internal/store/segment.go index e05590e..6fdaf60 100644 --- a/platform/neuroforge/internal/store/segment.go +++ b/platform/neuroforge/internal/store/segment.go @@ -184,6 +184,9 @@ func (s *SegmentStore) scanFile(path string) error { delete(s.scanMetadata, rec.ID) } else if rec.Memory != nil { m := cloneMemory(*rec.Memory) + // Historical segment records may have stored the canonical ID only + // in segmentRecord.ID. Restore the invariant Memory.ID == record ID. + m.ID = rec.ID if m.VectorDim == 0 && len(m.Vector) > 0 { m.VectorDim = len(m.Vector) } @@ -353,7 +356,10 @@ func (s *SegmentStore) Get(id string) (core.Memory, bool, bool, error) { if rec.Memory == nil { return core.Memory{}, true, false, errors.New("segment upsert has no memory body") } - return cloneMemory(*rec.Memory), true, false, nil + m := cloneMemory(*rec.Memory) + // The lookup key comes from the outer segment record and is authoritative. + m.ID = id + return m, true, false, nil } type liveSegmentCursor struct { diff --git a/platform/neuroforge/internal/store/segment_test.go b/platform/neuroforge/internal/store/segment_test.go index 1fe248e..4a99b8c 100644 --- a/platform/neuroforge/internal/store/segment_test.go +++ b/platform/neuroforge/internal/store/segment_test.go @@ -134,3 +134,51 @@ func TestIterateLiveVectorsSequentialUsesLatestSearchableRecords(t *testing.T) { t.Fatal("deleted memory must not be encoded") } } + +func TestLegacySegmentRecordRestoresCanonicalMemoryIDAndKnowledgeGraph(t *testing.T) { + dir := t.TempDir() + ss, err := openSegmentStore(filepath.Join(dir, "memory-segments"), 1<<20, false) + if err != nil { + t.Fatal(err) + } + // Historical compatibility fixture: the outer segment record owns the ID, + // while the embedded memory body has no ID. Older persisted data can have + // this shape even though current writers always populate both fields. + legacy := core.Memory{ + Kind: "evidence", + MemoryType: core.MemorySemantic, + Text: "legacy segment evidence", + Vector: []float32{1, 0}, + Salience: 1, + Confidence: .8, + } + if err := ss.appendRecord(segmentRecord{Revision: 1, Op: "upsert", ID: "legacy-id", Memory: &legacy}); err != nil { + t.Fatal(err) + } + ss.Close() + + s, err := New(dir) + if err != nil { + t.Fatal(err) + } + defer s.Close() + + if meta := s.state.Memories["legacy-id"]; meta == nil || meta.ID != "legacy-id" { + t.Fatalf("catalog identity was not repaired: %#v", meta) + } + got, ok := s.GetMemory("legacy-id") + if !ok || got.ID != "legacy-id" || got.Text != legacy.Text { + t.Fatalf("hydrated memory identity/body mismatch: ok=%v memory=%#v", ok, got) + } + g := s.KnowledgeGraph("", 3, 600) + if len(g.Nodes) != 1 || g.Nodes[0].ID != "legacy-id" { + t.Fatalf("legacy memory missing from knowledge graph: %#v", g.Nodes) + } + if g.TotalMemories != 1 || g.TotalSynapses != 0 || g.Truncated { + t.Fatalf("unexpected graph totals: %+v", g) + } + hits := s.SearchVector([]float32{1, 0}, 4, .1, 0) + if len(hits) == 0 || hits[0].Memory.ID != "legacy-id" { + t.Fatalf("legacy memory missing from vector retrieval: %#v", hits) + } +} diff --git a/platform/neuroforge/internal/store/source_index.go b/platform/neuroforge/internal/store/source_index.go index 293d171..9ef6131 100644 --- a/platform/neuroforge/internal/store/source_index.go +++ b/platform/neuroforge/internal/store/source_index.go @@ -57,11 +57,11 @@ func (s *Store) MemoryByProvenanceSourceID(sourceID string) (MemoryLookup, bool) if sourceID == "" { return MemoryLookup{}, false } - for _, meta := range s.state.Memories { + for id, meta := range s.state.Memories { if meta == nil || strings.TrimSpace(meta.Provenance.SourceID) != sourceID { continue } - m, ok := s.fullMemoryForReadLocked(meta.ID) + m, ok := s.fullMemoryForReadLocked(id) if ok { return MemoryLookup{Memory: cloneMemory(m)}, true } diff --git a/platform/neuroforge/internal/store/store.go b/platform/neuroforge/internal/store/store.go index 5c16531..ad9d791 100644 --- a/platform/neuroforge/internal/store/store.go +++ b/platform/neuroforge/internal/store/store.go @@ -481,7 +481,17 @@ func applyNewDefaults(c *core.Config) { } func migrateMemories(memories map[string]*core.Memory, localShard string) { - for _, m := range memories { + for id, m := range memories { + if m == nil { + continue + } + // The catalog map key is the canonical memory identity. Older segment + // records could carry the ID only in the outer segment record while the + // embedded Memory.ID was empty. Repair that legacy representation on + // every open so graph/index/retention code sees one stable identity. + if m.ID != id { + m.ID = id + } if m.MemoryType == "" { m.MemoryType = inferMemoryType(m.Kind) } @@ -711,7 +721,7 @@ func (s *Store) rebuildHotIndexesLocked() { continue } dim := len(m.Vector) - batches[dim] = append(batches[dim], vector.HNSWItem{ID: m.ID, Vector: m.Vector}) + batches[dim] = append(batches[dim], vector.HNSWItem{ID: c.id, Vector: m.Vector}) } for dim, items := range batches { idx := s.newIndexLocked() @@ -864,6 +874,8 @@ func (s *Store) fullMemoryForReadLocked(id string) (core.Memory, bool) { if err != nil || !found || deleted { return cloneMemory(*meta), true } + // Segment record ID / catalog key is authoritative for legacy records. + m.ID = id if s.pageCache != nil { s.pageCache.Put(m) } diff --git a/platform/neuroforge/internal/store/v3.go b/platform/neuroforge/internal/store/v3.go index a2651ef..f0e1cea 100644 --- a/platform/neuroforge/internal/store/v3.go +++ b/platform/neuroforge/internal/store/v3.go @@ -29,7 +29,7 @@ func (s *Store) resolveConflictLocked(in *core.Memory) []core.Memory { var changed []core.Memory group := "conflict:" + strings.ToLower(key) for id, meta := range s.state.Memories { - if meta.ID == in.ID || !strings.EqualFold(strings.TrimSpace(meta.TruthKey), key) || meta.Status == core.MemoryArchived { + if id == in.ID || !strings.EqualFold(strings.TrimSpace(meta.TruthKey), key) || meta.Status == core.MemoryArchived { continue } old, ok := s.materializeMemoryLocked(id) @@ -359,19 +359,22 @@ func (s *Store) RunRetention(now time.Time) (RetentionResult, error) { candidates := make([]retentionCandidate, 0, len(s.state.Memories)) deleteSet := map[string]bool{} changed := []core.Memory{} - for _, m := range s.state.Memories { + for id, m := range s.state.Memories { + if m == nil { + continue + } ageDays := now.Sub(m.CreatedAt).Hours() / 24 if m.MemoryType == core.MemoryWorking && cfg.WorkingTTLHours > 0 && now.Sub(m.CreatedAt).Hours() >= cfg.WorkingTTLHours { - deleteSet[m.ID] = true + deleteSet[id] = true continue } utility := memoryUtility(m, now) - candidates = append(candidates, retentionCandidate{id: m.ID, utility: utility, ageDays: ageDays}) + candidates = append(candidates, retentionCandidate{id: id, utility: utility, ageDays: ageDays}) if ageDays < cfg.MinAgeDays || utility >= cfg.MinUtility || m.Status == core.MemoryArchived { continue } if cfg.DeleteConsolidated && m.ConsolidatedInto != "" { - deleteSet[m.ID] = true + deleteSet[id] = true continue } full, ok := s.materializeMemoryLocked(m.ID)