fix(kernel): deterministic repo-map floor + 0.0-hit filter + closest-path suggestion
RepoMapComputed recorded per session (in-memory scan cache keeps the walk-once guarantee); zero-score embedder hits filtered before reaching the agent, falling back to the deterministic repo map; REFERENCE_EXISTS surfaces a closest-path hint via closestPathByFilename. Breaks the hallucinated-package-path read loop even with a noop embedder.
This commit is contained in:
@@ -4,6 +4,7 @@ import com.correx.core.config.ProjectConfig
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.RepoMapComputedEvent
|
||||
import com.correx.core.events.events.RepoMapEntry
|
||||
import com.correx.core.events.events.SteeringNoteAddedEvent
|
||||
import com.correx.core.events.events.WorkspaceStateObservedEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
@@ -44,6 +45,11 @@ class ProjectMemoryService(
|
||||
private val indexer: RepoMapIndexerPort = RepoMapIndexer(),
|
||||
private val workspaceStateProbe: WorkspaceStateProbe = RealWorkspaceStateProbe(),
|
||||
) {
|
||||
// repoRoot+stateKey -> scanned entries, so a second session on the same workspace state reuses
|
||||
// the walk instead of re-scanning the FS (preserves the one-walk-per-state guarantee while still
|
||||
// emitting a per-session RepoMapComputedEvent). Per-process; cold after a restart (one walk then).
|
||||
private val entryCache = java.util.concurrent.ConcurrentHashMap<String, List<RepoMapEntry>>()
|
||||
|
||||
private fun tag(repoRoot: String) = "project:$repoRoot"
|
||||
|
||||
/** Resolved repo-root key: configured [ProjectConfig.root], else the working dir. */
|
||||
@@ -59,12 +65,17 @@ class ProjectMemoryService(
|
||||
val stateKey = eventStore.read(sessionId)
|
||||
.mapNotNull { it.payload as? WorkspaceStateObservedEvent }
|
||||
.lastOrNull()?.stateKey
|
||||
if (stateKey != null && l3MemoryStore.existsByTurnIdPrefix("repomap:$repoRoot:$stateKey")) {
|
||||
log.debug("repo-map already indexed for {}@{} — skipping", repoRoot, stateKey)
|
||||
return
|
||||
}
|
||||
runCatching {
|
||||
val entries = indexer.index(java.nio.file.Path.of(repoRoot))
|
||||
// Record the map into THIS session's log unconditionally. buildRepoMapEntries and replay
|
||||
// are session-scoped (invariant #9), so the old walk-once-and-skip left every session after
|
||||
// the first with no deterministic repo grounding. To keep the walk-once guarantee we reuse
|
||||
// the scanned entries from a per-stateKey in-memory cache (no re-walk), and only the
|
||||
// expensive L3 embed/store below stays guarded by the persistent L3 presence check.
|
||||
val cacheKey = stateKey?.let { "$repoRoot::$it" }
|
||||
val entries = cacheKey?.let { entryCache[it] }
|
||||
?: indexer.index(java.nio.file.Path.of(repoRoot)).also {
|
||||
if (cacheKey != null && it.isNotEmpty()) entryCache[cacheKey] = it
|
||||
}
|
||||
if (entries.isEmpty()) return
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
@@ -85,33 +96,47 @@ class ProjectMemoryService(
|
||||
),
|
||||
),
|
||||
)
|
||||
// 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 ->
|
||||
runCatching {
|
||||
val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
|
||||
l3MemoryStore.store(
|
||||
L3MemoryEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
sessionId = sessionId,
|
||||
turnId = tag,
|
||||
text = text,
|
||||
vector = embedder.embed(text),
|
||||
timestampMs = Clock.System.now().toEpochMilliseconds(),
|
||||
),
|
||||
)
|
||||
}.exceptionOrNull()?.let { e ->
|
||||
if (e is CancellationException) throw e
|
||||
log.warn("repo-map embedding failed for {}: {}", entry.path, e.message)
|
||||
}
|
||||
}
|
||||
embedRepoMapIntoL3(sessionId, repoRoot, stateKey, entries)
|
||||
}.exceptionOrNull()?.let { e ->
|
||||
if (e is CancellationException) throw e
|
||||
log.warn("repo-map indexing failed for {}: {}", repoRoot, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/** Embed the scanned repo-map entries into L3 once per workspace state (skips if already stored). */
|
||||
private suspend fun embedRepoMapIntoL3(
|
||||
sessionId: SessionId,
|
||||
repoRoot: String,
|
||||
stateKey: String?,
|
||||
entries: List<RepoMapEntry>,
|
||||
) {
|
||||
if (stateKey != null && l3MemoryStore.existsByTurnIdPrefix("repomap:$repoRoot:$stateKey")) {
|
||||
log.debug("repo-map already embedded for {}@{} — skipping L3 store", repoRoot, stateKey)
|
||||
return
|
||||
}
|
||||
// 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 ->
|
||||
runCatching {
|
||||
val text = entry.path + if (entry.symbols.isEmpty()) "" else ": ${entry.symbols.joinToString(", ")}"
|
||||
l3MemoryStore.store(
|
||||
L3MemoryEntry(
|
||||
id = UUID.randomUUID().toString(),
|
||||
sessionId = sessionId,
|
||||
turnId = tag,
|
||||
text = text,
|
||||
vector = embedder.embed(text),
|
||||
timestampMs = Clock.System.now().toEpochMilliseconds(),
|
||||
),
|
||||
)
|
||||
}.exceptionOrNull()?.let { e ->
|
||||
if (e is CancellationException) throw e
|
||||
log.warn("repo-map embedding failed for {}: {}", entry.path, e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe the workspace content state and record it as a [WorkspaceStateObservedEvent]
|
||||
* (invariant #9: environment observations are recorded as events, never re-observed).
|
||||
|
||||
Reference in New Issue
Block a user