import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/plugin-entry'; import { logger } from '../util/logger.js'; type AgentEvent = { runId?: unknown; stream?: unknown; data?: unknown; [key: string]: unknown; }; type AgentRunContext = { verboseLevel?: string; [key: string]: unknown; }; type AgentEventState = { listeners?: Set<(evt: AgentEvent) => void>; runContextById?: Map; }; const AGENT_EVENT_STATE_KEY = Symbol.for('openclaw.agentEvents.state'); const REMOTE_TOOL_CALL_IDS = new Set(); const REMOTE_TOOL_CALL_IDS_BY_RUN = new Map>(); const PROMOTED_RUNS = new Set(); let installed = false; let missingStateWarned = false; let missingRuntimeWarned = false; function asRecord(value: unknown): Record { return value && typeof value === 'object' ? value as Record : {}; } function readString(value: unknown): string { return typeof value === 'string' ? value : ''; } function readToolName(data: Record): string { const call = asRecord(data.call); return ( readString(data.name) || readString(data.toolName) || readString(data.tool_name) || readString(call.name) ).toLowerCase(); } function readToolCallId(data: Record): string { const call = asRecord(data.call); return ( readString(data.toolCallId) || readString(data.tool_call_id) || readString(data.id) || readString(call.id) ); } function isClawlinkRemoteToolName(name: string): boolean { return name.includes('call_remote_agent') || name.includes('clawlink_delegate'); } function rememberRemoteToolCall(runId: string, toolCallId: string): void { REMOTE_TOOL_CALL_IDS.add(toolCallId); let runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId); if (!runToolCallIds) { runToolCallIds = new Set(); REMOTE_TOOL_CALL_IDS_BY_RUN.set(runId, runToolCallIds); } runToolCallIds.add(toolCallId); } function forgetRemoteToolCall(runId: string, toolCallId: string): void { REMOTE_TOOL_CALL_IDS.delete(toolCallId); const runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId); if (!runToolCallIds) return; runToolCallIds.delete(toolCallId); if (runToolCallIds.size === 0) { REMOTE_TOOL_CALL_IDS_BY_RUN.delete(runId); } } function forgetRun(runId: string): void { PROMOTED_RUNS.delete(runId); const runToolCallIds = REMOTE_TOOL_CALL_IDS_BY_RUN.get(runId); if (!runToolCallIds) return; for (const toolCallId of runToolCallIds) { REMOTE_TOOL_CALL_IDS.delete(toolCallId); } REMOTE_TOOL_CALL_IDS_BY_RUN.delete(runId); } function getAgentEventState(): AgentEventState | null { const state = (globalThis as Record)[AGENT_EVENT_STATE_KEY]; if (!state || typeof state !== 'object') return null; return state as AgentEventState; } function promoteRunVerboseLevel(runId: string, toolCallId: string, toolName: string): void { const state = getAgentEventState(); const runContextById = state?.runContextById; if (!(runContextById instanceof Map)) { if (!missingStateWarned) { missingStateWarned = true; logger.warn('[verbose-preflight] OpenClaw agent event state is unavailable; ClawLink SSE payload may be stripped'); } return; } let ctx = runContextById.get(runId); if (!ctx) { ctx = {}; runContextById.set(runId, ctx); } if (ctx.verboseLevel !== 'full') { ctx.verboseLevel = 'full'; } if (!PROMOTED_RUNS.has(runId)) { PROMOTED_RUNS.add(runId); logger.info(`[verbose-preflight] promoted run verboseLevel=full (runId=${runId}, toolCallId=${toolCallId || 'unknown'}, tool=${toolName || 'unknown'})`); } } function handleAgentEvent(evt: AgentEvent): void { const runId = readString(evt.runId); if (!runId) return; const data = asRecord(evt.data); if (evt.stream === 'lifecycle') { const phase = readString(data.phase); if (phase === 'end' || phase === 'error') { forgetRun(runId); } return; } if (evt.stream !== 'tool') return; const toolName = readToolName(data); const toolCallId = readToolCallId(data); if (toolCallId && isClawlinkRemoteToolName(toolName)) { rememberRemoteToolCall(runId, toolCallId); } const shouldKeepPayload = isClawlinkRemoteToolName(toolName) || (toolCallId !== '' && REMOTE_TOOL_CALL_IDS.has(toolCallId)); if (!shouldKeepPayload) return; promoteRunVerboseLevel(runId, toolCallId, toolName); const phase = readString(data.phase); if (toolCallId && (phase === 'result' || phase === 'error' || phase === 'end')) { forgetRemoteToolCall(runId, toolCallId); } } function prioritizePreflightListener(listener: (evt: AgentEvent) => void): void { const listeners = getAgentEventState()?.listeners; if (!(listeners instanceof Set) || !listeners.has(listener)) return; const rest = Array.from(listeners).filter(item => item !== listener); listeners.clear(); listeners.add(listener); for (const item of rest) listeners.add(item); logger.debug('[verbose-preflight] listener moved before gateway subscribers'); } export function installClawlinkVerbosePreflight(api: OpenClawPluginApi): void { if (installed) return; const onAgentEvent = api.runtime?.events?.onAgentEvent; if (typeof onAgentEvent !== 'function') { if (!missingRuntimeWarned) { missingRuntimeWarned = true; logger.warn('[verbose-preflight] api.runtime.events.onAgentEvent is unavailable; ClawLink SSE payload preflight disabled'); } return; } onAgentEvent(handleAgentEvent); prioritizePreflightListener(handleAgentEvent); installed = true; logger.info('[verbose-preflight] installed'); }