diff --git a/core/events/src/main/kotlin/com/correx/core/events/execution/RetryPolicy.kt b/core/events/src/main/kotlin/com/correx/core/events/execution/RetryPolicy.kt index ef5fa730..a2744e20 100644 --- a/core/events/src/main/kotlin/com/correx/core/events/execution/RetryPolicy.kt +++ b/core/events/src/main/kotlin/com/correx/core/events/execution/RetryPolicy.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class RetryPolicy( val maxAttempts: Int, - val backoffMs: Long = 0L, + val backoffMs: Long = 1_000L, ) { init { require(maxAttempts >= 1) { "maxAttempts must be >= 1" } 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 6faddc86..441a4da4 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 @@ -6,10 +6,32 @@ package com.correx.core.kernel.orchestration 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.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.sessions.BoundProjectProfile import java.util.UUID +fun buildRetryFeedbackEntry(events: List, stageId: StageId): ContextEntry? { + val latest = events + .mapNotNull { it.payload as? RetryAttemptedEvent } + .lastOrNull { it.stageId == stageId } ?: return null + val content = "## Retry feedback\n" + + "Attempt ${latest.attemptNumber} of ${latest.maxAttempts} for stage '${stageId.value}'. " + + "The previous attempt failed: ${latest.failureReason}\n" + + "Address the failure cause directly. Do not repeat the identical approach." + return ContextEntry( + id = ContextEntryId(UUID.randomUUID().toString()), + layer = ContextLayer.L1, + content = content, + sourceType = "retryFeedback", + sourceId = stageId.value, + 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/SessionOrchestrator.kt b/core/kernel/src/main/kotlin/com/correx/core/kernel/orchestration/SessionOrchestrator.kt index 657b4172..e7d0e93f 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 @@ -365,9 +365,11 @@ abstract class SessionOrchestrator( } ?: emptyList() val projectProfileEntries = session.state.boundProjectProfile ?.let { listOf(buildProjectProfileEntry(it)) } ?: emptyList() + val retryFeedbackEntries = buildRetryFeedbackEntry(eventStore.read(sessionId), stageId) + ?.let { listOf(it) } ?: emptyList() var accumulatedEntries = systemPrompt + profileEntries + projectProfileEntries + journalEntries + repoMapEntries + - needsEntries + schemaEntries + promptEntries + steeringEntries + needsEntries + schemaEntries + promptEntries + steeringEntries + retryFeedbackEntries val contextPack = contextPackBuilder.build( id = ContextPackId(UUID.randomUUID().toString()), sessionId = sessionId, diff --git a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/RetryPolicyTest.kt b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/RetryPolicyTest.kt index cb5255d2..910ff879 100644 --- a/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/RetryPolicyTest.kt +++ b/testing/contracts/src/test/kotlin/com/correx/testing/contracts/model/orchestration/RetryPolicyTest.kt @@ -28,9 +28,9 @@ class RetryPolicyTest { } @Test - fun `backoffMs defaults to zero`() { + fun `backoffMs defaults to one second`() { val policy = RetryPolicy(maxAttempts = 1) - assertEquals(0L, policy.backoffMs) + assertEquals(1_000L, policy.backoffMs) } @Test diff --git a/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt b/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt index e059a75d..ca90a89d 100644 --- a/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt +++ b/testing/kernel/src/test/kotlin/ContextFeedbackTest.kt @@ -1,13 +1,47 @@ import com.correx.core.context.model.ContextLayer import com.correx.core.context.model.EntryRole +import com.correx.core.events.events.RetryAttemptedEvent +import com.correx.core.events.types.SessionId +import com.correx.core.events.types.StageId import com.correx.core.kernel.orchestration.buildProjectProfileEntry +import com.correx.core.kernel.orchestration.buildRetryFeedbackEntry import com.correx.core.sessions.BoundProjectProfile +import com.correx.testing.fixtures.EventFixtures.stored import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test class ContextFeedbackTest { + @Test + fun `no retry events yields null`() { + assertNull(buildRetryFeedbackEntry(emptyList(), StageId("impl"))) + } + + @Test + fun `latest retry event for the stage becomes a labeled L1 system entry`() { + val events = listOf( + stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("impl"), 1, 3, "old reason")), + stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("other"), 1, 3, "other stage")), + stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("impl"), 2, 3, "tests failed: NPE in FooTest")), + ) + val entry = buildRetryFeedbackEntry(events, StageId("impl"))!! + assertEquals("retryFeedback", entry.sourceType) + assertEquals(ContextLayer.L1, entry.layer) + assertEquals(EntryRole.SYSTEM, entry.role) + assertTrue(entry.content.contains("Attempt 2 of 3"), "content: ${entry.content}") + assertTrue(entry.content.contains("tests failed: NPE in FooTest"), "content: ${entry.content}") + } + + @Test + fun `retry entry for other stage is not returned for queried stage`() { + val events = listOf( + stored(payload = RetryAttemptedEvent(SessionId("s"), StageId("other"), 1, 3, "reason")), + ) + assertNull(buildRetryFeedbackEntry(events, StageId("impl"))) + } + @Test fun `project profile renders as single L0 entry`() { val entry = buildProjectProfileEntry(