feat(context): required/optional bucket split, budget ceilings, grounded handoffs
Context rework from the 2026-07-05 context-poisoning diagnosis (session 6ca236c4 read-loop): - ContextBucket on ContextEntry: REQUIRED (task, constraints, needed artifacts, failure feedback) is never pruned/compressed/dropped; the build fails fast (RequiredContextOverflowException -> non-retryable stage failure) if required alone exceeds the stage budget. - OptionalCeilings: per-sourceType token caps for the optional block (repoMap 1200, relevantFiles 1600, journal 800, profiles 400/600, vocabulary 400), tuned for the 9-26B tier at 16k; enforced by truncation (journal keeps its tail, others their head). - Repo map is now a structural 'what exists' listing (alphabetical, per-directory, no recency ranking, no symbols); .md/docs paths are gated behind an explicit docs mention in the stage prompt — retrieval hits remain the only other path for docs to enter context. - file_written needs render as a manifest of ALL files the producing stage wrote (projected from ArtifactContentStored -> ToolInvocation -> FileWritten events), fixing the last-write-wins collapse; content stays lazy behind file_read. - Decision journal folds a stage's RETRY records into one summary line once a later transition leaves that stage (render-time only). - Dedupe retrieved-file lines in buildRelevantFilesEntry.
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package com.correx.core.journal
|
||||
|
||||
import com.correx.core.journal.model.DecisionJournalState
|
||||
import com.correx.core.journal.model.DecisionKind
|
||||
import com.correx.core.journal.model.DecisionRecord
|
||||
|
||||
class DecisionJournalRenderer(private val keepLast: Int = 40) {
|
||||
fun render(state: DecisionJournalState, summaryText: String? = null): String {
|
||||
if (state.records.isEmpty() && summaryText == null) return ""
|
||||
val visible = foldResolvedRetries(state.records)
|
||||
return buildString {
|
||||
appendLine("## Decision history (chronological — ground truth)")
|
||||
if (summaryText != null) {
|
||||
@@ -13,7 +16,34 @@ class DecisionJournalRenderer(private val keepLast: Int = 40) {
|
||||
appendLine("+${state.lowSalienceOmittedCount} routine transitions/retries omitted")
|
||||
}
|
||||
}
|
||||
state.records.takeLast(keepLast).forEach { appendLine("- [${it.kind}] ${it.summary}") }
|
||||
visible.takeLast(keepLast).forEach { appendLine("- [${it.kind}] ${it.summary}") }
|
||||
}.trimEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* A RETRY record is resolved once a later TRANSITION leaves its stage — the failure was
|
||||
* overcome and its details are noise for every subsequent stage. Fold each resolved stage's
|
||||
* retries into one summary line at the position of the first retry; unresolved (still-live)
|
||||
* retries render verbatim. Render-time only: the journal state and event log are untouched.
|
||||
*/
|
||||
private fun foldResolvedRetries(records: List<DecisionRecord>): List<DecisionRecord> {
|
||||
val resolvedAt = records
|
||||
.filter { it.kind == DecisionKind.TRANSITION && it.fromStageId != null }
|
||||
.groupBy { it.fromStageId!! }
|
||||
.mapValues { (_, transitions) -> transitions.maxOf { it.sequence } }
|
||||
val foldedStages = mutableSetOf<String>()
|
||||
return records.mapNotNull { record ->
|
||||
val stage = record.stageId
|
||||
val resolution = stage?.let { resolvedAt[it] }
|
||||
if (record.kind != DecisionKind.RETRY || resolution == null || record.sequence > resolution) {
|
||||
return@mapNotNull record
|
||||
}
|
||||
if (stage in foldedStages) return@mapNotNull null
|
||||
foldedStages += stage
|
||||
val count = records.count {
|
||||
it.kind == DecisionKind.RETRY && it.stageId == stage && it.sequence <= resolution
|
||||
}
|
||||
record.copy(summary = "$stage: $count retr${if (count == 1) "y" else "ies"}, resolved — stage completed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ class DefaultDecisionJournalReducer : DecisionJournalReducer {
|
||||
kind = DecisionKind.TRANSITION,
|
||||
summary = "Advanced ${p.from.value} → ${p.to.value} (${p.transitionId.value})",
|
||||
stageId = p.to.value,
|
||||
fromStageId = p.from.value,
|
||||
)
|
||||
is RetryAttemptedEvent -> DecisionRecord(
|
||||
sequence = event.sessionSequence,
|
||||
|
||||
@@ -25,4 +25,8 @@ data class DecisionRecord(
|
||||
val kind: DecisionKind,
|
||||
val summary: String,
|
||||
val stageId: String? = null,
|
||||
// TRANSITION only: the stage the workflow advanced FROM. Lets the renderer tell that a
|
||||
// stage's earlier RETRY records are resolved (the stage was eventually left successfully)
|
||||
// and fold them to a single summary line.
|
||||
val fromStageId: String? = null,
|
||||
)
|
||||
|
||||
@@ -77,4 +77,44 @@ class DecisionJournalRendererTest {
|
||||
assertTrue(result.contains("+2 routine transitions/retries omitted"))
|
||||
assertFalse(result.contains("- ["))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `resolved retries fold to a single summary line`() {
|
||||
val state = DecisionJournalState(
|
||||
records = listOf(
|
||||
record(1, DecisionKind.RETRY, "Retry 1/3 of scaffold: did not produce").copy(stageId = "scaffold"),
|
||||
record(2, DecisionKind.RETRY, "Retry 2/3 of scaffold: did not produce").copy(stageId = "scaffold"),
|
||||
record(3, DecisionKind.TRANSITION, "Advanced scaffold → hook (t1)")
|
||||
.copy(stageId = "hook", fromStageId = "scaffold"),
|
||||
),
|
||||
)
|
||||
val result = renderer.render(state)
|
||||
assertFalse(result.contains("did not produce"), "resolved retry detail must not render")
|
||||
assertTrue(result.contains("- [RETRY] scaffold: 2 retries, resolved — stage completed"))
|
||||
assertTrue(result.contains("- [TRANSITION] Advanced scaffold → hook"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unresolved retries render verbatim`() {
|
||||
val state = DecisionJournalState(
|
||||
records = listOf(
|
||||
record(1, DecisionKind.RETRY, "Retry 1/3 of hook: did not produce").copy(stageId = "hook"),
|
||||
),
|
||||
)
|
||||
val result = renderer.render(state)
|
||||
assertTrue(result.contains("- [RETRY] Retry 1/3 of hook: did not produce"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `retries after the stage was last left are not folded`() {
|
||||
val state = DecisionJournalState(
|
||||
records = listOf(
|
||||
record(1, DecisionKind.TRANSITION, "Advanced scaffold → hook (t1)")
|
||||
.copy(stageId = "hook", fromStageId = "scaffold"),
|
||||
record(2, DecisionKind.RETRY, "Retry 1/3 of scaffold: regression").copy(stageId = "scaffold"),
|
||||
),
|
||||
)
|
||||
val result = renderer.render(state)
|
||||
assertTrue(result.contains("- [RETRY] Retry 1/3 of scaffold: regression"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user