Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 53 additions & 2 deletions extension/channel/ax-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>();

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 = {
Expand Down Expand Up @@ -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<string, unknown>;

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) {
Expand Down
41 changes: 10 additions & 31 deletions extension/hooks/ax-bootstrap/handler.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading