fix(orchestration): break the no-op-write loop + un-rot retry feedback

A stage (configure_ethos_theme) looped forever re-writing an identical
postcss.config.js: file_write returned "written successfully" on a byte-
identical write, and the retry-feedback entry that says WHY it failed got
truncated out of context every turn, so the model cold-started on the same
wrong idea. CAS hashes leaked into its face from two sides too.

- SandboxedToolExecutor: a write whose result is byte-identical to disk now
  returns "No change: … nothing was written" (+ noop=true) instead of a false
  success signal. Uses the pre-image hashes already captured for reversibility.
- DefaultContextPackBuilder: pin "retryFeedback" in neverDropSourceTypes so the
  failure reason + already-written-files list survives truncation.
- ContextFeedback: drop the opaque "— CAS <hash>" from the retry entry; path only.
- tool_output withheld until the session actually spills over-cap output to CAS
  (StageConfig.ALWAYS_AVAILABLE_READ_TOOLS -> on-demand via sessionHasSpilledOutput),
  so a hash-eating tool isn't advertised to every stage that never spills.
- Wrap 10 long lines in kernel to bring detekt back under maxIssues (99 -> 89).

Tests: infrastructure:tools (+2 no-op cases), core:{transitions,context,kernel} green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 15:22:39 +04:00
parent 8a60778ca7
commit f08a784432
9 changed files with 120 additions and 18 deletions
@@ -57,7 +57,9 @@ class DefaultContextPackBuilder(
// "remainingDelta" is the shrinking stage-contract checklist (stage-termination design
// 2026-07-11): it must survive every budget/dedup pass, since its entire purpose is to give
// the model a progress signal that outlives the truncation which otherwise wipes its memory.
private val neverDropSourceTypes = setOf("steeringNote", "eventHistory", "factSheet", "remainingDelta")
// "retryFeedback" carries WHY the last attempt failed + the files already written this stage; if
// truncation evicts it the model cold-starts on the same wrong idea every turn (the write-loop rot).
private val neverDropSourceTypes = setOf("steeringNote", "eventHistory", "factSheet", "remainingDelta", "retryFeedback")
private companion object {
const val CHARS_PER_TOKEN = 4