feat(acr): Store 2 soft-confidence hints + Store 1 FileReadTool priming (#305)

Store 2 (Fixes): ConceptCompilerProjection already tracked the
unconfirmed(validatedFixes>=1)/falsified(contradicted)/confirmed(promoted)
lifecycle as data, but only confirmed (>=threshold) clusters were ever
delivered. unconfirmedFixEntries reactively matches the CURRENT retry's
classKey against that state and injects a soft one-shot hint (below
threshold) or steer-away (contradicted) before hard promotion — wired
into stage context build in SessionOrchestratorExecution.kt.

Store 3 (Plan-shapes): already shipped as SessionOrchestratorPlanPatterns.kt
(deterministic keyword-Jaccard over eventStore.allEvents(), no embedder
needed) — confirmed complete, no new code required.

FileReadTool hot-path extension to Store 1: a whole-file read now primes
the cross-session ObservationStore (when wired) the same way the write
path already does, so a later describeCached lookup for the same content
hash is warm even if the file was only read, not written, this session.
This commit is contained in:
2026-07-21 17:55:19 +04:00
parent 5df35879eb
commit 8806de1628
8 changed files with 114 additions and 5 deletions
@@ -4,8 +4,12 @@ import com.correx.core.context.model.ContextEntry
import com.correx.core.context.model.ContextLayer
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ConceptPromotedEvent
import com.correx.core.events.events.RetryAttemptedEvent
import com.correx.core.events.events.StoredEvent
import com.correx.core.events.types.ContextEntryId
import com.correx.core.events.types.StageId
import com.correx.core.kernel.concept.ConceptCompilerProjection
import com.correx.core.kernel.concept.conceptClassKey
import com.correx.core.transitions.graph.StageConfig
import java.util.UUID
@@ -66,4 +70,51 @@ internal suspend fun SessionOrchestrator.promotedConceptEntries(stageConfig: Sta
}
}
/**
* Store 2 fix-confidence lifecycle (docs/plans/2026-07-21-acr-knowledge-accretion.md §Store 2): a
* classKey below the hard-promotion threshold isn't silence — [ConceptCompilerProjection] already
* tracks it as `unconfirmed` (validatedFixes 1..threshold-1) or `falsified` (contradicted). Reactively
* matched against the CURRENT retry's own classKey (same normalization the compiler uses) and
* delivered as a soft, one-shot hint (or steer-away) BEFORE the cluster earns hard promotion. Once
* `classKey` is in [state.promoted][com.correx.core.kernel.concept.ConceptCompilerState.promoted] the
* hard-promoted delivery ([promotedConceptEntries]) already covers it, so this is skipped to avoid
* double delivery.
*/
internal suspend fun SessionOrchestrator.unconfirmedFixEntries(
sessionEvents: List<StoredEvent>,
stageId: StageId,
): List<ContextEntry> {
val latest = sessionEvents.mapNotNull { it.payload as? RetryAttemptedEvent }.lastOrNull { it.stageId == stageId }
val sig = latest?.failureReason?.lineSequence()?.firstOrNull()?.take(SIGNATURE_MAX)?.trim().orEmpty()
val classKey = latest?.let { conceptClassKey(it.gate, sig) }
val projection = ConceptCompilerProjection()
val state = eventStore.allEvents().fold(projection.initial(), projection::apply)
val cluster = classKey?.takeIf { it !in state.promoted }?.let { state.clusters[it] }
val content = when {
cluster == null -> null
cluster.contradicted ->
"## Steer away from a known dead end\nA prior attempt at this exact failure class " +
"(${cluster.signature}) was tried before and a later failure showed it did NOT hold. " +
"Do not repeat that approach — find a materially different fix."
cluster.validatedFixes >= 1 ->
"## Unconfirmed prior fix (validated ${cluster.validatedFixes}x, not yet settled)\n" +
"This failure class (${cluster.signature}) was resolved before" +
(cluster.fixPath?.let { " in `$it`" } ?: "") + " — worth trying first, but it hasn't " +
"recurred enough times across sessions to be a certain fix here. Verify it actually applies."
else -> null
} ?: return emptyList()
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L1,
content = content,
sourceType = "unconfirmedFix",
sourceId = classKey.orEmpty(),
tokenEstimate = estimateTokens(content),
role = EntryRole.USER,
),
)
}
private const val SIGNATURE_MAX = 200
private const val MAX_PROMOTED_CONCEPTS = 3
@@ -245,6 +245,9 @@ internal suspend fun SessionOrchestrator.executeStage(
?.let { listOf(it) } ?: emptyList()
val recoveryTicketEntries = buildRecoveryTicketEntry(sessionEvents, stageId)
?.let { listOf(it) } ?: emptyList()
// Store 2 soft-confidence lifecycle (below hard-promotion threshold): reactive hint/steer-away
// matched to THIS retry's own classKey, see unconfirmedFixEntries.
val unconfirmedFixHints = unconfirmedFixEntries(sessionEvents, stageId)
val vocabularyEntries = artifactKindRegistry
?.takeIf { stageConfig.metadata["injectArtifactKinds"] == "true" }
?.let { listOf(buildArtifactKindVocabularyEntry(it.list())) } ?: emptyList()
@@ -282,7 +285,7 @@ internal suspend fun SessionOrchestrator.executeStage(
journalEntries + repoMapEntries + claimedTaskEntries +
needsEntries + schemaEntries + vocabularyEntries + promptEntries + steeringEntries +
rejectionEntries + clarificationEntries + retryFeedbackEntries + groundingFeedbackEntries +
recoveryTicketEntries + remainingDeltaEntries,
recoveryTicketEntries + unconfirmedFixHints + remainingDeltaEntries,
)
val contextPack = runCatching {
contextPackBuilder.build(