Skip to content

Commit 88871ff

Browse files
Cipherclaude
andcommitted
feat(channel): add response sanitizer + fix setup.sh dep install
Adds sanitizeAgentResponse() to strip internal noise from agent messages before they reach users: - Reasoning: _italic block_ prefixes (leading and mid-text) - Italic self-talk preamble (_thinking out loud_ before the answer) - System identity blocks (🧭 Identity, 🦞 OpenClaw status) - Metadata lines (Runtime, Channel, Session, etc.) - [self-reference] tags - Reasoning toggle lines (Reasoning: on/off) Applied at all three message paths: async dispatch, sync dispatch, and outbound sendText. Also ports org_id fallback and agent_name fields to error callbacks. Fixes setup.sh sync to run npm install after copying extension files. Without this, rsync wipes node_modules and the plugin fails to load (@sinclair/typebox missing). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a700cc1 commit 88871ff

2 files changed

Lines changed: 110 additions & 3 deletions

File tree

extension/channel/ax-channel.ts

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
/^Reasoning:\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+
/Reasoning:\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+
/^Reasoning:\s*[^\n]*(?:\n(?!\n)[^\n]*)*/i,
157+
""
158+
);
159+
160+
// 4. Strip 🧭 Identity blocks
161+
text = text.replace(
162+
/^🧭\s*Identity\s*\n(?:(?:Channel|User\s*id|AllowFrom|Session|Runtime|Host|Model)[^\n]*\n?)*/gim,
163+
""
164+
);
165+
166+
// 5. Strip 🦞 OpenClaw status blocks
167+
text = text.replace(
168+
/^🦞\s*OpenClaw[^\n]*(?:\n(?![\n\r])[^\n]*)*/gim,
169+
""
170+
);
171+
172+
// 6. Strip standalone system metadata lines
173+
text = text.replace(
174+
/^\s*(?:[*_`~>]+\s*)?(?:Runtime|Channel|Session|Agent|Identity|AllowFrom|User\s*id)(?:\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*)?(?:Thinking|Reasoning)(?:\s*[*_`~]+)?\s*:\s*(?:[*_`~]+\s*)?(?:on|off|enabled|disabled)\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
119211
type 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) {

setup.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,15 @@ update_extension() {
161161
cp -r "$source_dir/"* "$EXTENSION_DIR/"
162162
fi
163163

164+
# Install dependencies if package.json exists
165+
if [[ -f "$EXTENSION_DIR/package.json" ]]; then
166+
log_info "Installing dependencies..."
167+
(cd "$EXTENSION_DIR" && npm install --omit=dev --no-audit --no-fund --loglevel=error 2>&1) || {
168+
log_warn "npm install had warnings (non-fatal)"
169+
}
170+
log_ok "Dependencies installed"
171+
fi
172+
164173
# Ensure installs manifest exists in config
165174
local has_install=$(jq -r '.plugins.installs["ax-platform"] // empty' "$CONFIG_FILE")
166175
if [[ -z "$has_install" ]]; then

0 commit comments

Comments
 (0)