@@ -19,6 +19,8 @@ import {
1919 formatSubAgentReport ,
2020 nextToolCallStreak ,
2121 parseSubAgentReport ,
22+ repetitionStopDetail ,
23+ stopReasonFromReport ,
2224 appendDeadlineParentHint ,
2325 appendNeverActedParentHint ,
2426 appendSubAgentParentHints ,
@@ -768,6 +770,48 @@ describe("sub-agent stop helpers", () => {
768770 ) ;
769771 } ) ;
770772
773+ test ( "forcedStopReport carries a machine-readable Stopped line the parent sees verbatim" , ( ) => {
774+ const repetition = forcedStopReport (
775+ "repetition" ,
776+ "Looped window (repeated 1363x): Groaning. " ,
777+ 'window "Groaning. " × 1363' ,
778+ ) ;
779+ expect ( repetition . startsWith ( 'Stopped: repetition — window "Groaning. " × 1363\n' ) ) . toBe ( true ) ;
780+ expect ( parseSubAgentReport ( repetition ) . stopped ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
781+ expect ( stopReasonFromReport ( repetition ) ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
782+ // Survives runSubAgent's parse/format normalization round-trip.
783+ const roundTripped = formatSubAgentReport ( parseSubAgentReport ( repetition ) ) ;
784+ expect ( stopReasonFromReport ( roundTripped ) ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
785+ // Classifiers and hints still fire on the unchanged Summary text.
786+ expect ( appendSubAgentParentHints ( repetition ) ) . toContain ( "degenerated into a loop" ) ;
787+
788+ const cancelled = forcedStopReport ( "cancelled" , "partial" , "Session closed" ) ;
789+ expect ( stopReasonFromReport ( cancelled ) ) . toBe ( "cancelled — Session closed" ) ;
790+ // Without a detail the line is the bare reason token.
791+ expect ( stopReasonFromReport ( forcedStopReport ( "cancelled" , "partial" ) ) ) . toBe ( "cancelled" ) ;
792+ expect ( stopReasonFromReport ( forcedStopReport ( "turn-budget" , "x" , "30/30 turns" ) ) ) . toBe (
793+ "turn-budget — 30/30 turns" ,
794+ ) ;
795+
796+ // A nested forced-stop quoted in Findings must not leak its Stopped line
797+ // as the outer report's reason.
798+ const nested = forcedStopReport (
799+ "never-acted" ,
800+ forcedStopReport ( "cancelled" , "inner" , "inner reason" ) ,
801+ ) ;
802+ expect ( stopReasonFromReport ( nested ) ) . toBe ( "never-acted" ) ;
803+ // A clean report has no Stopped line.
804+ expect ( stopReasonFromReport ( "## Summary\nDone.\n\n## Findings\nx" ) ) . toBe ( null ) ;
805+ } ) ;
806+
807+ test ( "repetitionStopDetail formats the looped window snippet and repeat count" , ( ) => {
808+ expect ( repetitionStopDetail ( { window : "Groaning. " , repeats : 1363 } ) ) . toBe (
809+ 'window "Groaning. " × 1363' ,
810+ ) ;
811+ const long = repetitionStopDetail ( { window : "x" . repeat ( 500 ) , repeats : 7 } ) ;
812+ expect ( long ) . toBe ( `window "${ "x" . repeat ( 80 ) } " × 7` ) ;
813+ } ) ;
814+
771815 test ( "createSubAgentRunController aborts on an explicit deadline and reports deadlineHit" , async ( ) => {
772816 const ctl = createSubAgentRunController ( undefined , 20 ) ;
773817 expect ( ctl . signal . aborted ) . toBe ( false ) ;
@@ -1884,6 +1928,29 @@ describe("createTaskTool", () => {
18841928 expect ( row ?. status ) . toBe ( "cancelled" ) ;
18851929 } ) ;
18861930
1931+ test ( "pre-progress cancel surfaces the recorded cancel reason to the parent" , async ( ) => {
1932+ const sessions = createSubAgentSessionStore ( ) ;
1933+ const tool = createTaskTool ( {
1934+ permissionGate : testPermissionGate ,
1935+ cwd : "/repo" ,
1936+ getWorkdirBase : ( ) => "/repo/.corbits" ,
1937+ provider,
1938+ sessions,
1939+ run : async ( ) => {
1940+ const row = sessions . list ( ) . find ( ( s ) => s . description === "reasoned" ) ;
1941+ if ( row !== undefined ) sessions . cancel ( row . id , "Session closed" ) ;
1942+ const err = new Error ( "aborted" ) ;
1943+ err . name = "AbortError" ;
1944+ throw err ;
1945+ } ,
1946+ } ) ;
1947+ const out = await callTask ( tool , { description : "reasoned" , prompt : "x" , intent : "explore" } ) ;
1948+ expect ( out ) . toContain ( "Stopped: cancelled — Session closed" ) ;
1949+ const row = sessions . list ( ) . find ( ( s ) => s . description === "reasoned" ) ;
1950+ expect ( row ?. status ) . toBe ( "cancelled" ) ;
1951+ expect ( row ?. stopReason ) . toBe ( "cancelled — Session closed" ) ;
1952+ } ) ;
1953+
18871954 test ( "pre-progress AbortError still surfaces as bare cancel" , async ( ) => {
18881955 const tool = createTaskTool ( {
18891956 permissionGate : testPermissionGate ,
0 commit comments