openclaw/src/gateway/assistant-identity.ts

132 lines
3.9 KiB
TypeScript
Raw Normal View History

import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { resolveAgentIdentity } from "../agents/identity.js";
import { loadAgentIdentity } from "../commands/agents.config.js";
import type { OpenClawConfig } from "../config/config.js";
import { normalizeAgentId } from "../routing/session-key.js";
import {
isAvatarHttpUrl,
isAvatarImageDataUrl,
looksLikeAvatarPath,
} from "../shared/avatar-policy.js";
const MAX_ASSISTANT_NAME = 50;
const MAX_ASSISTANT_AVATAR = 200;
2026-02-02 21:31:17 -05:00
const MAX_ASSISTANT_EMOJI = 16;
feat(compaction): add adaptive chunk sizing, progressive fallback, and UI indicator (#1466) * fix(ui): allow relative URLs in avatar validation The isAvatarUrl check only accepted http://, https://, or data: URLs, but the /avatar/{agentId} endpoint returns relative paths like /avatar/main. This caused local file avatars to display as text instead of images. Fixes avatar display for locally configured avatar files. * fix(gateway): resolve local avatars to URL in HTML injection and RPC The frontend fix alone wasn't enough because: 1. serveIndexHtml() was injecting the raw avatar filename into HTML 2. agent.identity.get RPC was returning raw filename, overwriting the HTML-injected value Now both paths resolve local file avatars (*.png, *.jpg, etc.) to the /avatar/{agentId} endpoint URL. * feat(compaction): add adaptive chunk sizing and progressive fallback - Add computeAdaptiveChunkRatio() to reduce chunk size for large messages - Add isOversizedForSummary() to detect messages too large to summarize - Add summarizeWithFallback() with progressive fallback: - Tries full summarization first - Falls back to partial summarization excluding oversized messages - Notes oversized messages in the summary output - Add SAFETY_MARGIN (1.2x) buffer for token estimation inaccuracy - Reduce MIN_CHUNK_RATIO to 0.15 for very large messages This prevents compaction failures when conversations contain unusually large tool outputs or responses that exceed the summarization model's context window. * feat(ui): add compaction indicator and improve event error handling Compaction indicator: - Add CompactionStatus type and handleCompactionEvent() in app-tool-stream.ts - Show '🧹 Compacting context...' toast while active (with pulse animation) - Show '🧹 Context compacted' briefly after completion - Auto-clear toast after 5 seconds - Add CSS styles for .callout.info, .callout.success, .compaction-indicator Error handling improvements: - Wrap onEvent callback in try/catch in gateway.ts to prevent errors from breaking the WebSocket message handler - Wrap handleGatewayEvent in try/catch with console.error logging to isolate errors and make them visible in devtools These changes address UI freezes during heavy agent activity by: 1. Showing users when compaction is happening 2. Preventing uncaught errors from silently breaking the event loop * fix(control-ui): add agentId to DEFAULT_ASSISTANT_IDENTITY TypeScript inferred the union type without agentId when falling back to DEFAULT_ASSISTANT_IDENTITY, causing build errors at control-ui.ts:222-223.
2026-01-23 01:32:30 -05:00
export const DEFAULT_ASSISTANT_IDENTITY: AssistantIdentity = {
agentId: "main",
name: "Assistant",
avatar: "A",
};
export type AssistantIdentity = {
agentId: string;
name: string;
avatar: string;
2026-02-02 21:31:17 -05:00
emoji?: string;
};
function coerceIdentityValue(value: string | undefined, maxLength: number): string | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (trimmed.length <= maxLength) {
return trimmed;
}
return trimmed.slice(0, maxLength);
}
function isAvatarUrl(value: string): boolean {
return isAvatarHttpUrl(value) || isAvatarImageDataUrl(value);
}
function normalizeAvatarValue(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (isAvatarUrl(trimmed)) {
return trimmed;
}
if (looksLikeAvatarPath(trimmed)) {
return trimmed;
}
if (!/\s/.test(trimmed) && trimmed.length <= 4) {
return trimmed;
}
return undefined;
}
2026-02-02 21:31:17 -05:00
function normalizeEmojiValue(value: string | undefined): string | undefined {
if (!value) {
return undefined;
}
const trimmed = value.trim();
if (!trimmed) {
return undefined;
}
if (trimmed.length > MAX_ASSISTANT_EMOJI) {
return undefined;
}
let hasNonAscii = false;
for (let i = 0; i < trimmed.length; i += 1) {
if (trimmed.charCodeAt(i) > 127) {
hasNonAscii = true;
break;
}
}
if (!hasNonAscii) {
return undefined;
}
if (isAvatarUrl(trimmed) || looksLikeAvatarPath(trimmed)) {
return undefined;
}
return trimmed;
}
export function resolveAssistantIdentity(params: {
2026-01-30 03:15:10 +01:00
cfg: OpenClawConfig;
agentId?: string | null;
workspaceDir?: string | null;
}): AssistantIdentity {
const agentId = normalizeAgentId(params.agentId ?? resolveDefaultAgentId(params.cfg));
const workspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, agentId);
const configAssistant = params.cfg.ui?.assistant;
const agentIdentity = resolveAgentIdentity(params.cfg, agentId);
const fileIdentity = workspaceDir ? loadAgentIdentity(workspaceDir) : null;
const name =
coerceIdentityValue(configAssistant?.name, MAX_ASSISTANT_NAME) ??
coerceIdentityValue(agentIdentity?.name, MAX_ASSISTANT_NAME) ??
coerceIdentityValue(fileIdentity?.name, MAX_ASSISTANT_NAME) ??
DEFAULT_ASSISTANT_IDENTITY.name;
const avatarCandidates = [
coerceIdentityValue(configAssistant?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_AVATAR),
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_AVATAR),
];
const avatar =
avatarCandidates.map((candidate) => normalizeAvatarValue(candidate)).find(Boolean) ??
DEFAULT_ASSISTANT_IDENTITY.avatar;
2026-02-02 21:31:17 -05:00
const emojiCandidates = [
coerceIdentityValue(agentIdentity?.emoji, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(fileIdentity?.emoji, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(agentIdentity?.avatar, MAX_ASSISTANT_EMOJI),
coerceIdentityValue(fileIdentity?.avatar, MAX_ASSISTANT_EMOJI),
];
const emoji = emojiCandidates.map((candidate) => normalizeEmojiValue(candidate)).find(Boolean);
return { agentId, name, avatar, emoji };
}