feat(concept): heuristic concept compiler (ACR fold-in)
Promotes recurring validated failure->fix patterns into L3 as retrieval-on-demand concepts. Deterministic core: ConceptCompilerProjection clusters RetryAttempted->StageCompleted pairs by fingerprint (gate-agnostic), promotes at N=3 cross-session validated fixes, never-contradicted. ConceptPromotedEvent is the sole authoritative write (idempotent under replay); ConceptCompilerService appends it + best-effort injects to L3 (non-authoritative, inv #6). Wired live in ServerModule.start() on StageCompleted. Design: docs/plans/2026-07-12-acr-concept-compiler.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhFXmKe4WisSSPf9LrmmTg
This commit is contained in:
@@ -535,6 +535,11 @@ fun main() {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
// Heuristic concept compiler (design 2026-07-12-acr-concept-compiler.md): promotes recurring
|
||||
// validated failure→fix patterns into L3 as retrieval-on-demand concepts. Fires only on ≥N
|
||||
// cross-session validated fixes, so it's harmless to run unconditionally.
|
||||
val conceptCompilerService =
|
||||
com.correx.apps.server.concept.ConceptCompilerService(eventStore, embedder, l3MemoryStore)
|
||||
// Built from a config snapshot and reused by ConfigService's rebuild hook so toggling
|
||||
// project.enabled / personalization.* applies live to the next session.
|
||||
fun buildProjectMemory(cfg: CorrexConfig): com.correx.apps.server.memory.ProjectMemoryService? =
|
||||
@@ -638,6 +643,7 @@ fun main() {
|
||||
narrationMaxPerRun = correxConfig.talkie.narration.maxPerRun,
|
||||
projectMemory = projectMemory,
|
||||
architectContradictionChecker = architectContradictionChecker,
|
||||
conceptCompilerService = conceptCompilerService,
|
||||
configHolder = configHolder,
|
||||
freestyleDriver = freestyleDriver,
|
||||
operatorProfile = operatorProfile,
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.correx.core.config.ProjectProfileLoader
|
||||
import com.correx.apps.server.memory.ArchitectContradictionChecker
|
||||
import com.correx.core.events.events.AgentInstructionsBoundEvent
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.events.ArtifactContentStoredEvent
|
||||
import com.correx.core.events.events.ArtifactCreatedEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
@@ -103,6 +104,9 @@ class ServerModule(
|
||||
// (tests / project.enabled=false). Unlike projectMemory there is no live config-rebuild hook:
|
||||
// a server restart re-reads project.enabled, which is enough for this informational flag.
|
||||
private val architectContradictionChecker: ArchitectContradictionChecker? = null,
|
||||
// Heuristic concept compiler write-side (design 2026-07-12-acr-concept-compiler.md). Null disables
|
||||
// the promotion hook (tests). Live-only like the subscriptions below — never runs under replay.
|
||||
private val conceptCompilerService: com.correx.apps.server.concept.ConceptCompilerService? = null,
|
||||
// Live, swappable config. Null only in tests that don't exercise config editing; defaults to a
|
||||
// holder seeded from defaults so callers always have a value to read.
|
||||
val configHolder: com.correx.core.config.ConfigHolder =
|
||||
@@ -240,6 +244,21 @@ class ServerModule(
|
||||
}
|
||||
.launchIn(moduleScope)
|
||||
}
|
||||
|
||||
// Heuristic concept compiler (design 2026-07-12-acr-concept-compiler.md). Live-only like the
|
||||
// hooks above: subscribeAll() replays nothing and ServerModule is never built under replay, so
|
||||
// promotion never re-fires on restart/replay (invariant #8). A StageCompleted may resolve a
|
||||
// validated failure→fix; re-fold the log and promote any newly-eligible fingerprint. Failures
|
||||
// are logged and swallowed — promotion is best-effort enrichment, never on the stage's path.
|
||||
conceptCompilerService?.let { compiler ->
|
||||
eventStore.subscribeAll()
|
||||
.filter { it.payload is StageCompletedEvent }
|
||||
.onEach {
|
||||
runCatching { compiler.runOnce() }
|
||||
.onFailure { e -> log.warn("concept compiler run failed: {}", e.message) }
|
||||
}
|
||||
.launchIn(moduleScope)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.correx.apps.server.concept
|
||||
|
||||
import com.correx.core.events.events.ConceptPromotedEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.EventId
|
||||
import com.correx.core.events.types.SessionId
|
||||
import com.correx.core.inference.Embedder
|
||||
import com.correx.core.kernel.concept.ConceptCluster
|
||||
import com.correx.core.kernel.concept.ConceptCompilerProjection
|
||||
import com.correx.core.kernel.concept.DEFAULT_PROMOTION_THRESHOLD
|
||||
import com.correx.core.talkie.l3.L3MemoryEntry
|
||||
import com.correx.core.talkie.l3.L3MemoryStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.datetime.Clock
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.util.UUID
|
||||
|
||||
private val log = LoggerFactory.getLogger(ConceptCompilerService::class.java)
|
||||
|
||||
/**
|
||||
* The write-side of the heuristic concept compiler (design 2026-07-12-acr-concept-compiler.md).
|
||||
* [ConceptCompilerProjection] does the deterministic clustering; this service is the single "write":
|
||||
* it folds the whole cross-session log, and for every fingerprint that has just become promotable it
|
||||
* appends a [ConceptPromotedEvent] (the authoritative record, idempotent under replay) and best-effort
|
||||
* injects the concept into L3 so it is retrieved on demand into future stage context — the same
|
||||
* pull/top-k path RepoKnowledge already uses, no broadcast. The L3 write is non-authoritative
|
||||
* (invariant #6); the event is truth, so an embed/store failure is logged and swallowed.
|
||||
*
|
||||
* Deterministic rule firing → no assessor gate needed (invariant #3/#7 hold trivially).
|
||||
*
|
||||
* ponytail: [runOnce] re-folds `allEvents()` on each call — O(n) in log size. Fine at local-first
|
||||
* scale and called on stage completions, not per-event. Switch to an incremental in-memory fold over
|
||||
* `subscribeAll()` if the log outgrows a full rescan.
|
||||
*/
|
||||
class ConceptCompilerService(
|
||||
private val eventStore: EventStore,
|
||||
private val embedder: Embedder,
|
||||
private val l3MemoryStore: L3MemoryStore,
|
||||
private val threshold: Int = DEFAULT_PROMOTION_THRESHOLD,
|
||||
) {
|
||||
private val projection = ConceptCompilerProjection()
|
||||
|
||||
/** Fold the full log, promote every newly-promotable fingerprint. Safe to call repeatedly. */
|
||||
suspend fun runOnce() {
|
||||
val state = eventStore.allEvents().fold(projection.initial(), projection::apply)
|
||||
state.promotable(threshold).forEach { promote(it) }
|
||||
}
|
||||
|
||||
private suspend fun promote(cluster: ConceptCluster) {
|
||||
val text = conceptText(cluster)
|
||||
eventStore.append(
|
||||
NewEvent(
|
||||
metadata = metadata(),
|
||||
payload = ConceptPromotedEvent(
|
||||
sessionId = SYSTEM_SESSION,
|
||||
fingerprint = cluster.fingerprint,
|
||||
gate = cluster.gate,
|
||||
conceptText = text,
|
||||
occurrences = cluster.validatedFixes,
|
||||
),
|
||||
),
|
||||
)
|
||||
injectToL3(cluster.fingerprint, text)
|
||||
log.info("promoted concept {} ({}x, gate={})", cluster.fingerprint, cluster.validatedFixes, cluster.gate)
|
||||
}
|
||||
|
||||
private suspend fun injectToL3(fingerprint: String, text: String) {
|
||||
runCatching {
|
||||
val now = Clock.System.now().toEpochMilliseconds()
|
||||
l3MemoryStore.store(
|
||||
L3MemoryEntry(
|
||||
id = "concept:$fingerprint",
|
||||
sessionId = SYSTEM_SESSION,
|
||||
turnId = "concept:$fingerprint",
|
||||
text = text,
|
||||
vector = embedder.embed(text),
|
||||
timestampMs = now,
|
||||
),
|
||||
)
|
||||
}.exceptionOrNull()?.let { e ->
|
||||
if (e is CancellationException) throw e
|
||||
log.warn("L3 inject failed for concept {}: {}", fingerprint, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun conceptText(c: ConceptCluster) =
|
||||
"Recurring ${c.gate} failure (validated-fixed ${c.validatedFixes}× across sessions): " +
|
||||
"${c.signature}. This failure class has a known resolution — check prior fixes before re-deriving."
|
||||
|
||||
private fun metadata() = EventMetadata(
|
||||
eventId = EventId(UUID.randomUUID().toString()),
|
||||
sessionId = SYSTEM_SESSION,
|
||||
timestamp = Clock.System.now(),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
)
|
||||
|
||||
companion object {
|
||||
/** Cross-session concepts live on their own stream, out of any user session's replay. */
|
||||
val SYSTEM_SESSION = SessionId("__concept_compiler__")
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.correx.apps.server.concept
|
||||
|
||||
import com.correx.core.events.events.ConceptPromotedEvent
|
||||
import com.correx.core.events.events.EventMetadata
|
||||
import com.correx.core.events.events.EventPayload
|
||||
import com.correx.core.events.events.NewEvent
|
||||
import com.correx.core.events.events.RetryAttemptedEvent
|
||||
import com.correx.core.events.events.StageCompletedEvent
|
||||
import com.correx.core.events.stores.EventStore
|
||||
import com.correx.core.events.types.EventId
|
||||
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.inference.Embedder
|
||||
import com.correx.core.talkie.l3.InMemoryL3MemoryStore
|
||||
import com.correx.core.talkie.l3.L3Query
|
||||
import com.correx.infrastructure.persistence.InMemoryEventStore
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.datetime.Clock
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.UUID
|
||||
|
||||
private class ConstantEmbedder(override val dimension: Int = 8) : Embedder {
|
||||
override suspend fun embed(text: String): FloatArray = FloatArray(dimension) { 1f }
|
||||
}
|
||||
|
||||
class ConceptCompilerServiceTest {
|
||||
|
||||
private val store = InMemoryEventStore()
|
||||
private val l3 = InMemoryL3MemoryStore()
|
||||
private val service = ConceptCompilerService(store, ConstantEmbedder(), l3, threshold = 3)
|
||||
|
||||
private suspend fun append(payload: EventPayload, session: String) {
|
||||
store.append(
|
||||
NewEvent(
|
||||
metadata = EventMetadata(
|
||||
eventId = EventId(UUID.randomUUID().toString()),
|
||||
sessionId = SessionId(session),
|
||||
timestamp = Clock.System.now(),
|
||||
schemaVersion = 1,
|
||||
causationId = null,
|
||||
correlationId = null,
|
||||
),
|
||||
payload = payload,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun validatedFix(fp: String, session: String) {
|
||||
append(
|
||||
RetryAttemptedEvent(
|
||||
sessionId = SessionId(session),
|
||||
stageId = StageId("impl"),
|
||||
attemptNumber = 1,
|
||||
maxAttempts = 3,
|
||||
failureReason = "detekt: MagicNumber\ntrace",
|
||||
gate = "lint",
|
||||
fingerprint = fp,
|
||||
),
|
||||
session,
|
||||
)
|
||||
append(StageCompletedEvent(SessionId(session), StageId("impl"), TransitionId("t")), session)
|
||||
}
|
||||
|
||||
private suspend fun promotedEvents(): List<ConceptPromotedEvent> =
|
||||
store.allEvents().mapNotNull { it.payload as? ConceptPromotedEvent }.toList()
|
||||
|
||||
@Test
|
||||
fun `promotes a fingerprint that crossed the threshold and injects it into L3`(): Unit = runBlocking {
|
||||
validatedFix("fp1", "sa")
|
||||
validatedFix("fp1", "sb")
|
||||
validatedFix("fp1", "sc")
|
||||
|
||||
service.runOnce()
|
||||
|
||||
val promoted = promotedEvents()
|
||||
assertEquals(1, promoted.size)
|
||||
assertEquals("fp1", promoted.single().fingerprint)
|
||||
assertEquals(3, promoted.single().occurrences)
|
||||
|
||||
val hits = l3.query(L3Query(vector = FloatArray(8) { 1f }, k = 5))
|
||||
assertTrue(hits.any { it.entry.turnId == "concept:fp1" }) { "concept not injected into L3" }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `does not promote below the threshold`(): Unit = runBlocking {
|
||||
validatedFix("fp1", "sa")
|
||||
validatedFix("fp1", "sb")
|
||||
|
||||
service.runOnce()
|
||||
|
||||
assertTrue(promotedEvents().isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `repeated runs do not re-promote the same fingerprint`(): Unit = runBlocking {
|
||||
validatedFix("fp1", "sa")
|
||||
validatedFix("fp1", "sb")
|
||||
validatedFix("fp1", "sc")
|
||||
|
||||
service.runOnce()
|
||||
service.runOnce()
|
||||
|
||||
assertEquals(1, promotedEvents().size)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user