From 7a9e060a55c29dc57b8a833b84502ffc230a583b Mon Sep 17 00:00:00 2001 From: kami Date: Thu, 11 Jun 2026 13:39:57 +0400 Subject: [PATCH] feat(kernel): RepoKnowledgeRetriever port + relevance-retrieved repo context with recency fallback --- .../kotlin/com/correx/apps/server/Main.kt | 23 +++++++++++---- .../server/memory/L3RepoKnowledgeRetriever.kt | 24 ++++++++++++++++ .../kernel/orchestration/ContextFeedback.kt | 17 +++++++++++ .../DefaultSessionOrchestrator.kt | 3 +- .../orchestration/RepoKnowledgeRetriever.kt | 8 ++++++ .../orchestration/SessionOrchestrator.kt | 28 ++++++++++++++++++- .../src/test/kotlin/ContextFeedbackTest.kt | 16 +++++++++++ 7 files changed, 111 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/main/kotlin/com/correx/apps/server/memory/L3RepoKnowledgeRetriever.kt create mode 100644 core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/RepoKnowledgeRetriever.kt diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt index 58093d7a..d12a57be 100644 --- a/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt +++ b/apps/server/src/main/kotlin/com/correx/apps/server/Main.kt @@ -276,6 +276,22 @@ fun main() { val artifactKindRegistry = DefaultArtifactKindRegistry().also { reg -> configArtifactKindsEarly.forEach { reg.register(it) } } + val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder) + val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3) + // Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries: + // persisted vectors survive restart but the metadata map does not (no-op for + // non-rehydratable backends like in_memory). + runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) } + val repoKnowledgeRetriever: com.correx.apps.server.memory.L3RepoKnowledgeRetriever? = + if (correxConfig.project.enabled) { + com.correx.apps.server.memory.L3RepoKnowledgeRetriever( + embedder = embedder, + l3MemoryStore = l3MemoryStore, + repoRoot = workspaceRoot.toString(), + ) + } else { + null + } val orchestrator = DefaultSessionOrchestrator( repositories = repositories, engines = engines, @@ -285,13 +301,8 @@ fun main() { decisionJournalRepository = decisionJournalRepository, compactionService = journalCompactionService, artifactKindRegistry = artifactKindRegistry, + repoKnowledgeRetriever = repoKnowledgeRetriever, ) - val embedder = InfrastructureModule.createEmbedderFromConfig(correxConfig.router.embedder) - val l3MemoryStore = InfrastructureModule.createL3MemoryStoreFromConfig(correxConfig.router.l3) - // Rebuild the L3 metadata map from the ChatTurnEvent log before serving queries: - // persisted vectors survive restart but the metadata map does not (no-op for - // non-rehydratable backends like in_memory). - runBlocking { L3MetadataRehydrator(eventStore).rehydrate(l3MemoryStore) } val workflowRegistry = FileSystemWorkflowRegistry( InfrastructureModule.createWorkflowLoader(configArtifactKindsEarly), ) diff --git a/apps/server/src/main/kotlin/com/correx/apps/server/memory/L3RepoKnowledgeRetriever.kt b/apps/server/src/main/kotlin/com/correx/apps/server/memory/L3RepoKnowledgeRetriever.kt new file mode 100644 index 00000000..5aeef84c --- /dev/null +++ b/apps/server/src/main/kotlin/com/correx/apps/server/memory/L3RepoKnowledgeRetriever.kt @@ -0,0 +1,24 @@ +package com.correx.apps.server.memory + +import com.correx.core.events.events.RepoKnowledgeHit +import com.correx.core.events.types.SessionId +import com.correx.core.inference.Embedder +import com.correx.core.kernel.orchestration.RepoKnowledgeRetriever +import com.correx.core.router.l3.L3MemoryStore +import com.correx.core.router.l3.L3Query + +private const val RETRIEVAL_OVERSAMPLE_FACTOR = 4 + +class L3RepoKnowledgeRetriever( + private val embedder: Embedder, + private val l3MemoryStore: L3MemoryStore, + private val repoRoot: String, +) : RepoKnowledgeRetriever { + override suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List { + val vector = embedder.embed(query) + return l3MemoryStore.query(L3Query(vector = vector, k = k * RETRIEVAL_OVERSAMPLE_FACTOR)) + .filter { it.entry.turnId.startsWith("repomap:$repoRoot") } + .take(k) + .map { RepoKnowledgeHit(path = it.entry.text.substringBefore(":"), text = it.entry.text, score = it.score) } + } +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ContextFeedback.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ContextFeedback.kt index cc1d13fe..9517a6af 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ContextFeedback.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/ContextFeedback.kt @@ -4,6 +4,7 @@ package com.correx.core.kernel.orchestration // and to stay under detekt's function-count threshold (same pattern as isBackEdge). import com.correx.core.artifacts.kind.ArtifactKind +import com.correx.core.events.events.RepoKnowledgeHit import com.correx.core.context.model.ContextEntry import com.correx.core.context.model.ContextLayer import com.correx.core.context.model.EntryRole @@ -66,6 +67,22 @@ fun buildArtifactKindVocabularyEntry(kinds: Collection): ContextEn ) } +fun buildRelevantFilesEntry(hits: List): ContextEntry { + val content = buildString { + append("## Relevant files (retrieved by semantic similarity)\n") + hits.forEach { append("- ").append(it.text).append("\n") } + }.trimEnd() + return ContextEntry( + id = ContextEntryId(UUID.randomUUID().toString()), + layer = ContextLayer.L3, + content = content, + sourceType = "relevantFiles", + sourceId = "repo-knowledge", + tokenEstimate = content.length / 4, + role = EntryRole.SYSTEM, + ) +} + fun buildProjectProfileEntry(profile: BoundProjectProfile): ContextEntry { val content = buildString { append("## Project profile\n") diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt index 8f3b610d..6563f075 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/DefaultSessionOrchestrator.kt @@ -57,7 +57,8 @@ class DefaultSessionOrchestrator( private val decisionJournalRepository: DefaultDecisionJournalRepository, private val compactionService: JournalCompactionService? = null, artifactKindRegistry: ArtifactKindRegistry? = null, -) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry), ApprovalGateway { + repoKnowledgeRetriever: RepoKnowledgeRetriever? = null, +) : SessionOrchestrator(repositories, engines, artifactStore, decisionJournalRepository, artifactKindRegistry = artifactKindRegistry, repoKnowledgeRetriever = repoKnowledgeRetriever), ApprovalGateway { override val tokenizer: Tokenizer? = tokenizer override val cancellations: ConcurrentHashMap = ConcurrentHashMap() diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/RepoKnowledgeRetriever.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/RepoKnowledgeRetriever.kt new file mode 100644 index 00000000..5465fd4e --- /dev/null +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/RepoKnowledgeRetriever.kt @@ -0,0 +1,8 @@ +package com.correx.core.kernel.orchestration + +import com.correx.core.events.events.RepoKnowledgeHit +import com.correx.core.events.types.SessionId + +interface RepoKnowledgeRetriever { + suspend fun retrieve(sessionId: SessionId, query: String, k: Int): List +} diff --git a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index b6bdde2f..8283d69c 100644 --- a/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt +++ b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt @@ -35,6 +35,7 @@ import com.correx.core.events.events.ArtifactValidatingEvent import com.correx.core.events.events.EventMetadata import com.correx.core.events.events.EventPayload import com.correx.core.events.events.InferenceCompletedEvent +import com.correx.core.events.events.RepoKnowledgeRetrievedEvent import com.correx.core.events.events.RepoMapComputedEvent import com.correx.core.events.events.InferenceFailedEvent import com.correx.core.events.events.InferenceStartedEvent @@ -155,6 +156,7 @@ abstract class SessionOrchestrator( private val decisionJournalRepository: DefaultDecisionJournalRepository, private val decisionJournalRenderer: DecisionJournalRenderer = DecisionJournalRenderer(), private val artifactKindRegistry: ArtifactKindRegistry? = null, + private val repoKnowledgeRetriever: RepoKnowledgeRetriever? = null, ) { private val log = LoggerFactory.getLogger(this::class.java) private val eventStore: EventStore = repositories.eventStore @@ -349,7 +351,7 @@ abstract class SessionOrchestrator( role = EntryRole.SYSTEM, ), ) - val repoMapEntries = buildRepoMapEntries(sessionId) + val repoMapEntries = buildContextualRepoEntries(sessionId, stageId, stageConfig) val sessionEvents = eventStore.read(sessionId) val criticIds = criticArtifactIds(sessionEvents, graph, stageId) val criticFrom = sessionEvents @@ -1024,6 +1026,30 @@ abstract class SessionOrchestrator( ) } + private suspend fun buildContextualRepoEntries( + sessionId: SessionId, + stageId: StageId, + stageConfig: StageConfig, + ): List { + val recorded = eventStore.read(sessionId) + .mapNotNull { it.payload as? RepoKnowledgeRetrievedEvent } + .lastOrNull { it.stageId == stageId } + if (recorded != null) return listOf(buildRelevantFilesEntry(recorded.hits)) + + val retriever = repoKnowledgeRetriever ?: return buildRepoMapEntries(sessionId) + val query = (stageConfig.metadata["promptInline"] ?: stageConfig.metadata["prompt"] ?: stageId.value) + .trim().ifBlank { stageId.value } + val hits = runCatching { retriever.retrieve(sessionId, query, REPO_MAP_INJECT_TOP_K) } + .getOrElse { e -> + if (e is CancellationException) throw e + log.warn("repo-knowledge retrieval failed for stage {}: {}", stageId.value, e.message) + emptyList() + } + if (hits.isEmpty()) return buildRepoMapEntries(sessionId) + emit(sessionId, RepoKnowledgeRetrievedEvent(sessionId, stageId, query, hits)) + return listOf(buildRelevantFilesEntry(hits)) + } + private suspend fun buildNeedsArtifactEntries( sessionId: SessionId, stageConfig: StageConfig, diff --git a/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt b/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt index 840cfbb0..4de53ce9 100644 --- a/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt +++ b/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt @@ -10,8 +10,10 @@ import com.correx.core.events.types.ArtifactId import com.correx.core.events.types.SessionId import com.correx.core.events.types.StageId import com.correx.core.events.types.TransitionId +import com.correx.core.events.events.RepoKnowledgeHit import com.correx.core.kernel.orchestration.buildArtifactKindVocabularyEntry import com.correx.core.kernel.orchestration.buildProjectProfileEntry +import com.correx.core.kernel.orchestration.buildRelevantFilesEntry import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry import com.correx.core.kernel.orchestration.criticArtifactIds import com.correx.core.sessions.BoundProjectProfile @@ -109,6 +111,20 @@ class ContextFeedbackTest { assertTrue(entry.content.contains("report (llm-emitted)"), "content should flag llm-emitted: ${entry.content}") } + @Test + fun `buildRelevantFilesEntry produces L3 system entry with Relevant files header`() { + val hits = listOf( + RepoKnowledgeHit(path = "src/Parser.kt", text = "src/Parser.kt: Parser, Lexer", score = 0.9f), + RepoKnowledgeHit(path = "src/Main.kt", text = "src/Main.kt: main", score = 0.7f), + ) + val entry = buildRelevantFilesEntry(hits) + assertEquals(ContextLayer.L3, entry.layer) + assertEquals(EntryRole.SYSTEM, entry.role) + assertEquals("relevantFiles", entry.sourceType) + assertTrue(entry.content.startsWith("## Relevant files"), "content: ${entry.content}") + assertTrue(entry.content.contains("src/Parser.kt"), "content: ${entry.content}") + } + @Test fun `no refinement events yields empty set`() { assertTrue(criticArtifactIds(emptyList(), graphWith(), StageId("implement")).isEmpty())