@@ -734,3 +734,162 @@ describe('progress event subtypes', () => {
734734 expect ( events . length ) . toBeGreaterThanOrEqual ( 1 ) ;
735735 } ) ;
736736} ) ;
737+
738+ // ── System-injected message filtering ──────────────────────────────────────
739+
740+ describe ( 'system-injected message filtering' , ( ) => {
741+ it ( 'filters <task-notification> and emits agent.status processing instead' , async ( ) => {
742+ const filePath = join ( testDir , 'test.jsonl' ) ;
743+ await writeFile ( filePath , '' ) ;
744+ await startWatchingFile ( 'test_session' , filePath ) ;
745+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
746+
747+ await appendFile ( filePath , jsonlLine ( {
748+ type : 'user' ,
749+ timestamp : new Date ( ) . toISOString ( ) ,
750+ message : { content : [ { type : 'text' , text : '<task-notification>\n<task-id>abc123</task-id>\n<status>completed</status>\n<summary>Background command done</summary>\n</task-notification>' } ] } ,
751+ } ) ) ;
752+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
753+
754+ // Should NOT appear as user.message
755+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'task-notification' ) ) ;
756+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
757+
758+ // Should emit agent.status processing
759+ const statusEvents = emittedEvents . filter ( ( e ) => e . type === 'agent.status' && e . payload . status === 'processing' ) ;
760+ expect ( statusEvents . length ) . toBeGreaterThanOrEqual ( 1 ) ;
761+ } ) ;
762+
763+ it ( 'filters <system-reminder> and emits agent.status processing instead' , async ( ) => {
764+ const filePath = join ( testDir , 'test.jsonl' ) ;
765+ await writeFile ( filePath , '' ) ;
766+ await startWatchingFile ( 'test_session' , filePath ) ;
767+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
768+
769+ await appendFile ( filePath , jsonlLine ( {
770+ type : 'user' ,
771+ timestamp : new Date ( ) . toISOString ( ) ,
772+ message : { content : [ { type : 'text' , text : '<system-reminder>\nThe following tools are available...\n</system-reminder>' } ] } ,
773+ } ) ) ;
774+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
775+
776+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'system-reminder' ) ) ;
777+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
778+
779+ const statusEvents = emittedEvents . filter ( ( e ) => e . type === 'agent.status' && e . payload . status === 'processing' ) ;
780+ expect ( statusEvents . length ) . toBeGreaterThanOrEqual ( 1 ) ;
781+ } ) ;
782+
783+ it ( 'filters <command-name> slash commands' , async ( ) => {
784+ const filePath = join ( testDir , 'test.jsonl' ) ;
785+ await writeFile ( filePath , '' ) ;
786+ await startWatchingFile ( 'test_session' , filePath ) ;
787+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
788+
789+ await appendFile ( filePath , jsonlLine ( {
790+ type : 'user' ,
791+ timestamp : new Date ( ) . toISOString ( ) ,
792+ message : { content : [ { type : 'text' , text : '<command-name>/commit</command-name>\n<command-message>commit</command-message>\n<command-args></command-args>' } ] } ,
793+ } ) ) ;
794+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
795+
796+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'command-name' ) ) ;
797+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
798+ } ) ;
799+
800+ it ( 'filters <local-command-caveat> local commands' , async ( ) => {
801+ const filePath = join ( testDir , 'test.jsonl' ) ;
802+ await writeFile ( filePath , '' ) ;
803+ await startWatchingFile ( 'test_session' , filePath ) ;
804+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
805+
806+ await appendFile ( filePath , jsonlLine ( {
807+ type : 'user' ,
808+ timestamp : new Date ( ) . toISOString ( ) ,
809+ message : { content : [ { type : 'text' , text : '<local-command-caveat>Caveat: local command output</local-command-caveat>\n<local-command-stdout>Model set to opus</local-command-stdout>' } ] } ,
810+ } ) ) ;
811+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
812+
813+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'local-command' ) ) ;
814+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
815+ } ) ;
816+
817+ it ( 'filters <bash-input> / <bash-stdout> / <bash-stderr>' , async ( ) => {
818+ const filePath = join ( testDir , 'test.jsonl' ) ;
819+ await writeFile ( filePath , '' ) ;
820+ await startWatchingFile ( 'test_session' , filePath ) ;
821+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
822+
823+ await appendFile ( filePath , jsonlLine ( {
824+ type : 'user' ,
825+ timestamp : new Date ( ) . toISOString ( ) ,
826+ message : { content : [ { type : 'text' , text : '<bash-input>ls -la</bash-input>\n<bash-stdout>total 42\n</bash-stdout>' } ] } ,
827+ } ) ) ;
828+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
829+
830+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'bash-input' ) ) ;
831+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
832+ } ) ;
833+
834+ it ( 'filters <command-message> tags' , async ( ) => {
835+ const filePath = join ( testDir , 'test.jsonl' ) ;
836+ await writeFile ( filePath , '' ) ;
837+ await startWatchingFile ( 'test_session' , filePath ) ;
838+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
839+
840+ await appendFile ( filePath , jsonlLine ( {
841+ type : 'user' ,
842+ timestamp : new Date ( ) . toISOString ( ) ,
843+ message : { content : [ { type : 'text' , text : '<command-message>model</command-message>' } ] } ,
844+ } ) ) ;
845+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
846+
847+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'command-message' ) ) ;
848+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
849+ } ) ;
850+
851+ it ( 'filters string-form user content with system tags' , async ( ) => {
852+ const filePath = join ( testDir , 'test.jsonl' ) ;
853+ await writeFile ( filePath , '' ) ;
854+ await startWatchingFile ( 'test_session' , filePath ) ;
855+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
856+
857+ // String content (not array blocks) — real CC format
858+ await appendFile ( filePath , jsonlLine ( {
859+ type : 'user' ,
860+ timestamp : new Date ( ) . toISOString ( ) ,
861+ message : { role : 'user' , content : '<system-reminder>\nToday is 2026-03-20.\n</system-reminder>' } ,
862+ } ) ) ;
863+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
864+
865+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'system-reminder' ) ) ;
866+ expect ( userMsgs ) . toHaveLength ( 0 ) ;
867+ } ) ;
868+
869+ it ( 'does NOT filter normal user messages' , async ( ) => {
870+ const filePath = join ( testDir , 'test.jsonl' ) ;
871+ await writeFile ( filePath , '' ) ;
872+ await startWatchingFile ( 'test_session' , filePath ) ;
873+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
874+
875+ await appendFile ( filePath , userMessage ( 'Hello, please review this code' ) ) ;
876+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
877+
878+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && e . payload . text === 'Hello, please review this code' ) ;
879+ expect ( userMsgs . length ) . toBeGreaterThanOrEqual ( 1 ) ;
880+ } ) ;
881+
882+ it ( 'does NOT filter user messages that mention XML tags in natural text' , async ( ) => {
883+ const filePath = join ( testDir , 'test.jsonl' ) ;
884+ await writeFile ( filePath , '' ) ;
885+ await startWatchingFile ( 'test_session' , filePath ) ;
886+ await new Promise ( ( r ) => setTimeout ( r , 200 ) ) ;
887+
888+ // User talks ABOUT task-notification but it's not a real system inject
889+ await appendFile ( filePath , userMessage ( 'What does the task-notification XML look like?' ) ) ;
890+ await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
891+
892+ const userMsgs = emittedEvents . filter ( ( e ) => e . type === 'user.message' && String ( e . payload . text ) . includes ( 'task-notification' ) ) ;
893+ expect ( userMsgs . length ) . toBeGreaterThanOrEqual ( 1 ) ;
894+ } ) ;
895+ } ) ;
0 commit comments