@@ -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 critique director. Greybeard is also
273290 * intent=review and may spawn-only then envelope; that is not a fake
@@ -770,6 +787,12 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
770787 // transcript (which would interleave sub-agent text with the parent turn).
771788 const toolNamesUsed : string [ ] = [ ] ;
772789 let lastPartialText = "" ;
790+ // Accumulate assistant prose across turns (capped) so cancel/deadline
791+ // salvage Findings keep substantive mid-run text, not only the final cycle.
792+ const TURN_PROSE_CAP = 12_000 ;
793+ let accumulatedProse = "" ;
794+ // Thrash paths from tool.start so mid-tool cancel still lists files touched.
795+ let thrashState = EMPTY_THRASH_STATE ;
773796 // Watch the streamed text of the in-flight cycle so a salvage on
774797 // cancel/deadline has the cycle's tail as its payload, even though no
775798 // turn boundary has completed yet to carry it.
@@ -780,9 +803,27 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
780803 toolNamesUsed . push ( name ) ;
781804 params . onProgress ?.( { description : params . description , toolName : name } ) ;
782805 }
806+ if ( event . type === "tool.start" ) {
807+ const call = ( event as { data ?: { call ?: { name ?: unknown ; arguments ?: unknown } } } ) . data
808+ ?. call ;
809+ if ( typeof call ?. name === "string" && call . name . length > 0 ) {
810+ thrashState = nextThrashState ( thrashState , [
811+ { type : "tool_call" , name : call . name , arguments : call . arguments } ,
812+ ] ) ;
813+ }
814+ }
783815 cycleRecorder . handleEvent ( event ) ;
784816 const partial = partialTextFromEvent ( event ) ;
785- if ( partial !== null ) lastPartialText = partial ;
817+ if ( partial !== null ) {
818+ lastPartialText = partial ;
819+ const trimmed = partial . trim ( ) ;
820+ if ( trimmed . length > 0 ) {
821+ const joined =
822+ accumulatedProse . length === 0 ? trimmed : `${ accumulatedProse } \n\n${ trimmed } ` ;
823+ accumulatedProse =
824+ joined . length <= TURN_PROSE_CAP ? joined : joined . slice ( - TURN_PROSE_CAP ) ;
825+ }
826+ }
786827 params . onEvent ?.( event ) ;
787828 } ;
788829 streamPromise = consumeStream ( agent . stream ( ) , streamSink ) ;
@@ -925,11 +966,13 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
925966 if ( interruptController . signal . aborted && ! runController . signal . aborted ) {
926967 interruptedKeepAlive = true ;
927968 const abortedCycleText = await cycleRecorder . dispose ( "cancelled" , { drain : streamPromise } ) ;
928- const tail =
929- lastPartialText . trim ( ) . length > 0 ? lastPartialText : abortedCycleText . slice ( - 2000 ) ;
969+ const tail = salvageFindingsText ( accumulatedProse , lastPartialText , abortedCycleText ) ;
930970 return {
931971 report : appendActivitySummary (
932- forcedStopReport ( "cancelled" , tail , "interrupted by interrupt_agent" ) ,
972+ forcedStopReport ( "cancelled" , tail , {
973+ detail : "interrupted by interrupt_agent" ,
974+ paths : salvagePathsFromThrash ( thrashState ) ,
975+ } ) ,
933976 toolNamesUsed ,
934977 ) ,
935978 stopReason : "cancelled" ,
@@ -951,15 +994,17 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
951994 // Deadline always salvages (even with zero output). Cancel after any
952995 // tools or assistant prose salvages so the parent keeps partial work;
953996 // pre-progress cancel still surfaces as a bare AbortError.
954- const hadProgress = toolNamesUsed . length > 0 || lastPartialText . trim ( ) . length > 0 ;
997+ const hadProgress =
998+ toolNamesUsed . length > 0 ||
999+ lastPartialText . trim ( ) . length > 0 ||
1000+ accumulatedProse . trim ( ) . length > 0 ;
9551001 const outcome = resolveSubAgentCatchOutcome ( {
9561002 deadlineHit : runController . deadlineHit ( ) ,
9571003 hadProgress,
9581004 } ) ;
9591005 if ( outcome !== "rethrow" ) {
9601006 const reason = outcome === "salvage-deadline" ? "deadline" : "cancelled" ;
961- const tail =
962- lastPartialText . trim ( ) . length > 0 ? lastPartialText : abortedCycleText . slice ( - 2000 ) ;
1007+ const tail = salvageFindingsText ( accumulatedProse , lastPartialText , abortedCycleText ) ;
9631008 const detail =
9641009 reason === "deadline" && resolvedDeadlineMs !== undefined
9651010 ? `${ resolvedDeadlineMs } ms elapsed`
@@ -971,7 +1016,13 @@ export async function runSubAgent(params: RunSubAgentParams): Promise<RunSubAgen
9711016 ...( detail !== undefined ? { detail } : { } ) ,
9721017 } ) ;
9731018 return {
974- report : appendActivitySummary ( forcedStopReport ( reason , tail , detail ) , toolNamesUsed ) ,
1019+ report : appendActivitySummary (
1020+ forcedStopReport ( reason , tail , {
1021+ ...( detail !== undefined ? { detail } : { } ) ,
1022+ paths : salvagePathsFromThrash ( thrashState ) ,
1023+ } ) ,
1024+ toolNamesUsed ,
1025+ ) ,
9751026 stopReason : reason ,
9761027 } ;
9771028 }
0 commit comments