@@ -90,6 +90,7 @@ import {
9090 resolveSubAgentDeadlineMs ,
9191 type ForcedStopReason ,
9292} from "./stop-policy.js" ;
93+ import { EMPTY_THRASH_STATE , nextThrashState , salvagePathsFromThrash } from "./thrash.js" ;
9394import { SubAgentDirector } from "./nudge-director.js" ;
9495import { assertTierMayMountFleetVerb } from "./authority.js" ;
9596import { createReadAgentTraceTool } from "./trace-tool.js" ;
@@ -268,6 +269,22 @@ function abortReasonText(signal: AbortSignal): string | undefined {
268269 return undefined ;
269270}
270271
272+ /**
273+ * Findings payload for cancel/deadline salvage. Prefer multi-turn accumulated
274+ * prose; fall back to the last turn-boundary text, then the in-flight cycle tail.
275+ */
276+ function salvageFindingsText (
277+ accumulatedProse : string ,
278+ lastPartialText : string ,
279+ abortedCycleText : string ,
280+ ) : string {
281+ const prior = accumulatedProse . trim ( ) ;
282+ if ( prior . length > 0 ) return prior ;
283+ const last = lastPartialText . trim ( ) ;
284+ if ( last . length > 0 ) return last ;
285+ return abortedCycleText . slice ( - 2000 ) ;
286+ }
287+
271288/**
272289 * Arm requireEvidence only for the critic director. Greybeard is also
273290 * intent=review and may spawn-only then envelope; that is not a fake
@@ -772,6 +789,12 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
772789 // transcript (which would interleave sub-agent text with the parent turn).
773790 const toolNamesUsed : string [ ] = [ ] ;
774791 let lastPartialText = "" ;
792+ // Accumulate assistant prose across turns (capped) so cancel/deadline
793+ // salvage Findings keep substantive mid-run text, not only the final cycle.
794+ const TURN_PROSE_CAP = 12_000 ;
795+ let accumulatedProse = "" ;
796+ // Thrash paths from tool.start so mid-tool cancel still lists files touched.
797+ let thrashState = EMPTY_THRASH_STATE ;
775798 // Watch the streamed text of the in-flight cycle so a salvage on
776799 // cancel/deadline has the cycle's tail as its payload, even though no
777800 // turn boundary has completed yet to carry it.
@@ -782,9 +805,27 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
782805 toolNamesUsed . push ( name ) ;
783806 params . onProgress ?.( { description : params . description , toolName : name } ) ;
784807 }
808+ if ( event . type === "tool.start" ) {
809+ const call = ( event as { data ?: { call ?: { name ?: unknown ; arguments ?: unknown } } } ) . data
810+ ?. call ;
811+ if ( typeof call ?. name === "string" && call . name . length > 0 ) {
812+ thrashState = nextThrashState ( thrashState , [
813+ { type : "tool_call" , name : call . name , arguments : call . arguments } ,
814+ ] ) ;
815+ }
816+ }
785817 cycleRecorder . handleEvent ( event ) ;
786818 const partial = partialTextFromEvent ( event ) ;
787- if ( partial !== null ) lastPartialText = partial ;
819+ if ( partial !== null ) {
820+ lastPartialText = partial ;
821+ const trimmed = partial . trim ( ) ;
822+ if ( trimmed . length > 0 ) {
823+ const joined =
824+ accumulatedProse . length === 0 ? trimmed : `${ accumulatedProse } \n\n${ trimmed } ` ;
825+ accumulatedProse =
826+ joined . length <= TURN_PROSE_CAP ? joined : joined . slice ( - TURN_PROSE_CAP ) ;
827+ }
828+ }
788829 params . onEvent ?.( event ) ;
789830 } ;
790831 streamPromise = consumeStream ( agent . stream ( ) , streamSink ) ;
@@ -927,11 +968,13 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
927968 if ( interruptController . signal . aborted && ! runController . signal . aborted ) {
928969 interruptedKeepAlive = true ;
929970 const abortedCycleText = await cycleRecorder . dispose ( "cancelled" , { drain : streamPromise } ) ;
930- const tail =
931- lastPartialText . trim ( ) . length > 0 ? lastPartialText : abortedCycleText . slice ( - 2000 ) ;
971+ const tail = salvageFindingsText ( accumulatedProse , lastPartialText , abortedCycleText ) ;
932972 return {
933973 report : appendActivitySummary (
934- forcedStopReport ( "cancelled" , tail , "interrupted by interrupt_agent" ) ,
974+ forcedStopReport ( "cancelled" , tail , {
975+ detail : "interrupted by interrupt_agent" ,
976+ paths : salvagePathsFromThrash ( thrashState ) ,
977+ } ) ,
935978 toolNamesUsed ,
936979 ) ,
937980 stopReason : "cancelled" ,
@@ -953,15 +996,17 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
953996 // Deadline always salvages (even with zero output). Cancel after any
954997 // tools or assistant prose salvages so the parent keeps partial work;
955998 // pre-progress cancel still surfaces as a bare AbortError.
956- const hadProgress = toolNamesUsed . length > 0 || lastPartialText . trim ( ) . length > 0 ;
999+ const hadProgress =
1000+ toolNamesUsed . length > 0 ||
1001+ lastPartialText . trim ( ) . length > 0 ||
1002+ accumulatedProse . trim ( ) . length > 0 ;
9571003 const outcome = resolveSubAgentCatchOutcome ( {
9581004 deadlineHit : runController . deadlineHit ( ) ,
9591005 hadProgress,
9601006 } ) ;
9611007 if ( outcome !== "rethrow" ) {
9621008 const reason = outcome === "salvage-deadline" ? "deadline" : "cancelled" ;
963- const tail =
964- lastPartialText . trim ( ) . length > 0 ? lastPartialText : abortedCycleText . slice ( - 2000 ) ;
1009+ const tail = salvageFindingsText ( accumulatedProse , lastPartialText , abortedCycleText ) ;
9651010 const detail =
9661011 reason === "deadline" && resolvedDeadlineMs !== undefined
9671012 ? `${ resolvedDeadlineMs } ms elapsed`
@@ -973,7 +1018,13 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
9731018 ...( detail !== undefined ? { detail } : { } ) ,
9741019 } ) ;
9751020 return {
976- report : appendActivitySummary ( forcedStopReport ( reason , tail , detail ) , toolNamesUsed ) ,
1021+ report : appendActivitySummary (
1022+ forcedStopReport ( reason , tail , {
1023+ ...( detail !== undefined ? { detail } : { } ) ,
1024+ paths : salvagePathsFromThrash ( thrashState ) ,
1025+ } ) ,
1026+ toolNamesUsed ,
1027+ ) ,
9771028 stopReason : reason ,
9781029 } ;
9791030 }
0 commit comments