@@ -29,8 +29,40 @@ const logger = new Logger(config.LOG_LEVEL as any);
2929/**
3030 * Format a CLI progress event into a user-friendly Discord message
3131 */
32- function formatProgressMessage ( event : AddProgressEvent ) : string {
33- switch ( event . phase ) {
32+ export function formatProgressMessage ( event : AddProgressEvent ) : string {
33+ // Normalize phase: avoid printing literal 'undefined' and handle
34+ // events that do not include a phase field more helpfully.
35+ const phase = typeof event . phase === "string" && event . phase . trim ( ) !== "" ? event . phase : undefined ;
36+
37+ if ( ! phase ) {
38+ // If the CLI provided an explanatory message, surface it as an error-like
39+ // message so users and maintainers see something actionable in the thread.
40+ if ( event . message ) {
41+ const m = String ( event . message ) . trim ( ) ;
42+ // Keep output reasonably sized for Discord
43+ const truncated = m . length > 1500 ? `${ m . slice ( 0 , 1500 ) } …` : m ;
44+ // Include title or url context when available
45+ if ( event . title ) return `❌ ${ truncated } (${ event . title } )` ;
46+ if ( event . url ) return `❌ ${ truncated } (<${ event . url } >)` ;
47+ return `❌ ${ truncated } ` ;
48+ }
49+
50+ // No explicit message - include any small set of identifying fields so
51+ // maintainers have context instead of seeing 'undefined'.
52+ const parts : string [ ] = [ ] ;
53+ if ( event . id !== undefined ) parts . push ( `id:${ event . id } ` ) ;
54+ if ( event . title ) parts . push ( `title:${ event . title } ` ) ;
55+ if ( event . url ) parts . push ( `url:${ event . url } ` ) ;
56+ if ( event . timestamp ) parts . push ( `ts:${ event . timestamp } ` ) ;
57+
58+ if ( parts . length > 0 ) {
59+ return `⏳ Processing: unknown (${ parts . join ( ", " ) } )` ;
60+ }
61+
62+ return `⏳ Processing: unknown event` ;
63+ }
64+
65+ switch ( phase ) {
3466 case "downloading" :
3567 return "⏳ Downloading content..." ;
3668 case "extracting" :
@@ -42,7 +74,15 @@ function formatProgressMessage(event: AddProgressEvent): string {
4274 case "failed" :
4375 return `❌ Failed: ${ event . message || "Unknown error" } ` ;
4476 default :
45- return `⏳ Processing: ${ event . phase } ` ;
77+ // For unknown but present phases, include any short message text
78+ // the CLI might have provided to give more context.
79+ const base = `⏳ Processing: ${ phase } ` ;
80+ if ( event . message ) {
81+ const m = String ( event . message ) . trim ( ) ;
82+ const truncated = m . length > 1200 ? `${ m . slice ( 0 , 1200 ) } …` : m ;
83+ return `${ base } \n\n${ truncated } ` ;
84+ }
85+ return base ;
4686 }
4787}
4888
@@ -494,6 +534,10 @@ async function processUrlWithProgress(
494534 } ) ;
495535
496536 let lastPhase : string | null = null ;
537+ // Once we see a terminal phase ('completed' or 'failed') we should
538+ // suppress any subsequent progress updates emitted by the CLI to avoid
539+ // confusing the user with post-completion informational objects.
540+ let terminalPhaseSeen = false ;
497541 let eventCount = 0 ;
498542 let finalResult : AddResult | undefined ;
499543
@@ -518,17 +562,29 @@ async function processUrlWithProgress(
518562 // Process yielded progress event
519563 const event = iteration . value ;
520564 eventCount ++ ;
521- logger . info ( "Received CLI progress event" , {
522- messageId : message . id ,
523- url,
565+ logger . info ( "Received CLI progress event" , {
566+ messageId : message . id ,
567+ url,
524568 phase : event . phase ,
525- eventCount
569+ eventCount,
526570 } ) ;
527-
571+
572+ // If we've already observed a terminal phase, ignore any further
573+ // progress events to avoid confusing follow-up messages (some CLI
574+ // implementations emit informational objects after completion).
575+ if ( terminalPhaseSeen ) {
576+ logger . debug ( "Ignoring CLI progress event after terminal phase" , {
577+ messageId : message . id ,
578+ url,
579+ eventCount,
580+ } ) ;
581+ continue ;
582+ }
583+
528584 // Only send update if phase changed (avoid spam)
529585 if ( event . phase !== lastPhase ) {
530586 lastPhase = event . phase ;
531-
587+
532588 const progressMsg = formatProgressMessage ( event ) ;
533589
534590 // Always ensure URLs shown to users are wrapped in backticks to avoid embeds.
@@ -574,6 +630,16 @@ async function processUrlWithProgress(
574630 } ) ;
575631 }
576632 }
633+
634+ // If this event indicates a terminal state, mark it so we ignore
635+ // any subsequent non-actionable events.
636+ try {
637+ if ( event . phase === "completed" || event . phase === "failed" ) {
638+ terminalPhaseSeen = true ;
639+ }
640+ } catch {
641+ // ignore
642+ }
577643 }
578644 }
579645
@@ -597,28 +663,37 @@ async function processUrlWithProgress(
597663
598664 let summaryTargetThread : ThreadChannel | null = thread ;
599665
600- if ( thread ) {
601- try {
602- await thread . send ( successMsg ) ;
603- } catch ( error ) {
604- logger . warn ( "Failed to send final success message to thread; falling back to channel reply" , {
605- threadId : thread . id ,
606- error : error instanceof Error ? error . message : String ( error ) ,
607- } ) ;
608- summaryTargetThread = null ;
609- // Fallback to channel reply
666+ // If we already observed a 'completed' progress event earlier and
667+ // posted it to the thread/channel, avoid posting a duplicate final
668+ // success message. We detect this by checking the lastPhase value.
669+ const alreadyCompleted = lastPhase === "completed" ;
670+
671+ if ( ! alreadyCompleted ) {
672+ if ( thread ) {
610673 try {
611- await message . reply ( successMsg ) ;
612- } catch ( err ) {
613- logger . warn ( "Failed to send fallback success reply to channel" , {
614- messageId : message . id ,
615- error : err instanceof Error ? err . message : String ( err ) ,
674+ await thread . send ( successMsg ) ;
675+ } catch ( error ) {
676+ logger . warn ( "Failed to send final success message to thread; falling back to channel reply " , {
677+ threadId : thread . id ,
678+ error : error instanceof Error ? error . message : String ( error ) ,
616679 } ) ;
680+ summaryTargetThread = null ;
681+ // Fallback to channel reply
682+ try {
683+ await message . reply ( successMsg ) ;
684+ } catch ( err ) {
685+ logger . warn ( "Failed to send fallback success reply to channel" , {
686+ messageId : message . id ,
687+ error : err instanceof Error ? err . message : String ( err ) ,
688+ } ) ;
689+ }
617690 }
691+ } else {
692+ // Fallback to message reply if no thread
693+ await message . reply ( successMsg ) ;
618694 }
619695 } else {
620- // Fallback to message reply if no thread
621- await message . reply ( successMsg ) ;
696+ logger . debug ( "Skipping duplicate final success message because completed event was already posted" , { messageId : message . id , url } ) ;
622697 }
623698
624699 await sendGeneratedSummary ( message , summaryTargetThread , finalResult ) ;
@@ -658,7 +733,7 @@ async function processUrlWithProgress(
658733
659734 if ( thread ) {
660735 try {
661- await thread . send ( report ) ;
736+ await postCliErrorReport ( thread , report , "⚠️ CLI error encountered during processing. See attached diagnostic report." ) ;
662737 await thread . send ( errorMsg ) ;
663738 await thread . setArchived ( true ) . catch ( ( ) => { } ) ;
664739 } catch ( sendError ) {
@@ -667,7 +742,7 @@ async function processUrlWithProgress(
667742 error : sendError instanceof Error ? sendError . message : String ( sendError ) ,
668743 } ) ;
669744 try {
670- await message . reply ( report ) ;
745+ await postCliErrorReport ( message , report , "⚠️ CLI error encountered during processing. See attached diagnostic report." ) ;
671746 await message . reply ( errorMsg ) ;
672747 } catch ( replyError ) {
673748 logger . warn ( "Failed to send fallback CLI error report reply" , {
@@ -681,7 +756,7 @@ async function processUrlWithProgress(
681756 const t = await createThreadForMessage ( message , `CLI error: ${ new URL ( url ) . hostname } ` , 60 ) ;
682757 if ( t ) {
683758 try {
684- await t . send ( report ) ;
759+ await postCliErrorReport ( t , report , "⚠️ CLI error encountered during processing. See attached diagnostic report." ) ;
685760 await t . send ( errorMsg ) ;
686761 await t . setArchived ( true ) . catch ( ( ) => { } ) ;
687762 } catch ( threadErr ) {
@@ -691,7 +766,7 @@ async function processUrlWithProgress(
691766 error : threadErr instanceof Error ? threadErr . message : String ( threadErr ) ,
692767 } ) ;
693768 try {
694- await message . reply ( report ) ;
769+ await postCliErrorReport ( message , report , "⚠️ CLI error encountered during processing. See attached diagnostic report." ) ;
695770 await message . reply ( errorMsg ) ;
696771 } catch ( replyError ) {
697772 logger . warn ( "Failed to reply with CLI error report" , {
@@ -703,7 +778,7 @@ async function processUrlWithProgress(
703778 } else {
704779 // Last resort - reply in channel
705780 try {
706- await message . reply ( report ) ;
781+ await postCliErrorReport ( message , report , "⚠️ CLI error encountered during processing. See attached diagnostic report." ) ;
707782 await message . reply ( errorMsg ) ;
708783 } catch ( replyError ) {
709784 logger . warn ( "Failed to reply with CLI error report (no thread available)" , {
@@ -935,6 +1010,54 @@ async function suppressEmbedsIfPermitted(message: Message): Promise<boolean> {
9351010const DISCORD_CONTENT_LIMIT = 1900 ;
9361011const MARKDOWN_WRAP_WIDTH = 80 ;
9371012
1013+ /**
1014+ * Safely post a potentially large CLI diagnostic report to a Discord target.
1015+ * If the report is within the content limit, post as a normal message.
1016+ * If it exceeds the limit, post a short explanatory message and attach
1017+ * the full report as a file to avoid Discord's message length restriction.
1018+ */
1019+ export async function postCliErrorReport ( target : any , report : string , shortIntro ?: string ) : Promise < void > {
1020+ try {
1021+ if ( ! report ) return ;
1022+
1023+ if ( report . length <= DISCORD_CONTENT_LIMIT ) {
1024+ if ( typeof target . send === "function" ) {
1025+ await target . send ( report ) ;
1026+ return ;
1027+ }
1028+ if ( typeof target . reply === "function" ) {
1029+ await target . reply ( report ) ;
1030+ return ;
1031+ }
1032+ return ;
1033+ }
1034+
1035+ // Report too large for a single Discord message - send as attachment.
1036+ const intro = shortIntro || "Detailed CLI diagnostic attached." ;
1037+ const content = `${ intro } \n\n(Full report attached as cli-error-report.txt)` ;
1038+ const file = { attachment : Buffer . from ( report , "utf8" ) , name : "cli-error-report.txt" } ;
1039+
1040+ if ( typeof target . send === "function" ) {
1041+ await target . send ( { content, files : [ file ] } as any ) ;
1042+ return ;
1043+ }
1044+ if ( typeof target . reply === "function" ) {
1045+ await target . reply ( { content, files : [ file ] } as any ) ;
1046+ return ;
1047+ }
1048+ } catch ( err ) {
1049+ logger . warn ( "Failed to post CLI error report" , { error : err instanceof Error ? err . message : String ( err ) } ) ;
1050+ // Best-effort fallback: try to send a truncated inline excerpt
1051+ try {
1052+ const truncated = report . slice ( 0 , Math . max ( 0 , DISCORD_CONTENT_LIMIT - 50 ) ) + "…" ;
1053+ if ( typeof target . send === "function" ) await target . send ( truncated ) ;
1054+ else if ( typeof target . reply === "function" ) await target . reply ( truncated ) ;
1055+ } catch {
1056+ // ignore
1057+ }
1058+ }
1059+ }
1060+
9381061function wrapLineAtNearestSpace ( line : string , width : number ) : string [ ] {
9391062 if ( line . length <= width || width <= 0 ) {
9401063 return [ line ] ;
@@ -1804,14 +1927,14 @@ const bot = new DiscordBot({
18041927 if ( typeof message . startThread === "function" ) {
18051928 try {
18061929 const t = await message . startThread ( { name : `CLI error: ${ new URL ( seed ) . hostname } ` , autoArchiveDuration : 60 } ) ;
1807- await t . send ( report ) ;
1930+ await postCliErrorReport ( t , report , "⚠️ CLI error encountered while queueing a URL. See attached diagnostic report." ) ;
18081931 await t . setArchived ( true ) . catch ( ( ) => { } ) ;
18091932 } catch ( threadErr ) {
18101933 logger . warn ( "Failed to create thread for CLI queue error; falling back to reply" , { error : threadErr instanceof Error ? threadErr . message : String ( threadErr ) } ) ;
1811- await message . reply ( report ) ;
1934+ await postCliErrorReport ( message , report , "⚠️ CLI error encountered while queueing a URL. See attached diagnostic report." ) ;
18121935 }
18131936 } else {
1814- await message . reply ( report ) ;
1937+ await postCliErrorReport ( message , report , "⚠️ CLI error encountered while queueing a URL. See attached diagnostic report." ) ;
18151938 }
18161939 } catch ( err ) {
18171940 logger . warn ( "Failed to post detailed CLI error report for queue command" , { error : err instanceof Error ? err . message : String ( err ) } ) ;
0 commit comments