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
@@ -73,8 +73,12 @@ data class StageConfig(
get() = if (allowedTools.isEmpty()) allowedTools else allowedTools + ALWAYS_AVAILABLE_READ_TOOLS
companion object {
/** Read-only tools every tool-granting stage may call regardless of its declared set. */
/** Read-only tools every tool-granting stage may call regardless of its declared set.
* `tool_output` is deliberately NOT here — it can only retrieve output that has actually been
* spilled to CAS, so the orchestrator adds it on demand (once a spill has occurred) rather than
* advertising a hash-eating tool to every stage that will never spill (context noise that nudges
* models into reasoning about opaque hashes). */
val ALWAYS_AVAILABLE_READ_TOOLS: Set<String> =
setOf("file_read", "list_dir", "glob", "grep", "tool_output")
setOf("file_read", "list_dir", "glob", "grep")
}
}