feat(context): emit ContextAssembled manifest event per stage build (#307)
Root cause: context-assembly (unconfirmedFixEntries, promotedConceptEntries,
buildRetryFeedbackEntry, etc.) computes ContextEntry lists at stage-build time
but never records what was actually injected into a turn. The only way to
answer "did hint X fire in session Y" was decoding the CAS prompt artifact and
grepping raw JSON — the event log alone couldn't say.
Scope check: no existing event carries this (ContextTruncatedEvent only
reports drop counts, not the injected set) — added new ContextAssembledEvent
rather than extending an existing one.
Fix: ContextAssembledEvent (core/events/events/ContextEvents.kt) records ONE
manifest per stage's initial ContextPack build — sessionId, stageId,
contextPackId, and a List<ContextManifestEntry> of {sourceType, sourceId,
tokenEstimate, layer, role} pulled from the pack's actual (post-budget)
entries. No entry content — content stays a pure derived projection of the
event log (invariant #9/#6) and already lives in CAS. Registered in
eventModule (Serialization.kt). Emitted via
SessionOrchestrator.emitContextAssembled (SessionOrchestratorRepoContext.kt),
wired at the initial contextPackBuilder.build() call site in
SessionOrchestratorExecution.kt (mirrors emitContextTruncationIfNeeded).
Tests: ContextAssembledEventSerializationTest (round-trip + no-content-leak),
SessionOrchestratorIntegrationTest "stage context build emits
ContextAssembledEvent with a manifest (#307)".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgDL1v3GuQ9RZnYR6fDT95
This commit is contained in:
+1
@@ -304,6 +304,7 @@ internal suspend fun SessionOrchestrator.executeStage(
|
||||
return StageExecutionResult.Failure(e.message ?: "required context overflow", retryable = false)
|
||||
}
|
||||
emitContextTruncationIfNeeded(sessionId, stageId, contextPack)
|
||||
emitContextAssembled(sessionId, stageId, contextPack)
|
||||
|
||||
var currentContext = contextPack
|
||||
var inferenceResult = runInference(
|
||||
|
||||
+31
@@ -2,6 +2,8 @@ package com.correx.core.kernel.orchestration
|
||||
|
||||
import com.correx.core.context.model.ContextPack
|
||||
import com.correx.core.events.events.ApprovalRequestedEvent
|
||||
import com.correx.core.events.events.ContextAssembledEvent
|
||||
import com.correx.core.events.events.ContextManifestEntry
|
||||
import com.correx.core.events.events.ContextTruncatedEvent
|
||||
import com.correx.core.events.events.InitialIntentEvent
|
||||
import com.correx.core.events.events.OrchestrationPausedEvent
|
||||
@@ -78,6 +80,35 @@ internal suspend fun SessionOrchestrator.emitContextTruncationIfNeeded(
|
||||
)
|
||||
}
|
||||
|
||||
// #307: record the manifest of what got injected into the stage's initial context build — NOT
|
||||
// the content (that's derivable/replayable, invariant #9, and already in CAS via the prompt
|
||||
// artifact) — so the injected set is auditable from the event log alone.
|
||||
internal suspend fun SessionOrchestrator.emitContextAssembled(
|
||||
sessionId: SessionId,
|
||||
stageId: StageId,
|
||||
contextPack: ContextPack,
|
||||
) {
|
||||
val entries = contextPack.layers.values.flatten().map { entry ->
|
||||
ContextManifestEntry(
|
||||
sourceType = entry.sourceType,
|
||||
sourceId = entry.sourceId,
|
||||
tokenEstimate = entry.tokenEstimate,
|
||||
layer = entry.layer.name,
|
||||
role = entry.role.name,
|
||||
)
|
||||
}
|
||||
emit(
|
||||
sessionId,
|
||||
ContextAssembledEvent(
|
||||
sessionId = sessionId,
|
||||
stageId = stageId,
|
||||
contextPackId = contextPack.id.value,
|
||||
entries = entries,
|
||||
timestampMs = Clock.System.now().toEpochMilliseconds(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun SessionOrchestrator.fallbackTokenEstimate(content: String): Int {
|
||||
return (content.length / 4).coerceAtLeast(1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user