feat(context): purify leading SYSTEM, route context layers as USER turns (#290)

Move intent, repo map, docs catalog, decision journal and relevant-files
context entries to EntryRole.USER so they no longer fold into the single
leading SYSTEM block — that block stays pure policy/schema. Omit L3 repo-map
retrieval on repair retries (the transcript already carries the evidence).
Add "initialIntent" to REQUIRED_SOURCE_TYPES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 18:35:29 +04:00
parent bc050f8e8a
commit 3bf4dd7379
7 changed files with 57 additions and 13 deletions
@@ -252,7 +252,8 @@ fun buildRelevantFilesEntry(hits: List<RepoKnowledgeHit>): ContextEntry {
sourceType = "relevantFiles",
sourceId = "repo-knowledge",
tokenEstimate = content.length / 4,
role = EntryRole.SYSTEM,
// #290: semantic retrieval hits are L3 reference — USER role, not folded into leading system.
role = EntryRole.USER,
)
}
@@ -148,6 +148,9 @@ internal val REQUIRED_SOURCE_TYPES = setOf(
"retryFeedback",
"neededArtifact",
"criticFeedback",
// #290: original intent stays unprunable via the REQUIRED bucket now that it renders as
// L1/USER instead of relying on the old L0/SYSTEM never-drop placement.
"initialIntent",
)
// HTTP statuses that are transient despite being 4xx (F-002 retry classification).
@@ -161,12 +161,15 @@ internal suspend fun SessionOrchestrator.buildIntentEntry(sessionId: SessionId):
return listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
// #290: the original intent is the operator's request, not Correx policy — render it as
// L1/USER (not folded into leading system). It stays unprunable via the REQUIRED bucket
// (initialIntent ∈ REQUIRED_SOURCE_TYPES), so retention no longer rides on L0/SYSTEM.
layer = ContextLayer.L1,
content = content,
sourceType = "initialIntent",
sourceId = sessionId.value,
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
role = EntryRole.USER,
),
)
}
@@ -290,7 +293,8 @@ internal suspend fun SessionOrchestrator.buildRepoMapEntries(sessionId: SessionI
sourceType = "repoMap",
sourceId = "repo-map",
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
// #290: L3 retrieval is reference, not policy — USER role so it stays out of leading system.
role = EntryRole.USER,
),
)
}
@@ -376,7 +380,8 @@ internal suspend fun SessionOrchestrator.buildDocsCatalogEntry(sessionId: Sessio
sourceType = "docsCatalog",
sourceId = "docs-catalog",
tokenEstimate = estimateTokens(content),
role = EntryRole.SYSTEM,
// #290: docs catalog is L3 reference — USER role so it stays out of leading system.
role = EntryRole.USER,
),
)
}
@@ -188,16 +188,26 @@ internal suspend fun SessionOrchestrator.executeStage(
val journalEntries = if (journalText.isBlank()) emptyList() else listOf(
ContextEntry(
id = ContextEntryId(UUID.randomUUID().toString()),
layer = ContextLayer.L0,
// #290: the decision journal is L3/USER reference (OPTIONAL, ceiling-capped), not policy —
// keep it out of the leading system block that must hold policy/schema/constraints only.
layer = ContextLayer.L3,
content = journalText,
sourceType = "decisionJournal",
sourceId = "decision-journal",
tokenEstimate = estimateTokens(journalText),
role = EntryRole.SYSTEM,
role = EntryRole.USER,
),
)
val repoMapEntries = buildContextualRepoEntries(sessionId, stageId, stageConfig)
val sessionEvents = eventStore.read(sessionId)
// #290 (acceptance #5): on a gate-repair retry the model must patch the named failure against
// the recorded images, not go re-exploring — so omit the L3 retrieval bundle (repo map, docs
// catalog, semantic hits) on repair turns. Detected by a pending retry mandate for this stage.
val isRepairRetry = buildRetryFeedbackEntry(sessionEvents, stageId) != null
val repoMapEntries = if (isRepairRetry) {
emptyList()
} else {
buildContextualRepoEntries(sessionId, stageId, stageConfig)
}
val criticIds = criticArtifactIds(sessionEvents, graph, stageId)
val criticFrom = sessionEvents
.mapNotNull { it.payload as? RefinementIterationEvent }