@@ -19,7 +19,6 @@ import {
1919 formatSubAgentReport ,
2020 nextToolCallStreak ,
2121 parseSubAgentReport ,
22- repetitionStopDetail ,
2322 stopReasonFromReport ,
2423 appendDeadlineParentHint ,
2524 appendNeverActedParentHint ,
@@ -196,6 +195,49 @@ describe("sub-agent stop helpers", () => {
196195 ) . toBe ( "no-progress" ) ;
197196 } ) ;
198197
198+ // CL-6995: there is no repetition/similarity detector over tool results or
199+ // streamed text any more. A worker that keeps getting the same failure back
200+ // from the environment (e.g. a module a concurrent sibling has not finished
201+ // writing) and keeps varying its own tool calls in response must run to
202+ // completion rather than being killed mid-stream for "looping" on a stable
203+ // external error.
204+ test ( "many turns reacting to the same repeated tool failure still reach a normal complete" , ( ) => {
205+ let consecutiveIdentical = 0 ;
206+ let lastFingerprint : string | null = null ;
207+ const turns = DEFAULT_SUBAGENT_MAX_TURNS - 1 ;
208+ for ( let i = 0 ; i < turns ; i ++ ) {
209+ // Each turn varies its own tool call (different path), even though the
210+ // simulated tool result content would be identical every time.
211+ const fingerprint = fingerprintToolCalls ( [
212+ { type : "tool_call" , name : "read_file" , arguments : { path : `attempt-${ i } .ts` } } ,
213+ ] ) ;
214+ consecutiveIdentical = fingerprint === lastFingerprint ? consecutiveIdentical + 1 : 1 ;
215+ lastFingerprint = fingerprint ;
216+ expect (
217+ evaluateSubAgentStop ( {
218+ hasToolCalls : true ,
219+ everHadToolCalls : true ,
220+ turnsCompleted : i + 1 ,
221+ maxTurns : DEFAULT_SUBAGENT_MAX_TURNS ,
222+ consecutiveIdentical,
223+ repeatLimit : DEFAULT_SUBAGENT_REPEAT_LIMIT ,
224+ } ) ,
225+ ) . toBeNull ( ) ;
226+ }
227+ // The worker finally stops calling tools and reports a real result.
228+ expect (
229+ evaluateSubAgentStop ( {
230+ hasToolCalls : false ,
231+ everHadToolCalls : true ,
232+ turnsCompleted : turns + 1 ,
233+ maxTurns : DEFAULT_SUBAGENT_MAX_TURNS ,
234+ consecutiveIdentical : 0 ,
235+ repeatLimit : DEFAULT_SUBAGENT_REPEAT_LIMIT ,
236+ lastAssistantText : "## Summary\nDone.\n\n## Findings\nx\n\n## Blockers\nNone\n\n## Paths\n" ,
237+ } ) ,
238+ ) . toBe ( "complete" ) ;
239+ } ) ;
240+
199241 test ( "fingerprint is null when a turn has no tool calls" , ( ) => {
200242 expect ( fingerprintToolCalls ( [ { type : "text" } ] ) ) . toBeNull ( ) ;
201243 } ) ;
@@ -775,20 +817,6 @@ describe("sub-agent stop helpers", () => {
775817 } ) ;
776818
777819 test ( "forcedStopReport carries a machine-readable Stopped line the parent sees verbatim" , ( ) => {
778- const repetition = forcedStopReport (
779- "repetition" ,
780- "Looped window (repeated 1363x): Groaning. " ,
781- 'window "Groaning. " × 1363' ,
782- ) ;
783- expect ( repetition . startsWith ( 'Stopped: repetition — window "Groaning. " × 1363\n' ) ) . toBe ( true ) ;
784- expect ( parseSubAgentReport ( repetition ) . stopped ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
785- expect ( stopReasonFromReport ( repetition ) ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
786- // Survives runSubAgent's parse/format normalization round-trip.
787- const roundTripped = formatSubAgentReport ( parseSubAgentReport ( repetition ) ) ;
788- expect ( stopReasonFromReport ( roundTripped ) ) . toBe ( 'repetition — window "Groaning. " × 1363' ) ;
789- // Classifiers and hints still fire on the unchanged Summary text.
790- expect ( appendSubAgentParentHints ( repetition ) ) . toContain ( "degenerated into a loop" ) ;
791-
792820 const cancelled = forcedStopReport ( "cancelled" , "partial" , "Session closed" ) ;
793821 expect ( stopReasonFromReport ( cancelled ) ) . toBe ( "cancelled — Session closed" ) ;
794822 // Without a detail the line is the bare reason token.
@@ -808,18 +836,6 @@ describe("sub-agent stop helpers", () => {
808836 expect ( stopReasonFromReport ( "## Summary\nDone.\n\n## Findings\nx" ) ) . toBe ( null ) ;
809837 } ) ;
810838
811- test ( "repetitionStopDetail reports period length and repeat count, never the looped text" , ( ) => {
812- expect ( repetitionStopDetail ( { window : "Groaning. " , repeats : 1363 } , null ) ) . toBe (
813- "period 10ch × 1363" ,
814- ) ;
815- expect (
816- repetitionStopDetail (
817- { window : "x" . repeat ( 500 ) , repeats : 7 } ,
818- { windowMinChars : 8 , repeatThreshold : 16 , probeChars : 8192 } ,
819- ) ,
820- ) . toBe ( "period 500ch × 7 (threshold 16)" ) ;
821- } ) ;
822-
823839 test ( "createSubAgentRunController aborts on an explicit deadline and reports deadlineHit" , async ( ) => {
824840 const ctl = createSubAgentRunController ( undefined , 20 ) ;
825841 expect ( ctl . signal . aborted ) . toBe ( false ) ;
@@ -913,27 +929,6 @@ describe("sub-agent stop helpers", () => {
913929 expect ( resolveSubAgentCatchOutcome ( { deadlineHit : false , hadProgress : false } ) ) . toBe ( "rethrow" ) ;
914930 } ) ;
915931
916- test ( "resolveSubAgentCatchOutcome salvages a repetition abort even with zero progress" , ( ) => {
917- expect (
918- resolveSubAgentCatchOutcome ( {
919- deadlineHit : false ,
920- hadProgress : false ,
921- repetitionHit : true ,
922- } ) ,
923- ) . toBe ( "salvage-repetition" ) ;
924- } ) ;
925-
926- test ( "repetition forced stop reports the loop and warns against identical re-dispatch" , ( ) => {
927- const report = forcedStopReport ( "repetition" , "dig footer/chrome... 0/1.0 done. 1 remaining." ) ;
928- const parsed = parseSubAgentReport ( report ) ;
929- expect ( parsed . summary ) . toContain ( "degenerate repetition" ) ;
930- expect ( parsed . findings ) . toContain ( "dig footer/chrome" ) ;
931- expect ( parsed . blockers ) . toContain ( "will be refused" ) ;
932- expect ( parsed . blockers ) . toContain ( "not maxTurns alone" ) ;
933- const hinted = appendSubAgentParentHints ( report ) ;
934- expect ( hinted ) . toContain ( "Do not re-dispatch the identical brief" ) ;
935- } ) ;
936-
937932 test ( "partialTextFromEvent reads stream inference.done data.turn content" , ( ) => {
938933 const text = partialTextFromEvent ( {
939934 type : "inference.done" ,
@@ -2162,8 +2157,8 @@ describe("brief re-dispatch ledger (CL-4343 / CL-5203)", () => {
21622157 expect ( ledger . admit ( other ) . ok ) . toBe ( true ) ;
21632158 } ) ;
21642159
2165- test ( "hard-blocks no-progress, repetition, never-acted, never-edited; not turn-budget" , ( ) => {
2166- for ( const salvage of [ "no-progress" , "repetition" , " never-acted", "never-edited" ] as const ) {
2160+ test ( "hard-blocks no-progress, never-acted, never-edited; not turn-budget" , ( ) => {
2161+ for ( const salvage of [ "no-progress" , "never-acted" , "never-edited" ] as const ) {
21672162 const ledger = createBriefDispatchLedger ( ) ;
21682163 const fp = fingerprintTaskBrief ( { prompt : `job ${ salvage } ` } ) ;
21692164 expect ( ledger . admit ( fp ) . ok ) . toBe ( true ) ;
@@ -2225,7 +2220,6 @@ describe("brief re-dispatch ledger (CL-4343 / CL-5203)", () => {
22252220
22262221 test ( "classifyBriefSalvage maps forced-stop envelopes" , ( ) => {
22272222 expect ( classifyBriefSalvage ( forcedStopReport ( "no-progress" , "x" ) ) ) . toBe ( "no-progress" ) ;
2228- expect ( classifyBriefSalvage ( forcedStopReport ( "repetition" , "x" ) ) ) . toBe ( "repetition" ) ;
22292223 expect ( classifyBriefSalvage ( forcedStopReport ( "never-acted" , "x" ) ) ) . toBe ( "never-acted" ) ;
22302224 expect ( classifyBriefSalvage ( forcedStopReport ( "never-edited" , "x" ) ) ) . toBe ( "never-edited" ) ;
22312225 expect ( classifyBriefSalvage ( forcedStopReport ( "no-ship" , "x" ) ) ) . toBe ( "no-ship" ) ;
0 commit comments