fix(memory): trailing-delimiter turnId namespace + newline fingerprint
- repomap: tag prefix match requires trailing ':' so /repo can't match /repo2 (L3RepoKnowledgeRetriever filter + ProjectMemoryService write; the existing existsByTurnIdPrefix check already included the delimiter) - WorkspaceStateProbe fingerprint joins entries with newline to match docstring - regression test for /repo vs /repo2 non-collision Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@ class L3RepoKnowledgeRetriever(
|
||||
override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit> {
|
||||
val vector = embedder.embed(query)
|
||||
return l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
|
||||
.filter { it.entry.turnId.startsWith("repomap:$repoRoot") }
|
||||
.filter { it.entry.turnId.startsWith("repomap:$repoRoot:") }
|
||||
.take(k)
|
||||
.map { RepoKnowledgeHit(path = it.entry.text.substringBefore(":"), text = it.entry.text, score = it.score) }
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ class ProjectMemoryService(
|
||||
),
|
||||
),
|
||||
)
|
||||
val tag = if (stateKey != null) "repomap:$repoRoot:$stateKey" else "repomap:$repoRoot"
|
||||
val tag = if (stateKey != null) "repomap:$repoRoot:$stateKey" else "repomap:$repoRoot:"
|
||||
entries.forEach { entry ->
|
||||
runCatching {
|
||||
val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
|
||||
|
||||
@@ -109,7 +109,7 @@ class RealWorkspaceStateProbe : WorkspaceStateProbe {
|
||||
.toList()
|
||||
}
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
lines.forEach { digest.update(it.toByteArray()) }
|
||||
digest.update(lines.joinToString("\n").toByteArray())
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }.take(FINGERPRINT_HEX_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
+33
@@ -12,6 +12,7 @@ import com.correx.core.events.types.SessionId
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Path
|
||||
@@ -121,4 +122,36 @@ class ProjectMemoryServiceReuseTest {
|
||||
"entries should be embedded with stateKey tag",
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `repoRoot prefix does not collide across roots — repo vs repo2`(): Unit = runBlocking {
|
||||
val es = InMemoryEventStore()
|
||||
val l3 = InMemoryL3MemoryStore()
|
||||
val probe = FakeWorkspaceStateProbe(WorkspaceState("git:hash1", "git", "main", false))
|
||||
val embedder = ConstantEmbedderReuse()
|
||||
|
||||
// Index /repo and /repo2 into the same shared L3 store with the same stateKey.
|
||||
// The trailing-':' delimiter on the repomap: tag must keep their entries from bleeding
|
||||
// across roots: "/repo" must NOT match tags written for "/repo2".
|
||||
val repoEntries = listOf(RepoMapEntry(path = "src/Repo.kt", score = 1.0, symbols = listOf("RepoClass")))
|
||||
val repo2Entries = listOf(RepoMapEntry(path = "src/Repo2.kt", score = 1.0, symbols = listOf("Repo2Class")))
|
||||
|
||||
service(es, l3, CountingIndexer(repoEntries), probe, root = "/repo")
|
||||
.indexAndRecord(SessionId("session-collide-repo"), "/repo")
|
||||
service(es, l3, CountingIndexer(repo2Entries), probe, root = "/repo2")
|
||||
.indexAndRecord(SessionId("session-collide-repo2"), "/repo2")
|
||||
|
||||
// existsByTurnIdPrefix with the delimiter must not match the other root.
|
||||
assertTrue(l3.existsByTurnIdPrefix("repomap:/repo:"), "tag for /repo should exist")
|
||||
assertTrue(l3.existsByTurnIdPrefix("repomap:/repo2:"), "tag for /repo2 should exist")
|
||||
|
||||
// The retriever scoped to /repo must return only /repo's entry, never /repo2's.
|
||||
val hits = L3RepoKnowledgeRetriever(embedder = embedder, l3MemoryStore = l3, repoRoot = "/repo")
|
||||
.retrieve(SessionId("session-collide-query"), "anything", k = 10)
|
||||
|
||||
assertTrue(hits.isNotEmpty(), "retriever for /repo should find /repo entries")
|
||||
val text = hits.joinToString(" ") { it.text }
|
||||
assertTrue(text.contains("src/Repo.kt"), "should contain /repo entry; got: $text")
|
||||
assertFalse(text.contains("src/Repo2.kt"), "must NOT contain /repo2 entry (prefix collision); got: $text")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user