fix(memory): turnId repoRoot prefix collision (/repo vs /repo2)
The repo-map turnId tag and the L3 retriever filter used 'repomap:$repoRoot' with no delimiter, so a retriever for /repo also matched /repo2 entries (startsWith). Add a trailing ':' to the tag (no-stateKey branch) and the retriever filter so the repoRoot boundary is explicit. Test: /repo retrieval no longer leaks /repo2 entries, still matches its own with/without a stateKey suffix.
This commit is contained in:
+2
-1
@@ -17,7 +17,8 @@ class L3RepoKnowledgeRetriever(
|
|||||||
override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit> {
|
override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List<RepoKnowledgeHit> {
|
||||||
val vector = embedder.embed(query)
|
val vector = embedder.embed(query)
|
||||||
return l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
|
return l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR))
|
||||||
.filter { it.entry.turnId.startsWith("repomap:$repoRoot") }
|
// Trailing ':' so "/repo" does not also match "/repo2" turnIds (prefix collision).
|
||||||
|
.filter { it.entry.turnId.startsWith("repomap:$repoRoot:") }
|
||||||
.take(k)
|
.take(k)
|
||||||
.map { RepoKnowledgeHit(path = it.entry.text.substringBefore(":"), text = it.entry.text, score = it.score) }
|
.map { RepoKnowledgeHit(path = it.entry.text.substringBefore(":"), text = it.entry.text, score = it.score) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ class ProjectMemoryService(
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val tag = if (stateKey != null) "repomap:$repoRoot:$stateKey" else "repomap:$repoRoot"
|
// Trailing ':' delimiter so a repoRoot prefix can't collide with a longer sibling
|
||||||
|
// (/repo vs /repo2) under the retriever's startsWith filter.
|
||||||
|
val tag = if (stateKey != null) "repomap:$repoRoot:$stateKey" else "repomap:$repoRoot:"
|
||||||
entries.forEach { entry ->
|
entries.forEach { entry ->
|
||||||
runCatching {
|
runCatching {
|
||||||
val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
|
val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package com.correx.apps.server.memory
|
||||||
|
|
||||||
|
import com.correx.core.events.types.SessionId
|
||||||
|
import com.correx.core.inference.Embedder
|
||||||
|
import com.correx.core.router.l3.InMemoryL3MemoryStore
|
||||||
|
import com.correx.core.router.l3.L3MemoryEntry
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
private class RetrieverConstantEmbedder(override val dimension: Int = 4) : Embedder {
|
||||||
|
override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 0.5f }
|
||||||
|
}
|
||||||
|
|
||||||
|
class L3RepoKnowledgeRetrieverTest {
|
||||||
|
|
||||||
|
private val vec = FloatArray(4) { 0.5f }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retriever for a repoRoot does not match a sibling whose path it prefixes`(): Unit = runBlocking {
|
||||||
|
val l3 = InMemoryL3MemoryStore()
|
||||||
|
l3.store(L3MemoryEntry("1", SessionId("s"), "repomap:/repo:git:h", "repo/A.kt: Foo", vec, 0L))
|
||||||
|
l3.store(L3MemoryEntry("2", SessionId("s"), "repomap:/repo2:git:h", "repo2/B.kt: Bar", vec, 0L))
|
||||||
|
|
||||||
|
val hits = L3RepoKnowledgeRetriever(RetrieverConstantEmbedder(), l3, "/repo")
|
||||||
|
.retrieve(SessionId("s"), "anything", 10)
|
||||||
|
|
||||||
|
assertEquals(listOf("repo/A.kt: Foo"), hits.map { it.text }, "must not leak /repo2 entries into /repo")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retriever matches its own repoRoot entries with or without a stateKey suffix`(): Unit = runBlocking {
|
||||||
|
val l3 = InMemoryL3MemoryStore()
|
||||||
|
l3.store(L3MemoryEntry("1", SessionId("s"), "repomap:/repo:git:h1", "repo/A.kt", vec, 0L))
|
||||||
|
l3.store(L3MemoryEntry("2", SessionId("s"), "repomap:/repo:", "repo/B.kt", vec, 0L))
|
||||||
|
|
||||||
|
val hits = L3RepoKnowledgeRetriever(RetrieverConstantEmbedder(), l3, "/repo")
|
||||||
|
.retrieve(SessionId("s"), "anything", 10)
|
||||||
|
|
||||||
|
assertEquals(setOf("repo/A.kt", "repo/B.kt"), hits.map { it.text }.toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user