@@ -360,6 +360,30 @@ where
360360 if let Some ( root) = request. workspace . root . as_deref ( ) {
361361 request. executor . remap_workspace_root ( root) ;
362362 }
363+ let run_id = self . run_id . as_deref ( ) . unwrap_or ( & plan. plan_id ) ;
364+ let scratch = match crate :: core:: controller_scratch:: allocate_attempt (
365+ run_id,
366+ & plan. plan_id ,
367+ & request. task_id ,
368+ scheduled. attempt ,
369+ ) {
370+ Ok ( scratch) => scratch,
371+ Err ( error) => {
372+ let outcome =
373+ scratch_allocation_failure ( task_id. clone ( ) , error. to_string ( ) ) ;
374+ events. push ( event (
375+ & task_id,
376+ AgentTaskState :: Failed ,
377+ scheduled. attempt ,
378+ outcome. summary . clone ( ) ,
379+ ) ) ;
380+ record_completed_outcome ( & mut completed_by_task, & mut outcomes, outcome) ;
381+ continue ;
382+ }
383+ } ;
384+ request
385+ . executor
386+ . set_runtime_tmpdir ( scratch. path . to_string_lossy ( ) . as_ref ( ) ) ;
363387 let executor_key = executor_key ( & request) ;
364388 let executor = Arc :: clone ( & self . executor ) ;
365389 let plan_id = plan. plan_id . clone ( ) ;
@@ -415,6 +439,7 @@ where
415439 artifact_nonce : uuid:: Uuid :: new_v4 ( ) . to_string ( ) ,
416440 task_base_sha,
417441 source_provenance : harvest_preflight. source_provenance ,
442+ scratch : scratch. clone ( ) ,
418443 } ) ;
419444
420445 thread:: spawn ( move || {
@@ -424,6 +449,7 @@ where
424449 task_id,
425450 attempt,
426451 outcome,
452+ scratch,
427453 } ) ) ;
428454 } ) ;
429455 }
@@ -468,6 +494,7 @@ where
468494 let Some ( running_task) = running_task else {
469495 continue ;
470496 } ;
497+ debug_assert_eq ! ( result. scratch, running_task. scratch) ;
471498 let mut outcome = result. outcome ;
472499 if let Err ( error) = harvest_uncommitted_patch ( & mut outcome, & running_task)
473500 . and_then ( |_| harvest_committed_patch ( & mut outcome, & running_task) )
@@ -529,6 +556,7 @@ where
529556 retry_budget_used,
530557 & plan. options . retry . retryable_failure_classifications ,
531558 ) {
559+ release_scratch ( & result. scratch , "retry" , & outcome) ;
532560 retry_budget_used += 1 ;
533561 let retry_evidence = retry_attempt_evidence ( & outcome, & running_task) ;
534562 let mut retry_attempts = running_task. retry_attempts ;
@@ -573,6 +601,7 @@ where
573601 max_attempts,
574602 execution_budget. max_provider_rotations ,
575603 ) {
604+ release_scratch ( & result. scratch , "provider_rotation" , & outcome) ;
576605 let mut rotation_attempts = running_task. rotation_attempts ;
577606 rotation_attempts. push (
578607 AgentTaskScheduleSupport :: rotation_attempt_record (
@@ -651,6 +680,15 @@ where
651680 result. attempt ,
652681 running_task. rotation_index ,
653682 ) ;
683+ release_scratch (
684+ & result. scratch ,
685+ if running_task. timeout_cancel_requested {
686+ "scheduler_timeout_completion"
687+ } else {
688+ terminal_reason ( & outcome, cancellation. is_cancelled ( ) )
689+ } ,
690+ & outcome,
691+ ) ;
654692 record_completed_outcome ( & mut completed_by_task, & mut outcomes, outcome) ;
655693 }
656694 Err ( Some ( mpsc:: RecvTimeoutError :: Timeout ) ) => { }
@@ -743,6 +781,7 @@ pub(super) struct RunningTask {
743781 pub ( super ) task_base_sha : Option < String > ,
744782 /// Verified source identity for snapshot-backed candidate artifacts.
745783 pub ( super ) source_provenance : Option < serde_json:: Value > ,
784+ pub ( super ) scratch : crate :: core:: controller_scratch:: ControllerScratchAllocation ,
746785}
747786
748787#[ derive( Debug , Clone ) ]
@@ -760,6 +799,7 @@ struct TaskResult {
760799 task_id : String ,
761800 attempt : u32 ,
762801 outcome : AgentTaskOutcome ,
802+ scratch : crate :: core:: controller_scratch:: ControllerScratchAllocation ,
763803}
764804
765805fn retry_attempt_evidence ( outcome : & AgentTaskOutcome , running : & RunningTask ) -> serde_json:: Value {
@@ -774,6 +814,56 @@ fn retry_attempt_evidence(outcome: &AgentTaskOutcome, running: &RunningTask) ->
774814 } )
775815}
776816
817+ fn release_scratch (
818+ allocation : & crate :: core:: controller_scratch:: ControllerScratchAllocation ,
819+ reason : & str ,
820+ outcome : & AgentTaskOutcome ,
821+ ) {
822+ let evidence = serde_json:: json!( {
823+ "task_id" : outcome. task_id,
824+ "status" : outcome. status,
825+ "outcome" : outcome,
826+ } ) ;
827+ let _ = crate :: core:: controller_scratch:: release_attempt ( allocation, reason, evidence) ;
828+ }
829+
830+ fn terminal_reason ( outcome : & AgentTaskOutcome , cancelled : bool ) -> & ' static str {
831+ if cancelled || outcome. status == AgentTaskOutcomeStatus :: Cancelled {
832+ "cancelled"
833+ } else if outcome. status == AgentTaskOutcomeStatus :: Timeout {
834+ "provider_timeout"
835+ } else if matches ! (
836+ outcome. status,
837+ AgentTaskOutcomeStatus :: Succeeded | AgentTaskOutcomeStatus :: NoOp
838+ ) {
839+ "succeeded"
840+ } else {
841+ "provider_failure"
842+ }
843+ }
844+
845+ fn scratch_allocation_failure ( task_id : String , error : String ) -> AgentTaskOutcome {
846+ AgentTaskOutcome {
847+ schema : AGENT_TASK_OUTCOME_SCHEMA . to_string ( ) ,
848+ task_id,
849+ status : AgentTaskOutcomeStatus :: Failed ,
850+ summary : Some ( format ! ( "could not allocate provider scratch root: {error}" ) ) ,
851+ failure_classification : Some ( AgentTaskFailureClassification :: ExecutionFailed ) ,
852+ artifacts : Vec :: new ( ) ,
853+ typed_artifacts : Vec :: new ( ) ,
854+ evidence_refs : Vec :: new ( ) ,
855+ diagnostics : vec ! [ AgentTaskDiagnostic {
856+ class: "agent_task.controller_scratch_allocation_failed" . to_string( ) ,
857+ message: error,
858+ data: serde_json:: Value :: Null ,
859+ } ] ,
860+ outputs : serde_json:: Value :: Null ,
861+ workflow : None ,
862+ follow_up : None ,
863+ metadata : serde_json:: Value :: Null ,
864+ }
865+ }
866+
777867fn reset_attempt_request (
778868 mut request : AgentTaskRequest ,
779869 source_workspace_root : Option < String > ,
0 commit comments