From b0ba95356efb012d3f2bb82329825ed614444561 Mon Sep 17 00:00:00 2001 From: Cipher Date: Mon, 23 Feb 2026 02:17:19 +0000 Subject: [PATCH 1/2] fix(context): eliminate redundant context injection causing overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two remaining issues were causing context overflow for clawdbot_cipher: 1. Double injection: mission briefing was injected via BOTH before_agent_start hook (prependContext) AND agent:bootstrap hook (AX_MISSION.md bootstrap file). Disabled bootstrap hook — single injection point is now before_agent_start in index.ts. 2. Redundant conversation history: briefing re-injected up to 10 messages (~3-5k tokens) per dispatch, duplicating Clawdbot's native session history. Removed history section entirely — Clawdbot handles this natively. Briefing now contains only identity + collaborators. Result: ~100-200 tokens per dispatch (down from 2-5k). Co-Authored-By: Claude Opus 4.6 --- extension/hooks/ax-bootstrap/handler.ts | 41 +--- extension/lib/context.ts | 239 ++++-------------------- 2 files changed, 45 insertions(+), 235 deletions(-) diff --git a/extension/hooks/ax-bootstrap/handler.ts b/extension/hooks/ax-bootstrap/handler.ts index 06276af..e5b2f78 100644 --- a/extension/hooks/ax-bootstrap/handler.ts +++ b/extension/hooks/ax-bootstrap/handler.ts @@ -1,39 +1,18 @@ /** - * aX Bootstrap Hook - * Injects mission briefing into agent bootstrap files + * aX Bootstrap Hook — DISABLED + * + * Previously injected mission briefing as a bootstrap file (AX_MISSION.md). + * This caused double context injection because the same briefing was also + * injected via the before_agent_start hook in index.ts (prependContext). + * + * The before_agent_start hook is now the single injection point. + * This handler is kept as a no-op to avoid plugin load errors. */ import type { HookHandler } from "clawdbot/hooks"; -import { buildMissionBriefing } from "../../lib/context.js"; -import { getDispatchSession } from "../../channel/ax-channel.js"; -const handler: HookHandler = async (event) => { - // Only handle agent:bootstrap events - if (event.type !== "agent" || event.action !== "bootstrap") { - return; - } - - // Check if this is an aX session - const sessionKey = event.sessionKey; - if (!sessionKey) return; - - const session = getDispatchSession(sessionKey); - if (!session) return; - - // Build mission briefing - const briefing = buildMissionBriefing( - session.agentHandle, - session.spaceName, - session.senderHandle, - session.senderType, - session.contextData - ); - - // Inject as bootstrap file - event.context.bootstrapFiles?.push({ - name: "AX_MISSION.md", - content: briefing, - }); +const handler: HookHandler = async (_event) => { + // No-op: context injection is handled by before_agent_start in index.ts }; export default handler; diff --git a/extension/lib/context.ts b/extension/lib/context.ts index a49994d..ca6bd9e 100644 --- a/extension/lib/context.ts +++ b/extension/lib/context.ts @@ -1,101 +1,25 @@ /** * Build mission briefing context from dispatch payload - * - * Design principles (by @clawdbot_cipher, first webhook agent): - * 1. Recent messages should be COMPLETE - no truncation for the last few - * 2. Older messages can be summarized but shouldn't disappear silently - * 3. Agents should know when context was trimmed - * 4. Message IDs enable fetching full content if needed - * 5. Think about the NEXT webhook agent - make this experience fluid + * + * Optimized for minimal token usage (<500 tokens target). + * Conversation history is NOT included here — Clawdbot's native session + * history handles that. This briefing only provides aX-specific context + * that Clawdbot doesn't know: identity, collaborators, and sender info. */ -import type { ContextData, Message } from "./types.js"; +import type { ContextData } from "./types.js"; -// Configuration - can be overridden per-agent in the future -// NOTE (2026-02-19): Reduced history window to prevent context overflow in active threads. -// Group threads with many long agent messages were pushing the injected context over -// the model's limit even on fresh sessions (since history is re-injected per message). -// Previous: 5+15=20 messages. Current: 3+7=10 messages. const DEFAULT_CONFIG = { - // How many recent messages to include at full length - fullLengthMessages: 3, - // How many additional messages to include (truncated) - additionalMessages: 7, - // Max chars for truncated messages (older ones in the window) - truncatedMaxChars: 400, - // Max chars for full-length messages (most recent) - fullMaxChars: 1500, - // Max agent descriptions - maxAgentDescChars: 120, - // Max agents to show - maxAgents: 15, + maxAgentDescChars: 80, + maxAgents: 8, }; -// When truncating, try to break at word boundary if we're past this ratio -const WORD_BOUNDARY_THRESHOLD = 0.8; - -// Fallback for empty message content -const EMPTY_MESSAGE_FALLBACK = "(empty message)"; - -/** - * Format a single message for the context - */ -function formatMessage( - msg: Message, - maxChars: number, - includeTimestamp: boolean = false -): string { - const authorIcon = msg.author_type === "agent" ? "🤖" : "👤"; - const timestamp = includeTimestamp && msg.timestamp - ? ` (${formatRelativeTime(msg.timestamp)})` - : ""; - - // Handle empty or missing content - let content = msg.content?.trim() || EMPTY_MESSAGE_FALLBACK; - - if (content.length > maxChars) { - content = content.substring(0, maxChars).trim(); - // Try to break at a word boundary if we're past the threshold - const lastSpace = content.lastIndexOf(" "); - if (lastSpace > maxChars * WORD_BOUNDARY_THRESHOLD) { - content = content.substring(0, lastSpace); - } - content += " [...]"; - } - - // Preserve line breaks but collapse excessive whitespace - content = content.replace(/\n{3,}/g, "\n\n"); - - return `${authorIcon} **@${msg.author}**${timestamp}: ${content}`; -} - -/** - * Format relative time from ISO timestamp - */ -function formatRelativeTime(isoTimestamp: string): string { - try { - const date = new Date(isoTimestamp); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - - if (diffMins < 1) return "just now"; - if (diffMins < 60) return `${diffMins}m ago`; - - const diffHours = Math.floor(diffMins / 60); - if (diffHours < 24) return `${diffHours}h ago`; - - const diffDays = Math.floor(diffHours / 24); - return `${diffDays}d ago`; - } catch (err) { - console.warn(`[ax-platform] Failed to parse timestamp "${isoTimestamp}":`, err); - return ""; - } -} - /** * Build mission briefing markdown from context data * This will be injected via before_agent_start hook using prependContext + * + * Kept lean: identity + collaborators + sender type only. + * Conversation history is handled by Clawdbot's native session context. */ export function buildMissionBriefing( agentHandle: string, @@ -107,134 +31,55 @@ export function buildMissionBriefing( ): string { const lines: string[] = []; - // ═══════════════════════════════════════════════════════════════════════════ - // IDENTITY SECTION - // ═══════════════════════════════════════════════════════════════════════════ + // Identity (essential — agent must know who it is on aX) lines.push("# aX Platform Context"); - lines.push(""); - lines.push("## Your Identity"); - lines.push(`**IMPORTANT: You ARE ${agentHandle}.**`); - lines.push(`When someone @mentions ${agentHandle}, they are talking to YOU. You must respond.`); - lines.push(`When you see messages addressed to ${agentHandle}, those messages are FOR YOU.`); - lines.push(""); - lines.push(`- **Your handle:** ${agentHandle}`); - lines.push(`- **Current space:** ${spaceName}`); - lines.push(`- **Message from:** @${senderHandle}`); + lines.push(`You ARE ${agentHandle} in space "${spaceName}".`); + lines.push(`Message from: @${senderHandle}${senderType ? ` (${senderType})` : ""}`); + if (senderType === "mcp_agent") { + lines.push(`MCP agents require @mention to receive your response.`); + } lines.push(""); - // ═══════════════════════════════════════════════════════════════════════════ - // COLLABORATORS SECTION - // ═══════════════════════════════════════════════════════════════════════════ + // Collaborators (so agent knows who else is available) if (contextData?.agents && contextData.agents.length > 0) { - lines.push("## Other Agents in This Space"); - lines.push("These are OTHER agents you can @mention to collaborate with:"); - - // Filter out self first, then slice const otherAgents = contextData.agents.filter(agent => { const handle = `@${agent.name}`; return handle !== agentHandle && agent.name !== agentHandle.replace("@", ""); }); - - const displayedAgents = otherAgents.slice(0, config.maxAgents); - - for (const agent of displayedAgents) { - const typeIcon = agent.type === "sentinel" ? "🛡️" - : agent.type === "assistant" ? "🤖" - : "👤"; - - let desc = ""; - if (agent.description) { - desc = agent.description.length > config.maxAgentDescChars - ? ` - ${agent.description.substring(0, config.maxAgentDescChars)}...` - : ` - ${agent.description}`; - } - lines.push(`- @${agent.name} ${typeIcon}${desc}`); - } - - // Note if agents were omitted - use filtered array length - const omittedCount = otherAgents.length - displayedAgents.length; - if (omittedCount > 0) { - lines.push(`- *(${omittedCount} more agents in space)*`); - } - lines.push(""); - } - // ═══════════════════════════════════════════════════════════════════════════ - // CONVERSATION HISTORY SECTION - // ═══════════════════════════════════════════════════════════════════════════ - if (contextData?.messages && contextData.messages.length > 0) { - lines.push("## Recent Conversation"); - - const allMessages = contextData.messages; - const totalMessages = allMessages.length; - const windowSize = config.fullLengthMessages + config.additionalMessages; - - // If we have more messages than our window, note what's missing - if (totalMessages > windowSize) { - const droppedCount = totalMessages - windowSize; - lines.push(`*(${droppedCount} earlier messages not shown)*`); - lines.push(""); - } - - // Take the messages we'll display - const displayMessages = allMessages.slice(-windowSize); - - // Split into truncated (older) and full-length (recent) - const truncatedMessages = displayMessages.slice(0, -config.fullLengthMessages); - const recentMessages = displayMessages.slice(-config.fullLengthMessages); - - // Render truncated messages (older context) - if (truncatedMessages.length > 0) { - lines.push("### Earlier in Thread"); - for (const msg of truncatedMessages) { - lines.push(formatMessage(msg, config.truncatedMaxChars, true)); + if (otherAgents.length > 0) { + lines.push("## Agents in Space"); + const displayed = otherAgents.slice(0, config.maxAgents); + for (const agent of displayed) { + let desc = ""; + if (agent.description) { + desc = agent.description.length > config.maxAgentDescChars + ? ` - ${agent.description.substring(0, config.maxAgentDescChars)}...` + : ` - ${agent.description}`; + } + lines.push(`- @${agent.name}${desc}`); } - lines.push(""); - } - - // Render recent messages (full length) - if (recentMessages.length > 0) { - if (truncatedMessages.length > 0) { - lines.push("### Recent Messages"); - } - for (const msg of recentMessages) { - lines.push(formatMessage(msg, config.fullMaxChars, true)); + const omitted = otherAgents.length - displayed.length; + if (omitted > 0) { + lines.push(`- (${omitted} more)`); } lines.push(""); } } - // ═══════════════════════════════════════════════════════════════════════════ - // CURRENT MESSAGE SECTION - // ═══════════════════════════════════════════════════════════════════════════ - lines.push("## This Message"); - - // Sender type context for protocol decisions - if (senderType === "mcp_agent") { - lines.push(`From: @${senderHandle} (mcp_agent)`); - lines.push(`*→ MCP agents require @mention to receive your response*`); - } else if (senderType === "cloud_agent") { - lines.push(`From: @${senderHandle} (cloud_agent)`); - lines.push(`*→ Cloud agents see all messages in space*`); - } else { - lines.push(`From: @${senderHandle} (user)`); - } - lines.push(""); - return lines.join("\n"); } /** * Estimate token count for context (rough approximation) - * Useful for future token budget management */ export function estimateContextTokens(context: string): number { - // Rough estimate: ~4 chars per token for English text return Math.ceil(context.length / 4); } /** - * Build a compact context for token-constrained situations + * Compact briefing alias — now identical to buildMissionBriefing since + * the standard briefing is already minimal. */ export function buildCompactMissionBriefing( agentHandle: string, @@ -243,19 +88,5 @@ export function buildCompactMissionBriefing( senderType?: string, contextData?: ContextData ): string { - return buildMissionBriefing( - agentHandle, - spaceName, - senderHandle, - senderType, - contextData, - { - fullLengthMessages: 3, - additionalMessages: 5, - truncatedMaxChars: 200, - fullMaxChars: 800, - maxAgentDescChars: 60, - maxAgents: 8, - } - ); + return buildMissionBriefing(agentHandle, spaceName, senderHandle, senderType, contextData); } From eb0f89780637198b1bd543d08e8dea14d432e863 Mon Sep 17 00:00:00 2001 From: Cipher Date: Mon, 23 Feb 2026 03:30:54 +0000 Subject: [PATCH 2/2] fix(plugin): suppress raw overflow/rate-limit text in channel output --- extension/channel/ax-channel.ts | 55 +++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/extension/channel/ax-channel.ts b/extension/channel/ax-channel.ts index 7949c0d..e379cdb 100644 --- a/extension/channel/ax-channel.ts +++ b/extension/channel/ax-channel.ts @@ -78,6 +78,42 @@ const DEDUP_TTL_MS = 15 * 60 * 1000; // 15 minutes (must exceed backend timeout const DEDUP_CLEANUP_INTERVAL_MS = 60 * 1000; // Clean up every minute const SESSION_CLEANUP_DELAY_MS = 1000; // Grace period before session cleanup const LOG_PREVIEW_LENGTH = 100; // Max chars to show in log previews +const RECOVERY_NOTICE_COOLDOWN_MS = 2 * 60 * 1000; // 2 minutes per space + +// Raw internal errors that should never be forwarded verbatim to users +const RAW_INTERNAL_ERROR_PATTERNS = { + context: [ + "context overflow", + "prompt too large for the model", + "try /reset", + "try /new", + ], + rate: [ + "api rate limit reached", + "rate limit", + "too many requests", + ], +}; + +const recoveryNoticeBySpace = new Map(); + +function classifyInternalErrorText(text: string): "context" | "rate" | null { + const normalized = text.toLowerCase(); + if (RAW_INTERNAL_ERROR_PATTERNS.context.some((p) => normalized.includes(p))) { + return "context"; + } + if (RAW_INTERNAL_ERROR_PATTERNS.rate.some((p) => normalized.includes(p))) { + return "rate"; + } + return null; +} + +function buildRecoveryNotice(kind: "context" | "rate"): string { + if (kind === "context") { + return "Quick heads up — I’m compacting internal context and will follow with a clean response shortly."; + } + return "Quick heads up — I hit temporary model capacity and am retrying now. I’ll send the full response shortly."; +} // Dispatch state for deduplication type DispatchState = { @@ -728,14 +764,29 @@ export function createAxChannel(config: { return { channel: "ax-platform", ok: false, error: "No target space" }; } + let outboundText = text || ""; + const internalErrorKind = classifyInternalErrorText(outboundText); + if (internalErrorKind) { + const now = Date.now(); + const lastNoticeAt = recoveryNoticeBySpace.get(spaceId) || 0; + if (now - lastNoticeAt < RECOVERY_NOTICE_COOLDOWN_MS) { + logger.info(`[ax-platform] Suppressing duplicate internal error notice (${internalErrorKind}) for ${spaceId}`); + return { channel: "ax-platform", ok: true, suppressed: true }; + } + + outboundText = buildRecoveryNotice(internalErrorKind); + recoveryNoticeBySpace.set(spaceId, now); + logger.info(`[ax-platform] Replaced raw internal error text with graceful recovery notice (${internalErrorKind})`); + } + try { const result = await callAxTool(mcpEndpoint, authToken, "messages", { action: "send", - content: text, + content: outboundText, space_id: spaceId, }) as Record; - const preview = text.length > 50 ? text.substring(0, 50) + "..." : text; + const preview = outboundText.length > 50 ? outboundText.substring(0, 50) + "..." : outboundText; logger.info(`[ax-platform] Outbound sent to ${spaceId}: ${preview}`); return { channel: "ax-platform", ok: true, messageId: result.message_id }; } catch (err) {