feat(context): overlay sibling-stage writes onto repo retrieval

Files written by earlier stages this session aren't in the session-start
repo map and were never embedded, so semantic retrieval is structurally
blind to them. Overlay each stage's FileWrittenEvent post-images as
deterministic hits (score 1.0) leading the semantic hits, deduped by path
— no reindex, no embed. Descriptors derive via the comment-free
sourcedesc describe() so agent-written content can't inject prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 14:38:34 +04:00
parent a32784bfd8
commit 82a4395f34
3 changed files with 121 additions and 7 deletions
@@ -10,6 +10,7 @@ import com.correx.core.context.model.TokenBudget
import com.correx.core.context.model.EntryRole
import com.correx.core.events.events.ArtifactContentStoredEvent
import com.correx.core.events.events.FileWrittenEvent
import com.correx.core.events.events.RepoKnowledgeHit
import com.correx.core.events.events.ArtifactCreatedEvent
import com.correx.core.events.events.ArtifactRepairAttemptedEvent
import com.correx.core.events.events.ArtifactRepairFailedEvent
@@ -227,6 +228,40 @@ internal suspend fun SessionOrchestrator.fileWrittenManifest(sessionId: SessionI
}.trimEnd()
}
/**
* Files written earlier THIS session by OTHER stages, projected as deterministic retrieval hits so
* a successor stage discovers sibling output that the session-start repo map (a snapshot) can never
* contain and the embedder can't rank (it was never indexed). Latest post-image per path, capped,
* most-recent first; each carries the comment-free [describe] descriptor over its recorded CAS bytes
* (untrusted navigation; CAS hash authoritative). Score 1.0 — this is deterministic grounding, not
* cosine similarity — so it survives the retriever's floor and the useful-hit filter. The current
* stage's own writes are excluded (retry-repair state and the file-written manifest cover those).
* Pure projection over recorded events + CAS, replay-safe.
*/
internal suspend fun SessionOrchestrator.sessionWrittenHits(
sessionId: SessionId,
currentStageId: StageId,
): List<RepoKnowledgeHit> {
val events = eventStore.read(sessionId)
val invToStage = events.mapNotNull { it.payload as? ToolInvocationRequestedEvent }
.associate { it.invocationId to it.stageId }
val latestWrites = events.mapNotNull { it.payload as? FileWrittenEvent }
.filter { it.postImageHash != null && invToStage[it.invocationId].let { s -> s != null && s != currentStageId } }
.groupBy { it.path }
.mapValues { (_, writes) -> writes.last() }
if (latestWrites.isEmpty()) return emptyList()
return latestWrites.values
.sortedByDescending { it.timestampMs }
.take(tuning.repoMapInjectTopK)
.mapNotNull { write ->
val hash = write.postImageHash ?: return@mapNotNull null
val descriptor = artifactStore.get(ArtifactId(hash))?.let { describe(write.path, it).render() }
?.takeIf { it.isNotBlank() }
val text = if (descriptor != null) "${write.path}: $descriptor" else write.path
RepoKnowledgeHit(path = write.path, text = text, score = 1.0f)
}
}
internal suspend fun SessionOrchestrator.emitStageCheckpoint(
sessionId: SessionId,
stageId: StageId,
@@ -320,14 +320,23 @@ internal suspend fun SessionOrchestrator.buildContextualRepoEntries(
return repoEntriesOrMapFloor(sessionId, recorded.hits, stagePrompt) + buildDocsCatalogEntry(sessionId)
}
// Sibling-stage output written earlier this session isn't in the session-start repo map and was
// never embedded, so semantic retrieval is structurally blind to it. Overlay it as deterministic
// hits (score 1.0) that lead the semantic hits, deduped by path — no reindex, no embed.
val writtenHits = sessionWrittenHits(sessionId, stageId)
val retriever = repoKnowledgeRetriever
?: return buildRepoMapEntries(sessionId, stagePrompt) + buildDocsCatalogEntry(sessionId)
val hits = runCatching { retriever.retrieve(sessionId, stagePrompt, tuning.repoMapInjectTopK) }
.getOrElse { e ->
if (e is CancellationException) throw e
log.warn("repo-knowledge retrieval failed for stage {}: {}", stageId.value, e.message)
emptyList()
}
val semanticHits = if (retriever == null) {
emptyList()
} else {
runCatching { retriever.retrieve(sessionId, stagePrompt, tuning.repoMapInjectTopK) }
.getOrElse { e ->
if (e is CancellationException) throw e
log.warn("repo-knowledge retrieval failed for stage {}: {}", stageId.value, e.message)
emptyList()
}
}
val writtenPaths = writtenHits.mapTo(mutableSetOf()) { it.path }
val hits = writtenHits + semanticHits.filterNot { it.path in writtenPaths }
// Record the raw retrieval for audit (a degenerate all-zero result is itself the signal that
// the embedder is noop / the index is empty), but only useful hits ever reach the agent.
if (hits.isNotEmpty()) emit(sessionId, RepoKnowledgeRetrievedEvent(sessionId, stageId, stagePrompt, hits))