@@ -115,6 +115,98 @@ function buildRecoveryNotice(kind: "context" | "rate"): string {
115115 return "Quick heads up — I hit temporary model capacity and am retrying now. I’ll send the full response shortly." ;
116116}
117117
118+ // ─── Response Content Sanitizer ───────────────────────────────────────────────
119+ // Strip reasoning/thinking blocks and system noise from agent responses before
120+ // they are persisted as messages. Only the final answer should be visible.
121+ // ─────────────────────────────────────────────────────────────────────────────
122+
123+ /**
124+ * Strip reasoning blocks, system noise, and thinking preambles from agent
125+ * response text so that only the final answer is persisted in the chat.
126+ *
127+ * Patterns handled:
128+ * 1. "Reasoning: _italic block_" at the start — strip everything before the answer
129+ * 2. Multi-line "Reasoning:\n_..._\n" blocks — strip the reasoning portion
130+ * 3. Standalone "Reasoning: _..._" that IS the entire message — extract answer after closing _
131+ * 4. System metadata lines (Identity, Channel, Runtime, etc.)
132+ * 5. OpenClaw status blocks (🦞 / 🧭)
133+ */
134+ function sanitizeAgentResponse ( raw : string ) : string {
135+ if ( ! raw ) return raw ;
136+
137+ let text = raw ;
138+
139+ // 1. Strip leading "Reasoning: _..._" block followed by actual answer
140+ // Pattern: starts with "Reasoning:" then italic block, then answer text
141+ // e.g. "Reasoning: _I think about X_ Here is my answer"
142+ text = text . replace (
143+ / ^ R e a s o n i n g : \s * _ [ \s \S ] * ?_ \s * / i,
144+ ""
145+ ) ;
146+
147+ // 2. Strip "Reasoning: _..._" blocks that appear mid-text (e.g. after a NO_REPLY)
148+ text = text . replace (
149+ / R e a s o n i n g : \s * _ [ \s \S ] * ?_ \s * / gi,
150+ ""
151+ ) ;
152+
153+ // 3. Strip "Reasoning:" followed by non-italic content up to a double newline or end
154+ // e.g. "Reasoning: I considered several approaches\n\nHere is my answer"
155+ text = text . replace (
156+ / ^ R e a s o n i n g : \s * [ ^ \n ] * (?: \n (? ! \n ) [ ^ \n ] * ) * / i,
157+ ""
158+ ) ;
159+
160+ // 4. Strip 🧭 Identity blocks
161+ text = text . replace (
162+ / ^ 🧭 \s * I d e n t i t y \s * \n (?: (?: C h a n n e l | U s e r \s * i d | A l l o w F r o m | S e s s i o n | R u n t i m e | H o s t | M o d e l ) [ ^ \n ] * \n ? ) * / gim,
163+ ""
164+ ) ;
165+
166+ // 5. Strip 🦞 OpenClaw status blocks
167+ text = text . replace (
168+ / ^ 🦞 \s * O p e n C l a w [ ^ \n ] * (?: \n (? ! [ \n \r ] ) [ ^ \n ] * ) * / gim,
169+ ""
170+ ) ;
171+
172+ // 6. Strip standalone system metadata lines
173+ text = text . replace (
174+ / ^ \s * (?: [ * _ ` ~ > ] + \s * ) ? (?: R u n t i m e | C h a n n e l | S e s s i o n | A g e n t | I d e n t i t y | A l l o w F r o m | U s e r \s * i d ) (?: \s * [ * _ ` ~ ] + ) ? \s * : [ ^ \n ] * $ / gim,
175+ ""
176+ ) ;
177+
178+ // 7. Strip toggle-only reasoning/thinking lines (e.g. "Reasoning: on")
179+ text = text . replace (
180+ / ^ \s * (?: [ * _ ` ~ > ] + \s * ) ? (?: T h i n k i n g | R e a s o n i n g ) (?: \s * [ * _ ` ~ ] + ) ? \s * : \s * (?: [ * _ ` ~ ] + \s * ) ? (?: o n | o f f | e n a b l e d | d i s a b l e d ) \s * $ / gim,
181+ ""
182+ ) ;
183+
184+ // 8. Strip italic self-talk preamble
185+ // Agents (especially project_lead) wrap internal monologue in _italics_
186+ // before the actual answer. Find the last closing _ followed by the start
187+ // of real content (uppercase, bold, heading, number) and extract the tail.
188+ // Skips messages without italic preamble patterns.
189+ for ( let i = text . length - 2 ; i >= 0 ; i -- ) {
190+ if ( text [ i ] === "_" ) {
191+ const after = text [ i + 1 ] ;
192+ if ( / [ A - Z * # \[ 0 - 9 ] / . test ( after ) ) {
193+ const tail = text . substring ( i + 1 ) . trim ( ) ;
194+ const before = text . substring ( 0 , i ) ;
195+ // Only extract if: substantial tail, and there's italic content before
196+ if ( tail . length >= 10 && before . includes ( "_" ) ) {
197+ text = tail ;
198+ break ;
199+ }
200+ }
201+ }
202+ }
203+
204+ // 9. Collapse excessive newlines
205+ text = text . replace ( / \n { 3 , } / g, "\n\n" ) ;
206+
207+ return text . trim ( ) ;
208+ }
209+
118210// Dispatch state for deduplication
119211type DispatchState = {
120212 status : "in_progress" | "completed" ;
@@ -514,7 +606,7 @@ async function processDispatchAsync(
514606 let completionStatus : "success" | "failed" = "success" ;
515607
516608 if ( responseText ) {
517- finalResponse = responseText ;
609+ finalResponse = sanitizeAgentResponse ( responseText ) ;
518610 } else if ( lastError ) {
519611 finalResponse = `[Agent error: ${ lastError } ]` ;
520612 completionStatus = "failed" ;
@@ -536,7 +628,7 @@ async function processDispatchAsync(
536628 {
537629 agent_name : payload . agent_name || payload . agent_handle ,
538630 agent_id : payload . agent_id ,
539- org_id : payload . org_id ,
631+ org_id : payload . org_id || payload . space_id || session . spaceId ,
540632 message_id : payload . message_id ,
541633 completion_status : completionStatus ,
542634 response : finalResponse ,
@@ -779,6 +871,9 @@ export function createAxChannel(config: {
779871 logger . info ( `[ax-platform] Replaced raw internal error text with graceful recovery notice (${ internalErrorKind } )` ) ;
780872 }
781873
874+ // Sanitize outbound text (strip reasoning blocks, system noise, identity blocks)
875+ outboundText = sanitizeAgentResponse ( outboundText ) ;
876+
782877 try {
783878 const result = await callAxTool ( mcpEndpoint , authToken , "messages" , {
784879 action : "send" ,
@@ -1162,7 +1257,10 @@ export function createDispatchHandler(
11621257 payload . callback_url ,
11631258 payload . callback_api_key ,
11641259 {
1260+ agent_name : payload . agent_name || payload . agent_handle ,
11651261 agent_id : payload . agent_id ,
1262+ org_id : payload . org_id || payload . space_id || session . spaceId ,
1263+ message_id : payload . message_id ,
11661264 completion_status : "failed" ,
11671265 error : String ( err ) ,
11681266 } ,
@@ -1282,7 +1380,7 @@ export function createDispatchHandler(
12821380 // In this case, deliver() is never called because NO_REPLY is filtered out
12831381 let finalResponse : string ;
12841382 if ( responseText ) {
1285- finalResponse = responseText ;
1383+ finalResponse = sanitizeAgentResponse ( responseText ) ;
12861384 } else if ( lastError ) {
12871385 finalResponse = `[Agent error: ${ lastError } ]` ;
12881386 } else if ( deliverCallCount === 0 ) {
0 commit comments