feat(orchestration): escalate repeated scope/manifest write-block to user approval (#301)

Small models frequently fail to comply with the WRITE_SCOPE/PATH_OUTSIDE_MANIFEST
remediation ("add its path via task_update affected_paths") and instead thrash the
same out-of-scope write call turn after turn, burning the session without ever
completing. After `tuning.escalateScopeAfterN` (default 3) consecutive rejections
of the SAME path for a scope/manifest reason, the orchestrator now reaches back to
the FIRST rejected invocation of that path (its pristine, pre-degraded arguments),
and routes it through the existing approval/pause flow instead of rejecting again.
On approval, a new WriteScopeGrantedEvent widens the effective write manifest for
that path for the rest of the session (mirroring how OutsidePathAccessGrantedEvent
already widens out-of-workspace reads) and the first attempt's write is executed.
On denial, the call is rejected as before. escalateScopeAfterN = 0 preserves the
old hard-block-forever behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GeyGFXczJb8RUWGBKmkm6G
This commit is contained in:
kami
2026-07-21 02:22:09 +04:00
parent 0af2f200d8
commit 8cc418a381
5 changed files with 448 additions and 2 deletions
@@ -53,6 +53,21 @@ data class OutsidePathAccessGrantedEvent(
val path: String,
) : EventPayload
/**
* Records that the operator approved widening the write scope/manifest to admit [path], after
* the same path was rejected `escalate_scope_after_n` times in a row (small models frequently
* fail to comply with the WRITE_SCOPE/PATH_OUTSIDE_MANIFEST remediation and thrash instead —
* see #301). Folded the same way [OutsidePathAccessGrantedEvent] widens out-of-workspace reads:
* subsequent writes to this path this session are admitted without re-prompting.
*/
@Serializable
@SerialName("WriteScopeGranted")
data class WriteScopeGrantedEvent(
val sessionId: SessionId,
val stageId: StageId,
val path: String,
) : EventPayload
@Serializable
@SerialName("OrchestrationPaused")
data class OrchestrationPausedEvent(