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:
2026-07-05 18:04:49 +04:00
parent f90f2ab39d
commit 33ea44d8cc
12 changed files with 545 additions and 40 deletions
@@ -423,6 +423,28 @@ class SessionOrchestratorIntegrationTest {
val sessionId = SessionId("s-trunc")
val config = OrchestrationConfig(retryPolicy = RetryPolicy(maxAttempts = 1, backoffMs = 0))
// Seed a recorded repo map so the stage context carries an OPTIONAL entry the dropping
// compressor can discard: required entries (like the system prompt) are pinned by the
// bucket split and never reach the compressor.
eventStore.append(
com.correx.core.events.events.NewEvent(
com.correx.core.events.events.EventMetadata(
com.correx.core.events.types.EventId("rm-trunc"),
sessionId,
kotlinx.datetime.Instant.parse("2026-06-04T00:00:00Z"),
1,
null,
null,
),
com.correx.core.events.events.RepoMapComputedEvent(
sessionId,
"/repo",
listOf(com.correx.core.events.events.RepoMapEntry("src/Main.kt", 0.9)),
kotlinx.datetime.Instant.parse("2026-06-04T00:00:00Z"),
),
),
)
truncOrchestrator.run(sessionId, truncGraph, config)
val truncated = eventStore.read(sessionId).mapNotNull { it.payload as? ContextTruncatedEvent }