beeos-claw 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{randomUUID,createHash}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas/tools.js";import{parseMentionsParam,buildMentionInstruction}from"../a2a/mention-resolver.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(s=>e.includes(s))}function isSystemDefaultSessionKey(e){const s=e.split(":");if(s.length<3||"agent"!==s[0])return!1;const t=s.slice(2).join(":");return"main"===t||t===s[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let s=stripTransportMetadata(e);return/^\[System\b/.test(s)?void 0:(s=s.trim(),s||void 0)}function resolveStandaloneSlashCommandName(e,s){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const t=s.trim();if(!t||t.includes("\n")||!t.startsWith("/"))return;const n=t.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,s){const t=createHash("sha256").update(`${s}:${e}`).digest("hex").substring(0,32);return`session:${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}function parseAgentIdFromKey(e){const s=e.split(":");return(s.length>=3&&"agent"===s[0]?s[1]:"")||"main"}function createSessionObject(e){const s=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:s,platformAgentId:e.resolvePlatformAgentId?.(s),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;initialized=!1;agentMapping=null;_syncResolvers=[];setAgentMapping(e){this.agentMapping=e;for(const s of this.state.sessions.values())!s.platformAgentId&&s.agentId&&(s.platformAgentId=e.get(s.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[]}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(s=>{this._syncResolvers.push(s);const t=setTimeout(()=>{const e=this._syncResolvers.indexOf(s);-1!==e&&this._syncResolvers.splice(e,1),s()},e);"object"==typeof t&&"unref"in t&&t.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown",this.thirdPartyChannels=e.thirdPartyChannels??new Set}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e){this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e)}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":await this.handleSessionDelete(e);break;case"session/clear":await this.handleSessionClear(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:s instanceof Error?s.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const s=e.params,t=s?.surfaceId,n=this.state.resolveSessionFromParams(s);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${s?.sessionId}`);break}n.referencedSurfaceId=t||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${t?"set":"cleared"}: ${t??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const s=this.transport.forwardControl;!1===e.params?.forwardThinking&&(s.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(s.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const s=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,t="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const s=this.getGateway();s?.isConnected&&loadHistory(s,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.sessionsList();if(!t||"object"!=typeof t)return;const n=t.sessions;if(!Array.isArray(n))return;for(const s of n){const t=s,n=t.sessionKey??t.key??t.id??t.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(e),e}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let s=this.state.sessions.get(e);if(!s&&e.startsWith("agent:")&&(s=this.state.findSessionByKey(e)),s||e.startsWith("agent:")||(s=await this.recoverSessionFromGateway(e)),s)return{session:s,created:!1};const t=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return s=createSessionObject({gatewayKey:t?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(s),{session:s,created:!0}}async handleSessionLoad(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:t}=await this.resolveOrCreateSession(s),n=t.sessionId;this.state.lastActiveSessionId=n,t.chatReplayDedupKeys.clear();try{const e=this.getGateway(),s=await loadHistory(e,t.gatewaySessionKey,50),i=groupIntoTurns(s),o=i.filter(e=>"user"===e.role).length,a=i.filter(e=>"assistant"===e.role).length,r=i.filter(e=>e.toolCalls?.length).length,d=i.reduce((e,s)=>e+(s.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${s.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const s=i[e];if("user"===s.role&&"internal_system"===s.provenance?.kind){this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`);continue}if("system"===s.role){this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${s.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:s.content},metadata:s.metadata});continue}this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${s.role} contentLen=${s.content.length} content=${JSON.stringify(s.content.slice(0,80))} tcCount=${s.toolCalls?.length??0}`);const t={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls};s.metadata&&(t.metadata=s.metadata),this.transport.sendSessionUpdate(n,t)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,t.gatewaySessionKey,t.platformAgentId);void 0!==t.pendingPromptId&&!t.promptDone&&t.assistantMq&&!t.assistantMq.closed&&this.replayCurrentAssistantStream(t).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const s=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,t=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<t&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const t=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),s))]);if(!t||0===t.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const s of t)this.transport.sendSessionUpdate(e.sessionId,s),n++}n>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:n}})}async handleSessionList(e){const s=[];for(const[,e]of this.state.sessions)s.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const t=this.getGateway();if(t?.isConnected)try{const e=await t.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const t=e.sessions;for(const e of t){const t=e,n=t.sessionKey??t.key??t.id??t.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=s.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=t.label||cleanGatewayDerivedTitle(t.derivedTitle)||t.title||void 0;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const s=this.state.sessions.get(o.sessionId);s&&!s.title&&(s.title=e)}const e=t.updatedAt;e&&(o.updatedAt=e)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t.cwd,title:a||"New chat"});this.registerSession(e),s.push({sessionId:e.sessionId,title:a||"New chat",cwd:t.cwd,updatedAt:t.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n)})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}s.sort((e,s)=>(s.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(s.sessionId)),this.transport.sendResult(e.id,{sessions:s})}handleSessionRename(e){const s=e.params?.sessionId,t=e.params?.title;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=t.slice(0,64),i=this.state.sessions.get(s);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n;const o=i.gatewaySessionKey,a=this.getGateway();a?.isConnected&&a.sessionsPatch(o,{label:n}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:s,title:n})}async handleSessionDelete(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.info?.(`[bridge-core] session/delete sessionId=${s} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=t.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${s} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(s){const t=s instanceof Error?s.message:String(s);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${t}`),!t.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(s){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${s instanceof Error?s.message:s}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==t.pendingPromptId&&this.failPrompt(t,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(t),this.state.clearStreamIdleTimer(t),t.assistantMq?.close(),t.currentRunId&&this.state.promptsByRunId.delete(t.currentRunId);const a=new Set;for(const[e,t]of this.state.toolCallToSessionMap)t===s&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,s]of this.state.toolCallIdMap)a.has(s)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(s),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(s),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${s} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const s=e.params?.sessionId;if(!s)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${s} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${s} gatewayKey=${t.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&t.gatewaySessionKey)try{await n.sessionsReset(t.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${t.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${t.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${t.gatewaySessionKey}`);t.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(s,t.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const s=e.params?.sessionId,t=e.params?.mode,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t){const e={};if("verbose"===t||"detailed"===t?e.verboseLevel="full":"concise"!==t&&"default"!==t||(e.verboseLevel="default"),"reasoning"!==t&&"thinking"!==t||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:t??"default"})}async handleSessionSetModel(e){const s=e.params?.sessionId,t=e.params?.model,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t)try{await i.sessionsPatch(n.gatewaySessionKey,{model:t})}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:t??"default"})}async handleSkillsList(e){const s=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const s=await e.skillsStatus();return s?.skills?s.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,s)=>e.always&&!s.always?-1:!e.always&&s.always?1:e.name.localeCompare(s.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const s=e.params?.sessionId,t=e.params?.prompt;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(s);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let r=extractPromptBlocks(sanitizePromptPayload(t));if(!n.title){const e=r.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),a?.isConnected&&n.title&&a.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}))}const d=buildResolutionPlan(r);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);r=applyResolutions(r,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const l=toGatewayPrompt(r);if("error"in l)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:l.error});const{message:c,attachments:h}=l,g=resolveStandaloneSlashCommandName(r,c)??resolveSlashCmd(r,c),m="string"==typeof g;if(g&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(g)}catch(e){return s.logger.warn?.(`[bridge-core] /${g} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${g}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const p=m?"chat.send":"agent";let u=m?c:withUserMessagePrefix(c);const f=this.config.canvasEnabled&&!m&&!1!==n.canvasActive&&!n.channelType;if(f){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(u=`${CANVAS_TERM_MAPPING}\n\n${u}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(f){const e=this.getCanvasState?.();if(e){const s=e.resolveCanvasForSession(n.gatewaySessionKey);u=`${buildActiveSurfacesContext(e,s)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${u}`}const s=n.referencedSurfaceId;s&&(u=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${s}" for editing. You MUST call render_ui with id="${s}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${u}`,n.referencedSurfaceId=void 0)}if(!m){const s=parseMentionsParam(e.params?.mentions);if(s.length>0){n.activeMentions=s;u=`${buildMentionInstruction(s)}\n${u}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${s.length} agent(s)`)}else n.activeMentions=void 0}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${g}`),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=p,this.emitPromptAsUserSessionUpdates(o,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=h.length>0?h.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let s;s=m?await a.chatSend(n.gatewaySessionKey,u,e):await a.agentSend(n.gatewaySessionKey,u,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=s.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(s.runId,n)}catch(s){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:s instanceof Error?s.message:"failed to send prompt to gateway"})),n.pendingAgentSends?.length&&(this.logger.warn?.("[bridge-core] Discarding %d deferred agentSends after prompt send failure",n.pendingAgentSends.length),n.pendingAgentSends=void 0)}}emitPromptAsUserSessionUpdates(e,s){for(const t of s){const s=this.toUserSessionUpdateContent(t);s&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource"===e.type){const s=e.resource??{},t=s.uri??e.uri,n=s.mimeType??s.mime_type??e.mimeType??e.mime_type,i=s.text??e.text,o=s.data??e.data,a=s.fileName??s.file_name??s.filename??s.name??e.fileName??e.file_name??e.filename??e.name;if(!t&&!i&&!o)return;return{type:"resource",resource:{...t?{uri:t}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,s){const t=s?.sessionId;if(!t)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(t);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&n.currentRunId&&i.chatAbort(n.gatewaySessionKey,n.currentRunId).catch(e=>{this.logger.debug?.(`[bridge-core] chatAbort failed: ${e instanceof Error?e.message:e}`)}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const s=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;s<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${s}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${s}ms`))},s),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const s=e.currentRunId;if(s){this.state.promptsByRunId.delete(s);const e=`tc_${s}_`;for(const[s,t]of this.state.toolCallIdMap)t.startsWith(e)&&this.state.toolCallIdMap.delete(s)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const t=e.currentRunId;if(t&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(t,s)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:s}),this.cleanupPrompt(e)}failPrompt(e,s,t){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const n=e.currentRunId;n&&this.onRunFailed&&this.onRunFailed(n,t),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,s,t),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,s){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const t="string"==typeof s.state?s.state.toLowerCase():void 0;if(t)if("final"===t)this.completePrompt(e,"end_turn");else if("aborted"===t||"cancelled"===t||"cancel"===t)this.completePrompt(e,"cancelled");else if("error"===t){const t=("string"==typeof s.summary?s.summary:void 0)??("string"==typeof s.message?s.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(t))}}pushToolResultContent(e,s,t,n){if(0===t.length)return;const i=(n?this.state.promptsByRunId.get(n):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${n})`);const o=i.sessionId;for(const e of t){const s={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,s),i.assistantMq?.push(s)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:s,blockCount:t.length,sessionId:o}})}async handleCronRequest(e){const s=this.getGateway();if(!s?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const t=e.method.replace("/",".");try{const n=await s.request(t,e.params??{});this.transport.sendResult(e.id,n??{})}catch(s){const n=s instanceof Error?s.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${t} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}
1
+ import{randomUUID,createHash}from"node:crypto";import{sanitizePromptPayload,stripTransportMetadata}from"../message-filter.js";import{CANVAS_TERM_MAPPING,CANVAS_UPDATE_HINT,buildActiveSurfacesContext}from"../canvas/tools.js";import{parseMentionsParam,buildMentionInstruction}from"../a2a/mention-resolver.js";import{extractPromptBlocks,toGatewayPrompt,resolveSlashCommandName as resolveSlashCmd}from"./prompt-converter.js";import{loadHistory,groupIntoTurns}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS}from"./types.js";const DEFAULT_MODE={id:"default",name:"Default",description:"Default agent mode"},USER_MESSAGE_PREFIX="User Message From BeeOS:",USER_MESSAGE_PREFIX_WITH_NEWLINE=`${USER_MESSAGE_PREFIX}\n`,SLASH_COMMAND_NAME_RE=/^[a-z0-9_-]+$/i;function withUserMessagePrefix(e){return e.startsWith(USER_MESSAGE_PREFIX)?e:`${USER_MESSAGE_PREFIX_WITH_NEWLINE}${e}`}const BACKGROUND_SESSION_PATTERNS=[":cron:",":hook:","node-"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(s=>e.includes(s))}function isSystemDefaultSessionKey(e){const s=e.split(":");if(s.length<3||"agent"!==s[0])return!1;const t=s.slice(2).join(":");return"main"===t||t===s[1]}function cleanGatewayDerivedTitle(e){if(!e)return;let s=stripTransportMetadata(e);return/^\[System\b/.test(s)?void 0:(s=s.trim(),s||void 0)}function resolveStandaloneSlashCommandName(e,s){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const t=s.trim();if(!t||t.includes("\n")||!t.startsWith("/"))return;const n=t.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,s){const t=createHash("sha256").update(`${s}:${e}`).digest("hex").substring(0,32);return`session:${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20,32)}`}function parseAgentIdFromKey(e){const s=e.split(":");return(s.length>=3&&"agent"===s[0]?s[1]:"")||"main"}function createSessionObject(e){const s=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:s,platformAgentId:e.resolvePlatformAgentId?.(s),createdAt:Date.now(),chatReplayDedupKeys:new Set,cwd:e.cwd,title:e.title,channelType:resolveChannelFromSessionKey(e.gatewayKey,e.thirdPartyChannels)}}export class AcpGatewayBridgeCore{state;transport;config;logger;writeObs;getGateway;thirdPartyChannels;initialized=!1;agentMapping=null;_syncResolvers=[];setAgentMapping(e){this.agentMapping=e;for(const s of this.state.sessions.values())!s.platformAgentId&&s.agentId&&(s.platformAgentId=e.get(s.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[]}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(s=>{this._syncResolvers.push(s);const t=setTimeout(()=>{const e=this._syncResolvers.indexOf(s);-1!==e&&this._syncResolvers.splice(e,1),s()},e);"object"==typeof t&&"unref"in t&&t.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;handleSlashCommand=null;onPromptStart=null;instanceId;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.getGateway=e.getGateway,this.instanceId=e.instanceId||"unknown",this.thirdPartyChannels=e.thirdPartyChannels??new Set}resolvePlatformAgentId(e){return this.agentMapping?.get(e)?.id}getDefaultAgentId(){return this.agentMapping&&this.agentMapping.size>0?this.agentMapping.keys().next().value??"main":"main"}registerSession(e){this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e)}async handleRequest(e){this.writeObs({component:"bridge-core",domain:"forward",name:`rpc.request.${e.method}`,severity:"debug",payload:{id:e.id,method:e.method}});try{switch(e.method){case"initialize":this.handleInitialize(e);break;case"session/new":await this.handleSessionNew(e);break;case"session/load":await this.handleSessionLoad(e);break;case"session/list":await this.handleSessionList(e);break;case"session/rename":this.handleSessionRename(e);break;case"session/delete":await this.handleSessionDelete(e);break;case"session/clear":await this.handleSessionClear(e);break;case"session/prompt":await this.handleSessionPrompt(e);break;case"session/set_mode":await this.handleSessionSetMode(e);break;case"session/set_model":await this.handleSessionSetModel(e);break;case"skills/list":await this.handleSkillsList(e);break;case"models/list":await this.handleModelsList(e);break;case"session/cancel":this.handleSessionCancel(e.id,e.params);break;case"cron/list":case"cron/status":case"cron/add":case"cron/update":case"cron/remove":case"cron/run":case"cron/runs":await this.handleCronRequest(e);break;default:e.method.startsWith("terminal/")&&this.handleTerminalRequest?await this.handleTerminalRequest(e):this.transport.sendAcpError(e.id,Errors.MethodNotFound,{message:`Method not found: ${e.method}`})}}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:s instanceof Error?s.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSessionDeleted=null;onSessionCleared=null;handleNotification(e){switch(e.method){case"session/cancel":this.handleSessionCancel(void 0,e.params);break;case"canvas/toggle":case"canvas/clear":this.handleCanvasNotification?this.handleCanvasNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Canvas notification ignored (canvas disabled): ${e.method}`);break;case"canvas/reference":{const s=e.params,t=s?.surfaceId,n=this.state.resolveSessionFromParams(s);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${s?.sessionId}`);break}n.referencedSurfaceId=t||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${t?"set":"cleared"}: ${t??"(none)"}`);break}default:e.method.startsWith("terminal/")&&this.handleTerminalNotification?this.handleTerminalNotification(e.method,e.params):this.logger.debug?.(`[bridge-core] Unknown notification: ${e.method}`)}}handleInitialize(e){this.initialized=!0;const s=this.transport.forwardControl;!1===e.params?.forwardThinking&&(s.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(s.forwardToolCalls=!1),this.transport.sendResult(e.id,{protocolVersion:ACP_PROTOCOL_VERSION,agentCapabilities:{loadSession:!0,skills:!0,promptCapabilities:{image:!0,audio:!1,embeddedContext:!0},sessionCapabilities:{list:{},"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled,cron:!0},mcp:{}},agentInfo:{name:"beeos-claw",title:"BeeOS Agent Bridge",version:"0.1.0"},_meta:{},authMethods:[]})}async handleSessionNew(e){const s=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,t="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:s,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(s)}fetchHistoryInBackground(e){const s=this.getGateway();s?.isConnected&&loadHistory(s,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.sessionsList();if(!t||"object"!=typeof t)return;const n=t.sessions;if(!Array.isArray(n))return;for(const s of n){const t=s,n=t.sessionKey??t.key??t.id??t.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(e),e}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let s=this.state.sessions.get(e);if(!s&&e.startsWith("agent:")&&(s=this.state.findSessionByKey(e)),s||e.startsWith("agent:")||(s=await this.recoverSessionFromGateway(e)),s)return{session:s,created:!1};const t=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return s=createSessionObject({gatewayKey:t?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(s),{session:s,created:!0}}async handleSessionLoad(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const{session:t}=await this.resolveOrCreateSession(s),n=t.sessionId;this.state.lastActiveSessionId=n,t.chatReplayDedupKeys.clear();try{const e=this.getGateway(),s=await loadHistory(e,t.gatewaySessionKey,50),i=groupIntoTurns(s),o=i.filter(e=>"user"===e.role).length,a=i.filter(e=>"assistant"===e.role).length,r=i.filter(e=>e.toolCalls?.length).length,d=i.reduce((e,s)=>e+(s.toolCalls?.length??0),0);this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${s.length} turns=${i.length} userTurns=${o} asstTurns=${a} turnsWithToolCalls=${r} totalToolCalls=${d}`);for(let e=0;e<i.length;e++){const s=i[e];if("user"===s.role&&"internal_system"===s.provenance?.kind){this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`);continue}if("system"===s.role){this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${s.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:s.content},metadata:s.metadata});continue}this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${s.role} contentLen=${s.content.length} content=${JSON.stringify(s.content.slice(0,80))} tcCount=${s.toolCalls?.length??0}`);const t={sessionUpdate:"history_turn",role:s.role,content:s.content,reasoning:s.reasoning,toolCalls:s.toolCalls};s.metadata&&(t.metadata=s.metadata),this.transport.sendSessionUpdate(n,t)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,t.gatewaySessionKey,t.platformAgentId);void 0!==t.pendingPromptId&&!t.promptDone&&t.assistantMq&&!t.assistantMq.closed&&this.replayCurrentAssistantStream(t).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})}async replayCurrentAssistantStream(e){if(!e.assistantMq||e.assistantMq.closed)return;const s=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,t=Date.now()+DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS;let n=0;for(;Date.now()<t&&(e.currentRunId||e.pendingPromptId)&&!e.assistantMq.closed;){const t=await Promise.race([e.assistantMq.read(n),new Promise(e=>setTimeout(()=>e(null),s))]);if(!t||0===t.length){if(void 0!==e.pendingPromptId&&!e.promptDone)continue;break}for(const s of t)this.transport.sendSessionUpdate(e.sessionId,s),n++}n>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:n}})}async handleSessionList(e){const s=[];for(const[,e]of this.state.sessions)s.push({sessionId:e.sessionId,title:e.title||"New chat",cwd:e.cwd,updatedAt:e.lastStreamActivityTs??e.createdAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(e.gatewaySessionKey)});const t=this.getGateway();if(t?.isConnected)try{const e=await t.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const t=e.sessions;for(const e of t){const t=e,n=t.sessionKey??t.key??t.id??t.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=s.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),a=t.label||cleanGatewayDerivedTitle(t.derivedTitle)||t.title||void 0,r=t.model,d=t.modelProvider;if(o){if(!o.title||"New chat"===o.title){const e=a||o.sessionId;o.title=e;const s=this.state.sessions.get(o.sessionId);s&&!s.title&&(s.title=e)}const e=t.updatedAt;e&&(o.updatedAt=e),r&&(o.model=r),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:t.cwd,title:a||"New chat"});this.registerSession(e),s.push({sessionId:e.sessionId,title:a||"New chat",cwd:t.cwd,updatedAt:t.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:r,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}s.sort((e,s)=>(s.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(s.sessionId)),this.transport.sendResult(e.id,{sessions:s})}handleSessionRename(e){const s=e.params?.sessionId,t=e.params?.title;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=t.slice(0,64),i=this.state.sessions.get(s);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n;const o=i.gatewaySessionKey,a=this.getGateway();a?.isConnected&&a.sessionsPatch(o,{label:n}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:s,title:n})}async handleSessionDelete(e){const s=e.params?.sessionId;if(!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.info?.(`[bridge-core] session/delete sessionId=${s} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=t.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${s} gatewayKey=${n} gwConnected=${!!this.getGateway()?.isConnected}`);let i=!1;const o=this.getGateway();if(o?.isConnected&&n)try{await o.sessionsDelete(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${n}`)}catch(s){const t=s instanceof Error?s.message:String(s);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${t}`),!t.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(n),i=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${n}`)}catch(s){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${s instanceof Error?s.message:s}`),void this.transport.sendResult(e.id,{deleted:!1})}}else n&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${n}`);void 0!==t.pendingPromptId&&this.failPrompt(t,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(t),this.state.clearStreamIdleTimer(t),t.assistantMq?.close(),t.currentRunId&&this.state.promptsByRunId.delete(t.currentRunId);const a=new Set;for(const[e,t]of this.state.toolCallToSessionMap)t===s&&(this.state.toolCallToSessionMap.delete(e),a.add(e));for(const[e,s]of this.state.toolCallIdMap)a.has(s)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(s),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(s),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${s} gatewayKey=${n} deleted=true`),this.transport.sendResult(e.id,{deleted:!0})}async handleSessionClear(e){this.logger.info?.(`[bridge-core] handleSessionClear called, params=${JSON.stringify(e.params)}`);const s=e.params?.sessionId;if(!s)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const t=this.state.sessions.get(s);if(!t)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${s} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${s} gatewayKey=${t.gatewaySessionKey}`);const n=this.getGateway();if(n?.isConnected&&t.gatewaySessionKey)try{await n.sessionsReset(t.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${t.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${t.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n?.isConnected}, key=${t.gatewaySessionKey}`);t.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(s,t.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const s=e.params?.sessionId,t=e.params?.mode,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();if(n&&i?.isConnected&&t){const e={};if("verbose"===t||"detailed"===t?e.verboseLevel="full":"concise"!==t&&"default"!==t||(e.verboseLevel="default"),"reasoning"!==t&&"thinking"!==t||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await i.sessionsPatch(n.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:t??"default"})}async handleSessionSetModel(e){const s=e.params?.sessionId,t=e.params?.model,n=s?this.state.sessions.get(s):void 0,i=this.getGateway();let o=t??null,a=null;if(n&&i?.isConnected&&t)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:t}),s=e?.resolved;s?.model&&(o=s.model),s?.modelProvider&&(a=s.modelProvider)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch for model failed: ${e instanceof Error?e.message:e}`)}this.transport.sendResult(e.id,{model:o,modelProvider:a})}async handleModelsList(e){const s=this.getGateway();if(s?.isConnected)try{const t=await s.request("models.list",{});this.transport.sendResult(e.id,t??{models:[]})}catch(s){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${s instanceof Error?s.message:s}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const s=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:s})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const s=await e.skillsStatus();return s?.skills?s.skills.filter(e=>!1!==e.eligible&&!0!==e.disabled).sort((e,s)=>e.always&&!s.always?-1:!e.always&&s.always?1:e.name.localeCompare(s.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0})):[]}catch(e){return this.logger.warn?.(`[bridge-core] Failed to fetch skills: ${e instanceof Error?e.message:e}`),[]}}async pushSkillsList(){const e=await this.fetchSimplifiedSkills();this.transport.sendNotification("session/update",{sessionId:"_system",update:{sessionUpdate:"available_commands_update",commands:e}}),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"skills.pushed",severity:"debug",payload:{count:e.length}})}async handleSessionPrompt(e){const s=e.params?.sessionId,t=e.params?.prompt;if(!s||!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(s);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const a=this.getGateway();if(!a?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);let r=extractPromptBlocks(sanitizePromptPayload(t));if(!n.title){const e=r.find(e=>"text"===e.type&&e.text);e&&"text"in e&&e.text&&(n.title=e.text.slice(0,64).replace(/\n/g," ").trim(),a?.isConnected&&n.title&&a.sessionsPatch(n.gatewaySessionKey,{label:n.title}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}))}const d=buildResolutionPlan(r);if(d.refs.length>0)try{const e=await resolveResources(d,this.writeObs);r=applyResolutions(r,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}const l=toGatewayPrompt(r);if("error"in l)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:l.error});const{message:c,attachments:h}=l,m=resolveStandaloneSlashCommandName(r,c)??resolveSlashCmd(r,c),g="string"==typeof m;if(m&&this.handleSlashCommand){const s=this;if(await(async()=>{try{return await s.handleSlashCommand(m)}catch(e){return s.logger.warn?.(`[bridge-core] /${m} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${m}...`}}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const p=g?"chat.send":"agent";let u=g?c:withUserMessagePrefix(c);const f=this.config.canvasEnabled&&!g&&!1!==n.canvasActive&&!n.channelType;if(f){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(u=`${CANVAS_TERM_MAPPING}\n\n${u}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(f){const e=this.getCanvasState?.();if(e){const s=e.resolveCanvasForSession(n.gatewaySessionKey);u=`${buildActiveSurfacesContext(e,s)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${u}`}const s=n.referencedSurfaceId;s&&(u=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${s}" for editing. You MUST call render_ui with id="${s}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${u}`,n.referencedSurfaceId=void 0)}if(!g){const s=parseMentionsParam(e.params?.mentions);if(s.length>0){n.activeMentions=s;u=`${buildMentionInstruction(s)}\n${u}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${s.length} agent(s)`)}else n.activeMentions=void 0}g&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${m}`),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=p,this.emitPromptAsUserSessionUpdates(o,r),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""}}),this.schedulePromptTimeout(n);try{const e=h.length>0?h.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0;let s;s=g?await a.chatSend(n.gatewaySessionKey,u,e):await a.agentSend(n.gatewaySessionKey,u,n.agentId||this.config.gateway.agentId||this.getDefaultAgentId(),e),n.currentRunId=s.runId,n.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(s.runId,n)}catch(s){this.state.clearPromptTimeout(n),void 0!==n.pendingPromptId&&(n.pendingPromptId=void 0,n.assistantMq?.close(),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:s instanceof Error?s.message:"failed to send prompt to gateway"})),n.pendingAgentSends?.length&&(this.logger.warn?.("[bridge-core] Discarding %d deferred agentSends after prompt send failure",n.pendingAgentSends.length),n.pendingAgentSends=void 0)}}emitPromptAsUserSessionUpdates(e,s){for(const t of s){const s=this.toUserSessionUpdateContent(t);s&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.name;if(!e.data&&!e.uri)return;return{type:"image",...e.data?{data:e.data}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource_link"===e.type){if(!e.uri)return;return{type:"resource_link",uri:e.uri,...e.title?{title:e.title}:{},...e.name?{name:e.name}:{},...e.mimeType??e.mime_type?{mimeType:e.mimeType??e.mime_type}:{}}}if("file"===e.type){const s=e.mimeType??e.mime_type??"application/octet-stream",t=e.fileName??e.file_name??e.filename??e.name;if(!e.data&&!e.text&&!e.uri)return;return{type:"file",...e.data?{data:e.data}:{},...e.text?{text:e.text}:{},...e.uri?{uri:e.uri}:{},...s?{mimeType:s}:{},...t?{fileName:t}:{}}}if("resource"===e.type){const s=e.resource??{},t=s.uri??e.uri,n=s.mimeType??s.mime_type??e.mimeType??e.mime_type,i=s.text??e.text,o=s.data??e.data,a=s.fileName??s.file_name??s.filename??s.name??e.fileName??e.file_name??e.filename??e.name;if(!t&&!i&&!o)return;return{type:"resource",resource:{...t?{uri:t}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...a?{fileName:a}:{}}}}}handleSessionCancel(e,s){const t=s?.sessionId;if(!t)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(t);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway();i?.isConnected&&n.currentRunId&&i.chatAbort(n.gatewaySessionKey,n.currentRunId).catch(e=>{this.logger.debug?.(`[bridge-core] chatAbort failed: ${e instanceof Error?e.message:e}`)}),this.completePrompt(n,"cancelled"),void 0!==e&&this.transport.sendResult(e,{})}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const s=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;s<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${s}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${s}ms`))},s),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const s=e.currentRunId;if(s){this.state.promptsByRunId.delete(s);const e=`tc_${s}_`;for(const[s,t]of this.state.toolCallIdMap)t.startsWith(e)&&this.state.toolCallIdMap.delete(s)}e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,s){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const t=e.currentRunId;if(t&&this.onRunComplete){const s=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(t,s)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:s}),this.cleanupPrompt(e)}failPrompt(e,s,t){if(e.promptDone)return;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return;e.promptDone=!0;const n=e.currentRunId;n&&this.onRunFailed&&this.onRunFailed(n,t),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,s,t),this.cleanupPrompt(e)}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}maybeCompletePromptFromChatState(e,s){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const t="string"==typeof s.state?s.state.toLowerCase():void 0;if(t)if("final"===t)this.completePrompt(e,"end_turn");else if("aborted"===t||"cancelled"===t||"cancel"===t)this.completePrompt(e,"cancelled");else if("error"===t){const t=("string"==typeof s.summary?s.summary:void 0)??("string"==typeof s.message?s.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(t))}}pushToolResultContent(e,s,t,n){if(0===t.length)return;const i=(n?this.state.promptsByRunId.get(n):void 0)??(this.state.lastActiveSessionId?this.state.sessions.get(this.state.lastActiveSessionId):void 0);if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session (runId=${n})`);const o=i.sessionId;for(const e of t){const s={sessionUpdate:"agent_message_chunk",content:e};this.transport.sendSessionUpdate(o,s),i.assistantMq?.push(s)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:s,blockCount:t.length,sessionId:o}})}async handleCronRequest(e){const s=this.getGateway();if(!s?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const t=e.method.replace("/",".");try{const n=await s.request(t,e.params??{});this.transport.sendResult(e.id,n??{})}catch(s){const n=s instanceof Error?s.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${t} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;a2aOrch;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=5;drainNextDeferredSend(e){this.a2aOrch.drainNextDeferredSend(e,(e,t,s,n)=>this.agentSend(e,t,s,n))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const i=this.config.gateway.agentId||this.core.getDefaultAgentId();let a,r;if(s){if(a=this.state.findSessionByKey(s),!a)return void this.logger.warn?.("[acp-server] A2A result target session expired, discarding targetSessionKey=%s (known keys: %s)",s,[...this.state.sessionsByGatewayKey.keys()].join(", "));r=s,this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${a.sessionId}`)}else{const e=this.state.lastActiveSessionId;a=e?this.state.sessions.get(e):void 0,r=a?.gatewaySessionKey??`agent:${i}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using fallback key=${r}`)}if(a&&!a.promptDone&&(void 0!==a.pendingPromptId||void 0!==a.currentRunId))return(a.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",a.pendingAgentSends.length);const o=await this.canvasOrch.enrichMessageWithCanvasContext(e,r,this.thirdPartyChannels),c=await this.gateway.agentSend(r,o,i,t,n);return a&&c?.runId&&(this.state.resetForNewRun(a,a.sessionId,this.transport),a.currentRunId=c.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,a)),c}trackA2ARun(e,t,s){this.a2aOrch.trackA2ARun(e,t,s)}cancelA2ARunByTaskId(e){return this.a2aOrch.cancelA2ARunByTaskId(e)}emitA2ASystemNotification(e){this.a2aOrch.emitA2ASystemNotification(e)}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.a2aOrch=new A2AOrchestrator({state:this.state,transport:this.transport,logger:this.logger}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),this.canvasOrch.onPromptComplete(e),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{this.a2aOrch.onRunComplete(e,t)},this.core.onRunFailed=(e,t)=>{this.a2aOrch.onRunFailed(e,t)},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(){const e=this.config.bridge.url,t=this.config.keyFile;if(e&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch{this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),e&&t&&BridgeClientCtor?await this.startBridgeMode(e,t):(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone())}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n}, switching`),this.config.bridge.url=n,writeDiscoveryCache({bridge:{url:n,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)};try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")}`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e)),this.canvasOrch.updateDeps({gateway:this.gateway?{isConnected:this.gateway.isConnected,request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.a2aOrch.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
1
+ import{WebSocketServer,WebSocket}from"ws";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{A2AOrchestrator}from"./a2a-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";export class ACPServer{agentFetchFn;instanceId;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;canvasOrch;a2aOrch;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=5;drainNextDeferredSend(e){this.a2aOrch.drainNextDeferredSend(e,(e,t,s,n)=>this.agentSend(e,t,s,n))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async agentSend(e,t,s,n){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const i=this.config.gateway.agentId||this.core.getDefaultAgentId();let a,r;if(s){if(a=this.state.findSessionByKey(s),!a)return void this.logger.warn?.("[acp-server] A2A result target session expired, discarding targetSessionKey=%s (known keys: %s)",s,[...this.state.sessionsByGatewayKey.keys()].join(", "));r=s,this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${a.sessionId}`)}else{const e=this.state.lastActiveSessionId;a=e?this.state.sessions.get(e):void 0,r=a?.gatewaySessionKey??`agent:${i}:a2a`,this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, using fallback key=${r}`)}if(a&&!a.promptDone&&(void 0!==a.pendingPromptId||void 0!==a.currentRunId))return(a.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",a.pendingAgentSends.length);const o=await this.canvasOrch.enrichMessageWithCanvasContext(e,r,this.thirdPartyChannels),c=await this.gateway.agentSend(r,o,i,t,n);return a&&c?.runId&&(this.state.resetForNewRun(a,a.sessionId,this.transport),a.currentRunId=c.runId,a.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(c.runId,a)),c}trackA2ARun(e,t,s){this.a2aOrch.trackA2ARun(e,t,s)}cancelA2ARunByTaskId(e){return this.a2aOrch.cancelA2ARunByTaskId(e)}emitA2ASystemNotification(e){this.a2aOrch.emitA2ASystemNotification(e)}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,s,n,i,a){this.agentFetchFn=n,this.instanceId=i,this.config=e,this.logger=t,this.writeObs=s??(()=>{}),this.thirdPartyChannels=a??new Set,this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs),this.state=new AcpGatewaySessionState(this.logger),this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.a2aOrch=new A2AOrchestrator({state:this.state,transport:this.transport,logger:this.logger}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>{this.core.completePrompt(e,t),this.canvasOrch.onPromptComplete(e),this.drainNextDeferredSend(e)},failPrompt:(e,t,s)=>{this.core.failPrompt(e,t,s),this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn}),this.terminalMgr=new TerminalManager,this.terminalHandler=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs}),this.terminalHandler.setupListeners(),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.onRunComplete=(e,t)=>{this.a2aOrch.onRunComplete(e,t)},this.core.onRunFailed=(e,t)=>{this.a2aOrch.onRunFailed(e,t)},this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,s)=>{this.canvasOrch.onSessionLoaded(e,t,s)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>this.core.cleanupPrompt(e),e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}async start(e){e&&(this.config.bridge.url=e);const t=this.config.bridge.url,s=this.config.keyFile;if(t&&!BridgeClientCtor)try{const e=await import("@beeos-ai/bridge-client");BridgeClientCtor=e.BridgeClient}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}await this.connectGateway(),t&&s&&BridgeClientCtor?await this.startBridgeMode(t,s):(this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone())}async startBridgeMode(e,t){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");const s=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});this.bridgeInstance=s,s.on("connected",()=>{this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync()}),s.on("disconnected",(...e)=>{const n=String(e[0]??"unknown");if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${n}`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async n=>{n&&n!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n}, switching`),this.config.bridge.url=n,writeDiscoveryCache({bridge:{url:n,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n,t))}).catch(()=>{})}}),s.on("message",(...e)=>{const t=e[0],s="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(s);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),s.on("error",(...e)=>{const t=e[0];this.logger.error?.("[acp-server] Bridge error:",t?.message)});const n={readyState:WebSocket.OPEN,send:(e,t)=>{try{s.send(e),t?.()}catch(e){t?.(e)}},close:()=>s.close(),terminate:()=>s.close(!1),on:()=>n,once:()=>n,removeListener:()=>n,removeAllListeners:()=>n,ping:()=>{}};Object.defineProperty(n,"readyState",{get:()=>"connected"===s.state?WebSocket.OPEN:WebSocket.CLOSED}),this.client=n;try{await s.connect()}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}handleUpgrade(e,t,s){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,s,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();const t=()=>{if(e+1>=6)return this.logger.warn?.("[acp-server] agent sync exhausted 6 attempts — giving up"),void this.core.markAgentSyncDone();const t=Math.min(5e3*2**e,12e4);this.logger.info?.(`[acp-server] agent sync retry #${e+1} in ${Math.round(t/1e3)}s`),this.agentSyncRetryTimer=setTimeout(()=>this.tryAgentSync(e+1),t)};try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")}`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){const e=this.config.gateway;this.gateway=new GatewayClient({url:e.url,token:e.token||"",clientId:e.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}}),this.gateway.on("chat",e=>this.events.handleChatEvent(e)),this.gateway.on("agent",e=>this.events.handleAgentEvent(e)),this.gateway.on("cron",e=>this.events.handleCronEvent(e));const t=this.gateway;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,s)=>this.gateway.agentSend(e,t,s)}:null}),this.config.openclawCanvasCompat&&(this.gateway.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),this.gateway.on("connected",()=>{this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"})}),this.gateway.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),this.gateway.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),this.gateway.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),this.gateway.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),this.gateway.on("transport",e=>{this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),this.gateway.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),this.gateway.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{await this.gateway.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e,this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");try{const e=JSON.parse(t);"id"in e&&void 0!==e.id?this.core.handleRequest(e):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}pushToolResultContent(e,t,s,n){this.core.pushToolResultContent(e,t,s,n)}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const s=this.state.findSessionByKey(t);if(s?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,s.sessionId),s.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const s=this.state.toolCallIdMap.get(e);if(s&&s!==e){const e=this.state.toolCallToSessionMap.get(s);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const s=this.state.findSessionByKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(s?.agentId??"")??s?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const s=this.state.sessions.get(t);return s?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.destroy(),this.a2aOrch.destroy(),this.canvasOrch.destroy(),this.gateway&&(this.gateway.removeAllListeners(),this.gateway.stop(),this.gateway=null),this.client&&(this.client.removeAllListeners(),this.client.close(),this.client=null),this.bridgeInstance=null,this.bridgeConsecutiveFailures=0,this.terminalHandler.destroy(),this.terminalMgr.destroy(),this.wss&&(this.wss.removeAllListeners(),this.wss.close(),this.wss=null),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"})}}
@@ -1 +1 @@
1
- export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t,n){const e=t.surfaceId??"",i=Date.now();if(i-(this.lastActionTime.get(e)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${e}`);this.lastActionTime.set(e,i);const s=this.deps.stateManager.getActionHandler(e,n);if(s)try{this.deps.emitActionAsUserMessage?.(t),await s(t),this.emitObs(t,"bound_handler")}catch(n){throw this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${e} threw: ${n instanceof Error?n.message:n}`),n}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){if(this.pendingActions.length>=CanvasActionRouter.MAX_PENDING){const t=this.pendingActions.shift();this.deps.logger.warn?.(`[action-router] Queue full (${CanvasActionRouter.MAX_PENDING}), dropping oldest action (surface=${t?.surfaceId??"?"})`)}this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
1
+ export class CanvasActionRouter{deps;lastActionTime=new Map;pendingActions=[];static DEBOUNCE_MS=300;static MAX_PENDING=5;constructor(t){this.deps=t}async route(t,n){const e=t.surfaceId??"",i=Date.now();if(i-(this.lastActionTime.get(e)??0)<CanvasActionRouter.DEBOUNCE_MS)return void this.deps.logger.debug?.(`[action-router] Debounced action on ${e}`);this.lastActionTime.set(e,i);const s=this.deps.stateManager.getActionHandler(e,n);if(this.deps.logger.info?.(`[action-router] Routing: surface=${e} handler=${s?"bound":"agent"} canvasId=${n??"none"}`),s)try{this.deps.emitActionAsUserMessage?.(t),await s(t),this.emitObs(t,"bound_handler")}catch(n){throw this.emitObs(t,"bound_handler_error"),this.deps.logger.warn?.(`[action-router] Bound handler for ${e} threw: ${n instanceof Error?n.message:n}`),n}else this.emitObs(t,"agent_forward"),await this.deps.forwardToAgent(t)}enqueue(t){if(this.pendingActions.length>=CanvasActionRouter.MAX_PENDING){const t=this.pendingActions.shift();this.deps.logger.warn?.(`[action-router] Queue full (${CanvasActionRouter.MAX_PENDING}), dropping oldest action (surface=${t?.surfaceId??"?"})`)}this.pendingActions.push(t)}async drainPending(){if(0===this.pendingActions.length)return;const t=this.deduplicatePending();this.pendingActions=[];for(const n of t)await this.deps.forwardToAgent(n)}clearPending(){this.pendingActions=[]}reset(){this.lastActionTime.clear(),this.pendingActions=[]}deduplicatePending(){if(this.pendingActions.length<=1)return[...this.pendingActions];const t=new Map;for(const n of this.pendingActions)t.set(n.surfaceId??"",n);return Array.from(t.values())}emitObs(t,n){this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.action_routed",severity:n.endsWith("_error")?"warn":"info",payload:{surfaceId:t.surfaceId,actionName:t.userAction?.name,resolution:n}})}}export function formatActionForAgent(t,n){const e=t.surfaceId??"unknown",i=t.userAction?.name??"unknown",s=t.userAction?.context;let o=`[Canvas Interaction] The user clicked "${i}" on surface "${e}"`;if(n?.title&&(o+=` ("${n.title}")`),o+=".",n?.types?.length&&(o+=` Surface contains: ${n.types.join(", ")}.`),s&&Object.keys(s).length>0){const t=JSON.stringify(s);o+=`\nAction context: ${t.length>2e3?t.slice(0,2e3)+"…(truncated)":t}`}return o+=`\nRespond to this interaction. If the surface needs updating, use render_ui with id="${e}".`,o}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,s={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),s=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const n=new WebSocket(t,{headers:s});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=n,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[]}),n.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(s)}),n.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,s)=>{this.pendingJoins.set(e,{resolve:t,reject:s}),this.sendJoinCommand(e);const n=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(n),o.resolve()},reject:e=>{clearTimeout(n),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){const t=randomUUID();return e.reqId=t,new Promise((s,n)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),n(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:s,reject:n,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const s=(new TextEncoder).encode(e),n=new Uint8Array(1+s.length+t.length);n[0]=s.length,n.set(s,1),n.set(t,1+s.length),this.ws.send(n)}handleTextMessage(e){try{const t=JSON.parse(e),s=t.type;if(s?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(s){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.actionHandler&&this.actionHandler(t);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${s}`)}}catch{this.opts.logger?.warn?.("[canvas-relay] Failed to parse control message")}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const s=(new TextDecoder).decode(e.slice(1,1+t)),n=e.slice(1+t),o=this.canvasHandlers.get(s);if(!o)return;const a=n[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(n):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(n)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):this.hasConnectedOnce=!0}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch{this.opts.logger?.warn?.("[canvas-relay] Failed to parse control message")}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -1 +1 @@
1
- import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,t]of Object.entries(e)){if("object"==typeof t&&null!==t){const a=t;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const t=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const t=this.deps.state.sessions.get(s),a=t?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},n=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:t,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=n,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:t,schemaRegistry:n,formBuilder:i,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s&&this.emitCanvasActionAsUserMessage(s.sessionId,e)}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action handler wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){const s=e??this.deps.state.lastActiveSessionId,t=s?this.deps.state.sessions.get(s):void 0;return!1!==t?.canvasActive}isCanvasActiveForGatewayKey(e){const s=this.deps.state.sessionsByGatewayKey.get(e);return!1!==s?.canvasActive}onSessionLoaded(e,s,t){if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const t=this.canvasState?.resolveCanvasForSession(s);t&&(this.canvasState.clear(t),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0!==e.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${e.length} pending canvas link(s)`);for(const{acpSessionId:s,gatewaySessionKey:t}of e)this.ensureCanvasForSession(s,t)}}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,t){if(!this.canvasState)return e;if(t.has(s.split(":").slice(0,2).join(":"))||t.has(s))return e;try{let t=this.canvasState.resolveCanvasForSession(s);if(!t){const e=this.canvasState.getRelayClient();if(e)try{t=await e.ensureCanvas(s),this.canvasState.registerSession(s,t)}catch{}}if(t){let s=this.canvasState.buildContextForCanvas(t);s||(await this.canvasState.lazyLoadCanvas(t),s=this.canvasState.buildContextForCanvas(t));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.("[canvas-orch] Canvas context injection failed: %s",e)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,t=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=t,t&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,t=a.agentId,n=this.canvasState?.getRelayClient();if(n){const i=this.deps.resolvePlatformAgentId(t??"")??a.platformAgentId,o=s&&s!==e?e:void 0;n.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${t}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:t}})}else if("canvas/clear"===e){const e=s,t=this.deps.state.resolveSessionFromParams(e),a=t?this.canvasState.resolveCanvasForSession(t.gatewaySessionKey):void 0,n=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${n} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,t=s.action;if("a2ui_push"===t||"surface_update"===t){const e=String(s.surfaceId??nextOclId("openclaw")),t=s.components;if(t&&Array.isArray(t)){const a=sanitizeA2UIComponents(t),n=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,n)?this.canvasState.update(e,a,n):this.canvasState.create(n??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===t||"url_open"===t){const e=String(s.url??"");if(e){const t=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),n=[{id:`${t}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,t,"inline",n,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${t}`)}ensureCanvasForSession(e,s){const t=this.canvasState?.getRelayClient();if(!t)return;const a=e!==s?e:void 0;t.ensureCanvas(s,{linkSessionId:a}).then(async t=>{this.canvasState?.registerSession(s,t),await(this.canvasState?.joinCanvas(t,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${t} acpSid=${e} gwKey=${s}`)}).catch(t=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} err=${t}`)})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(!this.deps.gateway?.isConnected)return;if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.debug?.("[canvas-orch] Agent busy, canvas action queued");this.emitCanvasActionAsUserMessage(s.sessionId,e),this.deps.state.resetForNewRun(s,s.sessionId,this.deps.transport);const t=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,t??void 0);try{const e=await this.deps.gateway.agentSend(s.gatewaySessionKey,a,this.deps.config.gateway.agentId||this.deps.getDefaultAgentId());s.currentRunId=e.runId,s.assistantStreamStartedAt=Date.now(),this.deps.state.promptsByRunId.set(e.runId,s)}catch(t){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${t instanceof Error?t.message:t}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const t=s.userAction?.name??"unknown",a=`Clicked "${t}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:t,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}})}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:t,userAction:a,requestId:n,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,n,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,n,i,"no_session");const c={surfaceId:t,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.sendRelayActionResult(s,n,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,n,i,"processing_failed")})}sendRelayActionResult(e,s,t,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:t,status:a})}sendRelayActionError(e,s,t,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:t,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
1
+ import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,t]of Object.entries(e)){if("object"==typeof t&&null!==t){const n=t;for(const[e,s]of Object.entries(n))"string"==typeof s&&STRING_PROPS.has(e)&&(n[e]={literalString:s});e[s]=n}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const t=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},n=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const t=this.deps.state.sessions.get(s),n=t?.gatewaySessionKey;return n?e.resolveCanvasForSession(n):void 0},a=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:t,resolveCanvasId:n,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=a,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:t,schemaRegistry:a,formBuilder:i,resolveCanvasId:n,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action handler wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}isCanvasActive(e){const s=e??this.deps.state.lastActiveSessionId,t=s?this.deps.state.sessions.get(s):void 0;return!1!==t?.canvasActive}isCanvasActiveForGatewayKey(e){const s=this.deps.state.sessionsByGatewayKey.get(e);return!1!==s?.canvasActive}onSessionLoaded(e,s,t){if(!this.deps.config.canvasEnabled)return;const n=this.canvasState?.getRelayClient();if(!n)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const t=this.canvasState?.resolveCanvasForSession(s);t&&(this.canvasState.clear(t),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,t=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==t.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${t.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of t)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,t){if(!this.canvasState)return e;if(t.has(s.split(":").slice(0,2).join(":"))||t.has(s))return e;try{let t=this.canvasState.resolveCanvasForSession(s);if(!t){const e=this.canvasState.getRelayClient();if(e)try{t=await e.ensureCanvas(s),this.canvasState.registerSession(s,t)}catch{}}if(t){let s=this.canvasState.buildContextForCanvas(t);s||(await this.canvasState.lazyLoadCanvas(t),s=this.canvasState.buildContextForCanvas(t));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.("[canvas-orch] Canvas context injection failed: %s",e)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,t=!1!==e.enabled,n=this.deps.state.resolveSessionFromParams(e);if(n&&(n.canvasActive=t,t&&(n.canvasPromptInjected=!1,n.sessionId))){const e=n.sessionId,s=n.gatewaySessionKey,t=n.agentId,a=this.canvasState?.getRelayClient();if(a){const i=this.deps.resolvePlatformAgentId(t??"")??n.platformAgentId,o=s&&s!==e?e:void 0;a.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${t}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:t}})}else if("canvas/clear"===e){const e=s,t=this.deps.state.resolveSessionFromParams(e),n=t?this.canvasState.resolveCanvasForSession(t.gatewaySessionKey):void 0,a=n?this.canvasState.getAllActive(n).length:0;n&&this.canvasState.clear(n),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${a} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,t=s.action;if("a2ui_push"===t||"surface_update"===t){const e=String(s.surfaceId??nextOclId("openclaw")),t=s.components;if(t&&Array.isArray(t)){const n=sanitizeA2UIComponents(t),a=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,a)?this.canvasState.update(e,n,a):this.canvasState.create(a??nextOclId("_openclaw"),e,"inline",n,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===t||"url_open"===t){const e=String(s.url??"");if(e){const t=nextOclId("openclaw-nav"),n=String(s.canvasId??nextOclId("_openclaw_nav")),a=[{id:`${t}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(n,t,"inline",a,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${t}`)}ensureCanvasForSession(e,s){const t=this.canvasState?.getRelayClient();if(!t)return;const n=e!==s?e:void 0;t.ensureCanvas(s,{linkSessionId:n}).then(async t=>{this.canvasState?.registerSession(s,t),await(this.canvasState?.joinCanvas(t,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${t} acpSid=${e} gwKey=${s}`)}).catch(t=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} err=${t}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);this.deps.state.resetForNewRun(s,s.sessionId,this.deps.transport);const t=this.canvasState?.summarize(e.surfaceId??""),n=formatActionForAgent(e,t??void 0);try{const e=await this.deps.gateway.agentSend(s.gatewaySessionKey,n,this.deps.config.gateway.agentId||this.deps.getDefaultAgentId());s.currentRunId=e.runId,s.assistantStreamStartedAt=Date.now(),this.deps.state.promptsByRunId.set(e.runId,s)}catch(t){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${t instanceof Error?t.message:t}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const t=s.userAction?.name??"unknown",n=`Clicked "${t}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:n},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:t,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${t} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:t,userAction:n,requestId:a,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,a,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,a,i,"no_session");const c={surfaceId:t,userAction:{name:n.name,sourceComponentId:n.sourceComponentId,timestamp:n.timestamp,context:n.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${t} request=${a}`),this.sendRelayActionResult(s,a,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,a,i,"processing_failed")})}sendRelayActionResult(e,s,t,n){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:t,status:n})}sendRelayActionError(e,s,t,n){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:t,error:n})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools}from"./a2a/tools.js";import{createA2ATaskHandler}from"./a2a/task-handler.js";import{createA2AResultHandler}from"./a2a/result-handler.js";import{createA2ATaskCancelHandler}from"./a2a/cancel-handler.js";import{TaskHandleRegistry}from"./a2a/task-handle.js";import{pollPendingTasks}from"./a2a/poll-pending.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;function initializePlugin(e){let r,a;try{r=e.runtime?.config?.loadConfig?.()}catch{}try{a=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:r})}catch(r){return e.logger.error?.("[beeos-claw] Failed to resolve config:",r),null}const o=a.config;pluginConfig=o;const s=buildThirdPartyChannelSet(r);s.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...s].join(", ")}`);const t=o.log.enabled?createObsEventWriter({dir:o.log.dir,redaction:{gatewayToken:o.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:o,sources:a.sources,openclawConfigPath:a.openclawConfigPath}});for(const r of a.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:r.severity,summary:r.message,error:{code:r.code,message:r.message,nextSteps:r.nextSteps}}),"error"===r.severity?e.logger.error?.(`[beeos-claw] ${r.message}`):e.logger.warn?.(`[beeos-claw] ${r.message}`);if(a.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),o.keyFile)try{agentKeys=loadAgentKeyPair(o.keyFile);const r=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${r})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${r}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return{cfg:o,writeObs:t,thirdPartyChannels:s,openclawConfig:r}}function registerUploadTools(e,r){const{cfg:a,writeObs:o}=r;cleanupUploadToolResultCache().catch(r=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(r)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} params=${JSON.stringify(s)}`);const t=await executeUpload(s,a,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${r} ok=${t.ok}`),o({component:"upload-tool",domain:"tool",name:t.ok?"upload.success":"upload.failure",severity:t.ok?"info":"warn",payload:{toolCallId:r,ok:t.ok,fileCount:t.files?.length??0,error:t.error}}),t.ok&&t.content&&acpServer&&acpServer.pushToolResultContent(r,BEEOS_UPLOAD_TOOL_NAME,t.content),(async a=>{try{await writeUploadToolResult(r,BEEOS_UPLOAD_TOOL_NAME,a),await cleanupUploadToolResultCache()}catch(a){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${r}: ${String(a)}`)}return a})(t)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,a){await handleFileStage(e,a,r.cfg)}})}function registerServices(e,r){const{cfg:a,writeObs:o,thirdPartyChannels:s}=r,t=agentKeys?(e,r)=>agentFetch(e,agentKeys,r):void 0,n=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;acpServer=new ACPServer(a,e.logger,o,t,n,s),e.registerService({id:"beeos-acp",async start(){if(!a.bridge.url&&a.agentGatewayUrl&&t){const r=await discoverBridgeFromGW(a.agentGatewayUrl,t);if(r)a.bridge.url=r,writeDiscoveryCache({bridge:{url:r,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r}`);else{const r=readDiscoveryCache();r?.bridge?.url?(a.bridge.url=r.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${r.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await acpServer.start(),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const i=new TaskHandleRegistry;let l=null;const{enabled:c,url:g,principalId:d,a2aEnabled:p}=a.messageBus;let f;if(a.agentGatewayUrl&&agentKeys){const e=a.agentGatewayUrl,r=agentKeys;f=async a=>{const o=await agentFetch(`${e}/api/v1/messaging/token`,r,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:a})});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${o.status}: ${e}`)}return o.json()}}if(c&&(f||a.messageBus.serviceUrl)){l=new MessageSDK({serviceUrl:f?void 0:a.messageBus.serviceUrl,centrifugoUrl:g},e.logger);const r=d;e.registerService({id:"beeos-message-bus",async start(){let a=r;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&acpServer)try{await acpServer.waitForAgentMapping(),a=acpServer.getDefaultPlatformAgentId()||""}catch(r){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",r)}a?(await l.connect(a,{centrifugoUrl:g,tokenProvider:f}),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",a,f?"gateway":"direct")):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(l?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const o=(e,r,a,o)=>acpServer.agentSend(e,r,a,o),s=(e,r,a)=>acpServer.trackA2ARun(e,r,a),t=e=>acpServer.emitA2ASystemNotification(e),n=a.agentGatewayUrl?{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger}:void 0;l.onMessage("a2a_task",createA2ATaskHandler(o,n,s,t)),l.onMessage("a2a_result",createA2AResultHandler(o,i,t)),l.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(o,e=>acpServer.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await acpServer.waitForAgentMapping();const r=await pollPendingTasks(n,o,s);r>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",r)}})}else c?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",c);if(!1!==p&&a.agentGatewayUrl){const r=e=>acpServer?.getGatewaySessionKeyForToolCall(e),o=e=>acpServer?.getMentionsForToolCall(e),s=(e,r)=>{acpServer?.broadcastNotification(e,r)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},i,r,o,s),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){const r=new CanvasStateManager;if(a.agentGatewayUrl&&agentKeys){let o;r.setLogger(e.logger);{const e=a.agentGatewayUrl,r=agentKeys;o=async()=>{const a=await agentFetch(`${e}/api/v1/canvas/token`,r,{method:"POST"});if(!a.ok){const e=await a.text().catch(()=>"");throw new Error(`Canvas token returned ${a.status}: ${e}`)}return a.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await acpServer.waitForAgentMapping();const s=acpServer.getDefaultPlatformAgent();if(!s?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const t=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:s.id,displayName:s.displayName,avatarUrl:s.avatarUrl,tokenProvider:o,logger:e.logger});t.connect(),r.setRelayClient(t),r.setAgentId(s.id),acpServer.wireRelayActionHandler(),acpServer.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${s.id}, name=${s.name??"none"})`)}catch(r){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${r}`)}},stop(){r.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const t={stateManager:r,logger:e.logger,writeObs:e=>o(e),isCanvasActive:()=>acpServer.isCanvasActive(),getActiveSessionId:e=>acpServer.getSessionIdForToolCall(e),getPlatformAgentId:e=>acpServer.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>acpServer.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>acpServer.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const l=e=>async(r,a)=>(acpServer.preMapToolCallSession(r),e(r,a));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:l(createRenderUiHandler(t))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:l(createCloseUiHandler(t))}),acpServer.setCanvasState(r,{schemaRegistry:n,descriptorRegistry:i});const c=acpServer.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:l(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const g=acpServer.getResourceRenderer();if(g){generateActionTools(i,e,async(e,r)=>acpServer.gatewayRequest(e,r),g,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(i),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:l(createRenderResourceHandler(i,g,e.logger))})}const d=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]),p=256,f=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,r)=>{if(r?.sessionKey){if(f.size>=p&&!f.has(r.sessionKey)){const e=f.keys().next().value;void 0!==e&&f.delete(e)}f.set(r.sessionKey,r.channelId)}if(isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:r?.channelId}))return;return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,r)=>{if(r?.sessionKey&&acpServer.setPendingToolCallSessionKey(r.sessionKey),!d.has(e.toolName))return;const a=r?.sessionKey?f.get(r.sessionKey):void 0;if(isThirdPartyChannel(s,{sessionKey:r?.sessionKey,channelId:a}))return{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."};return(r?.sessionKey?acpServer.isCanvasActiveForGatewayKey(r.sessionKey):acpServer.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const u=new TerminalManager,y=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:u,logger:e.logger,writeObs:o});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){y.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){y.stop(),u.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),r.openclawConfig){const a=mirrorOpenClawConfig(r.openclawConfig);a.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${a.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${a.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){const r=initializePlugin(e);r&&(registerUploadTools(e,r),registerServices(e,r),e.logger.info?.("[beeos-claw] Plugin registered successfully"))}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools}from"./a2a/tools.js";import{createA2ATaskHandler}from"./a2a/task-handler.js";import{createA2AResultHandler}from"./a2a/result-handler.js";import{createA2ATaskCancelHandler}from"./a2a/cancel-handler.js";import{TaskHandleRegistry}from"./a2a/task-handle.js";import{pollPendingTasks}from"./a2a/poll-pending.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools,RENDER_RESOURCE_TOOL_NAME,RENDER_RESOURCE_TOOL_SCHEMA,buildRenderResourceDescription,createRenderResourceHandler}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let pluginConfig=null,acpServer=null,agentKeys=null;function initializePlugin(e){let a,o;try{a=e.runtime?.config?.loadConfig?.()}catch{}try{o=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:a})}catch(a){return e.logger.error?.("[beeos-claw] Failed to resolve config:",a),null}const r=o.config;pluginConfig=r;const s=buildThirdPartyChannelSet(a);s.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...s].join(", ")}`);const t=r.log.enabled?createObsEventWriter({dir:r.log.dir,redaction:{gatewayToken:r.gateway.token},logger:e.logger}):()=>{};t({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),t({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:r,sources:o.sources,openclawConfigPath:o.openclawConfigPath}});for(const a of o.validation)t({component:"plugin",domain:"config",name:"config.validation",severity:a.severity,summary:a.message,error:{code:a.code,message:a.message,nextSteps:a.nextSteps}}),"error"===a.severity?e.logger.error?.(`[beeos-claw] ${a.message}`):e.logger.warn?.(`[beeos-claw] ${a.message}`);if(o.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),r.keyFile)try{agentKeys=loadAgentKeyPair(r.keyFile);const a=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${a})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${a}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return{cfg:r,writeObs:t,thirdPartyChannels:s,openclawConfig:a}}function registerUploadTools(e,a){const{cfg:o,writeObs:r}=a;cleanupUploadToolResultCache().catch(a=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(a)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(a,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} params=${JSON.stringify(s)}`);const t=await executeUpload(s,o,agentKeys);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${a} ok=${t.ok}`),r({component:"upload-tool",domain:"tool",name:t.ok?"upload.success":"upload.failure",severity:t.ok?"info":"warn",payload:{toolCallId:a,ok:t.ok,fileCount:t.files?.length??0,error:t.error}}),t.ok&&t.content&&acpServer&&acpServer.pushToolResultContent(a,BEEOS_UPLOAD_TOOL_NAME,t.content),(async o=>{try{await writeUploadToolResult(a,BEEOS_UPLOAD_TOOL_NAME,o),await cleanupUploadToolResultCache()}catch(o){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${a}: ${String(o)}`)}return o})(t)}}),e.registerHttpRoute({path:"/beeos/files/stage",async handler(e,o){await handleFileStage(e,o,a.cfg)}})}function registerServices(e,a){const{cfg:o,writeObs:r,thirdPartyChannels:s}=a,t=agentKeys?(e,a)=>agentFetch(e,agentKeys,a):void 0,n=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0;acpServer=new ACPServer(o,e.logger,r,t,n,s);const i=acpServer;e.registerService({id:"beeos-acp",async start(){if(!o.bridge.url&&o.agentGatewayUrl&&t){const a=await discoverBridgeFromGW(o.agentGatewayUrl,t);if(a)o.bridge.url=a,writeDiscoveryCache({bridge:{url:a,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${a}`);else{const a=readDiscoveryCache();a?.bridge?.url?(o.bridge.url=a.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${a.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await i.start(o.bridge.url||void 0),e.logger.info?.("[beeos-claw] ACP Server started")},async stop(){await(acpServer?.stop()),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const l=new TaskHandleRegistry;let c=null;const{enabled:g,url:d,principalId:p,a2aEnabled:f}=o.messageBus;let u;if(o.agentGatewayUrl&&agentKeys){const e=o.agentGatewayUrl,a=agentKeys;u=async o=>{const r=await agentFetch(`${e}/api/v1/messaging/token`,a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:o})});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${r.status}: ${e}`)}return r.json()}}if(g&&(u||o.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:u?void 0:o.messageBus.serviceUrl,centrifugoUrl:d},e.logger);const a=p;e.registerService({id:"beeos-message-bus",async start(){let o=a;if(o||(o=process.env.BEEOS_PRINCIPAL_ID||""),!o&&acpServer)try{await acpServer.waitForAgentMapping(),o=acpServer.getDefaultPlatformAgentId()||""}catch(a){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",a)}o?(await c.connect(o,{centrifugoUrl:d,tokenProvider:u}),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,u?"gateway":"direct")):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const r=(e,a,o,r)=>i.agentSend(e,a,o,r),s=(e,a,o)=>i.trackA2ARun(e,a,o),t=e=>i.emitA2ASystemNotification(e),n=o.agentGatewayUrl?{agentGatewayUrl:o.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger}:void 0;c.onMessage("a2a_task",createA2ATaskHandler(r,n,s,t)),c.onMessage("a2a_result",createA2AResultHandler(r,l,t)),c.onMessage("a2a_task_cancel",createA2ATaskCancelHandler(r,e=>i.cancelA2ARunByTaskId(e))),n&&e.registerService({id:"beeos-a2a-poll",async start(){await i.waitForAgentMapping();const a=await pollPendingTasks(n,r,s);a>0&&e.logger.info?.("[beeos-claw] Dispatched %d pending A2A tasks on startup",a)}})}else g?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",g);if(!1!==f&&o.agentGatewayUrl){const a=e=>acpServer?.getGatewaySessionKeyForToolCall(e),r=e=>acpServer?.getMentionsForToolCall(e),s=(e,a)=>{acpServer?.broadcastNotification(e,a)};registerA2ATools(e,{agentGatewayUrl:o.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},l,a,r,s),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(o.canvasEnabled){const a=new CanvasStateManager;if(o.agentGatewayUrl&&agentKeys){let r;a.setLogger(e.logger);{const e=o.agentGatewayUrl,a=agentKeys;r=async()=>{const o=await agentFetch(`${e}/api/v1/canvas/token`,a,{method:"POST"});if(!o.ok){const e=await o.text().catch(()=>"");throw new Error(`Canvas token returned ${o.status}: ${e}`)}return o.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await i.waitForAgentMapping();const s=i.getDefaultPlatformAgent();if(!s?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const t=new CanvasRelayClient({canvasRelayUrl:o.canvasRelayUrl,agentKeys:agentKeys,agentId:s.id,displayName:s.displayName,avatarUrl:s.avatarUrl,tokenProvider:r,onReconnect:()=>i.onRelayReconnected(),logger:e.logger});t.connect(),a.setRelayClient(t),a.setAgentId(s.id),i.wireRelayActionHandler(),i.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${s.id}, name=${s.name??"none"})`)}catch(a){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${a}`)}},stop(){a.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const t={stateManager:a,logger:e.logger,writeObs:e=>r(e),isCanvasActive:()=>i.isCanvasActive(),getActiveSessionId:e=>i.getSessionIdForToolCall(e),getPlatformAgentId:e=>i.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>i.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>i.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const l=new ResourceDescriptorRegistry;l.register(CRON_RESOURCE);const c=e=>async(a,o)=>(i.preMapToolCallSession(a),e(a,o));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:c(createRenderUiHandler(t))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:c(createCloseUiHandler(t))}),i.setCanvasState(a,{schemaRegistry:n,descriptorRegistry:l});const g=i.getFormBuilder();g&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:c(createRenderFormHandler({registry:n,builder:g,logger:e.logger}))});const d=i.getResourceRenderer();if(d){generateActionTools(l,e,async(e,a)=>i.gatewayRequest(e,a),d,e.logger),e.registerTool({name:RENDER_RESOURCE_TOOL_NAME,description:buildRenderResourceDescription(l),parameters:RENDER_RESOURCE_TOOL_SCHEMA,execute:c(createRenderResourceHandler(l,d,e.logger))})}const p=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME,RENDER_RESOURCE_TOOL_NAME]),f=256,u=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,a)=>{if(a?.sessionKey){if(u.size>=f&&!u.has(a.sessionKey)){const e=u.keys().next().value;void 0!==e&&u.delete(e)}u.set(a.sessionKey,a.channelId)}if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey,channelId:a?.channelId}))return;return(a?.sessionKey?i.isCanvasActiveForGatewayKey(a.sessionKey):i.isCanvasActive())?{appendSystemContext:CANVAS_SYSTEM_PROMPT}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, render_form, or render_resource. Respond with plain text only."}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,a)=>{if(a?.sessionKey&&i.setPendingToolCallSessionKey(a.sessionKey),!p.has(e.toolName))return;if(isThirdPartyChannel(s,{sessionKey:a?.sessionKey}))return{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."};return(a?.sessionKey?i.isCanvasActiveForGatewayKey(a.sessionKey):i.isCanvasActive())?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form, render_resource + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const y=new TerminalManager,m=createTerminalWebSocketService({config:{port:o.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:o.terminalWsAllowedOrigins},terminalMgr:y,logger:e.logger,writeObs:r});if(o.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){m.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${o.terminalWsPort}`)},stop(){m.stop(),y.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),a.openclawConfig){const o=mirrorOpenClawConfig(a.openclawConfig);o.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${o.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${o.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){const a=initializePlugin(e);a&&(registerUploadTools(e,a),registerServices(e,a),e.logger.info?.("[beeos-claw] Plugin registered successfully"))}};export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",