beeos-claw 0.2.36 → 0.2.38
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.
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/history-replay.js +1 -1
- package/dist/acp-gateway/history-replay.test.js +1 -0
- package/dist/acp-gateway/transport.js +1 -1
- package/dist/acp-gateway/transport.test.js +1 -0
- package/dist/acp-server.js +1 -1
- package/dist/gateway-client.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,readFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload,stripTransportMetadata,isHeartbeatPromptText,isHeartbeatOnlyText}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,extractBeeosFileUrisFromOutput,splitUserReplayBlocks}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readEnrichmentCache,readUploadToolResult}from"../upload/result-cache.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.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}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-",":a2a:"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const i=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return i&&!i.includes("/")&&SLASH_COMMAND_NAME_RE.test(i)?i.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),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;beeosFileResolver;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_loadSessionLock=new Map;setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[]}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=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,this.beeosFileResolver=e.beeosFileResolver}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),this.ensureVerboseFull(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let i=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const n=t.label||t.derivedTitle||t.title||void 0,o=n&&!isPlaceholderTitle(n)?n:void 0,r=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:o});this.registerSession(r),i++}i>0&&this.logger.info?.(`[bridge-core] preloadSessions: loaded ${i} session(s) from gateway`)}catch(e){this.logger.debug?.("[bridge-core] preloadSessions failed:",e)}else this.logger.debug?.("[bridge-core] preloadSessions skipped: gateway not connected")}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"skills/install":await this.handleSkillsInstall(e);break;case"skills/uninstall":await this.handleSkillsUninstall(e);break;case"skills/update":await this.handleSkillsUpdate(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(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=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 t=e.params,s=t?.surfaceId,i=this.state.resolveSessionFromParams(t);if(!i){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}i.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(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 t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.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 t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",i=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(i),this.transport.sendResult(e.id,{sessionId:i.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:i.agentId,platformAgentId:i.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const i=s.sessions;if(!Array.isArray(i))return;for(const t of i){const s=t,i=s.sessionKey??s.key??s.id??s.name;if(i&&"string"==typeof i&&deriveSessionId(i,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,n=createSessionObject({gatewayKey:i,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(n),n}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),i=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${i}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this._loadSessionLock.get(t);if(s)try{await s}catch{}const i=this._doSessionLoad(e,t);this._loadSessionLock.set(t,i);try{await i}finally{this._loadSessionLock.get(t)===i&&this._loadSessionLock.delete(t)}}async _doSessionLoad(e,t){const{session:s}=await this.resolveOrCreateSession(t),i=s.sessionId;this.state.lastActiveSessionId=i,s.chatReplayDedupKeys.clear();let n=[];try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,100);n=groupIntoTurns(t);const o=n.filter(e=>"user"===e.role).length,r=n.filter(e=>"assistant"===e.role).length,a=n.filter(e=>e.toolCalls?.length).length,l=n.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${i} messages=${t.length} turns=${n.length} userTurns=${o} asstTurns=${r} turnsWithToolCalls=${a} totalToolCalls=${l}`),n.length>0&&isPlaceholderTitle(s.title)&&"manual"!==s.titleSource){const e=n.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(e?.content){const t=stripTransportMetadata(e.content).trim(),n=t.slice(0,64).replace(/\n/g," ").trim();if(n){s.title=n,s.titleSource="truncated";const e=this.getGateway();e?.isConnected&&e.sessionsPatch(s.gatewaySessionKey,{label:n}).catch(()=>{}),this.transport.sendSessionUpdate(i,{sessionUpdate:"title_update",title:n}),this.generateTitleAsync(s,i,t)}}}for(let e=0;e<n.length;e++){const t=n[e];"user"!==t.role||"internal_system"!==t.provenance?.kind?"user"===t.role&&/^\s*system\s*\(untrusted[^)]*\)/i.test(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] system-injected user message`):"user"===t.role&&isHeartbeatPromptText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat cron prompt`):"assistant"===t.role&&isHeartbeatOnlyText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat assistant reply`):"system"!==t.role?(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`),"user"===t.role?this.replayUserTurnAsChunks(i,t.content,t.metadata,t.timestamp):await this.replayAssistantTurnAsStream(i,t)):(this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(i,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata,...t.timestamp?{timestamp:t.timestamp}:{}})):this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}const o=new Set;for(const e of n)if(e.toolCalls)for(const t of e.toolCalls)t.id&&o.add(t.id),t.callId&&t.callId!==t.id&&o.add(t.callId);let r="",a="";for(let e=n.length-1;e>=0;e--)if("assistant"===n[e].role){r=n[e].content.slice(-200),a=(n[e].reasoning??"").slice(-200);break}if(void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed)try{this.replayCurrentAssistantStream(s,o,r,a)}catch(e){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}(()=>{this.transport.sendResult(e.id,{sessionId:i}),this.onSessionLoaded?.(i,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})})()}replayCurrentAssistantStream(e,t,s="",i=""){if(!e.assistantMq||e.assistantMq.closed)return;const n=e.assistantMq.peek(0);if(0===n.length)return;let o=0,r=0,a="",l="";for(const d of n)if(t?.size&&d.toolCallId&&t.has(d.toolCallId)&&("tool_call"===d.sessionUpdate||"tool_call_update"===d.sessionUpdate))r++;else{if(s.length>0&&"agent_message_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(a+=e,a.length<=s.length)){r++;continue}}if(i.length>0&&"agent_thought_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(l+=e,l.length<=i.length)){r++;continue}}this.transport.sendSessionUpdate(e.sessionId,d),o++}r>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${r} duplicate tool events (already in history)`),o>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:o}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||t.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 s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,i=s.sessionKey??s.key??s.id??s.name;if(!i||"string"!=typeof i)continue;if(isBackgroundSessionKey(i))continue;const n=deriveSessionId(i,this.instanceId),o=t.find(e=>e.sessionId===i||e.sessionId===n||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===i),r=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=r&&!isPlaceholderTitle(r)?r:void 0,l=s.model,d=s.modelProvider;if(o){if(a&&isPlaceholderTitle(o.title)){o.title=a;const e=this.state.sessions.get(o.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(o.updatedAt=e),l&&(o.model=l),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:i,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(i),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const i=s.slice(0,64),n=this.state.sessions.get(t);if(!n)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);n.title=i,n.titleSource="manual";const o=n.gatewaySessionKey,r=this.getGateway();r?.isConnected&&r.sessionsPatch(o,{label:i}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (rename) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendResult(e.id,{sessionId:t,title:i})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your only task is to generate a concise, human-readable title summarizing the user\'s first message.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric), not the action verb.\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If the message is too vague (e.g. "hi", "continue", "ok", "?"), return "新会话" (for zh) or "New Conversation" (for en).\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput: "帮我写一个关于BeeOS设备接入的PRD"\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput: "What\'s the best way to integrate x402 payment protocol?"\nOutput: {"title": "x402 Payment Integration"}\n\nInput: "hi"\nOutput: {"title": "新会话"}\n\nInput: "分析一下OKX CeDeFi PM这个岗位的机会"\nOutput: {"title": "OKX CeDeFi 岗位分析"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}generateTitleAsync(e,t,s){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen-async SKIP guard message sid=${t}`);const i=this.matchTitleTemplate(s);if(i){e.title=i,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:i}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:i})}const n=s.length>2e3?s.slice(0,1500):s,o=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),r=process.env.AROUTER_API_KEY||"";o&&r?(this.logger.info?.(`[bridge-core] title-gen-async CALLING LLM sid=${t} msg="${s.slice(0,60)}"`),fetch(`${o}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:n}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen-async FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const i=await s.json(),n=i.choices?.[0]?.message?.content?.trim();if(!n)return;const o=n.replace(/^```json\s*/,"").replace(/```$/,"").trim();let r;try{r=JSON.parse(o).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title gen JSON parse failed: ${o}`)}if(!r)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen-async OK sid=${t} title="${r}"`),e.title=r,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:r})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen-async ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})):this.logger.info?.(`[bridge-core] title-gen-async SKIP sid=${t}: no AROUTER env vars`)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const i=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} gatewayKey=${i} gwConnected=${!!this.getGateway()?.isConnected}`);let n=!1;const o=this.getGateway();if(o?.isConnected&&i)try{await o.sessionsDelete(i),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.delete SUCCESS for key=${i}`)}catch(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${i}: ${s}`),!s.includes("Cannot delete the main session"))return void this.transport.sendResult(e.id,{deleted:!1});try{await o.sessionsReset(i),n=!0,this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${i}`)}catch(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${i}: ${t instanceof Error?t.message:t}`),void this.transport.sendResult(e.id,{deleted:!1})}}else i&&this.logger.warn?.(`[bridge-core] Gateway not connected, proceeding with local-only delete for key=${i}`);void 0!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const r=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),r.add(e));for(const[e,t]of this.state.toolCallIdMap)r.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(i),this.transport.resetSessionUpdateCount(t),n&&this.onSessionDeleted?.(i),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} gatewayKey=${i} 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 t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const i=this.getGateway(),n=i?.isConnected;if(s.promptDone||void 0===s.pendingPromptId&&void 0===s.currentRunId||(this.logger.info?.(`[bridge-core] session/clear: cancelling active run sid=${t} runId=${s.currentRunId}`),n&&(s.currentRunId||s.gatewayRequestId)&&i.agentCancel(s.gatewaySessionKey,{runId:s.currentRunId,requestId:s.gatewayRequestId}),this.endRun(s,{kind:"complete",stopReason:"cancelled"})),n&&s.gatewaySessionKey)try{await i.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${n}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,i=t?this.state.sessions.get(t):void 0,n=this.getGateway();if(i&&n?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(e.reasoningLevel="stream"),Object.keys(e).length>0)try{await n.sessionsPatch(i.gatewaySessionKey,e)}catch(e){this.logger.warn?.(`[bridge-core] sessions.patch failed: ${e instanceof Error?e.message:e}`)}}this.transport.sendResult(e.id,{mode:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,i=t?this.state.sessions.get(t):void 0,n=this.getGateway();let o=s??null,r=null;if(i&&n?.isConnected&&s)try{const e=await n.sessionsPatch(i.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(o=t.model),t?.modelProvider&&(r=t.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:r})}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();if(!t?.skills)return[];const s=t.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||void 0,iconUrl:void 0})),i=this.getSkillsDir();for(const e of s){const t=e.skillKey??e.name;try{const s=await readFile(join(i,t,".beeos-meta.json"),"utf8"),n=JSON.parse(s);n?.iconUrl&&(e.iconUrl=n.iconUrl)}catch{}}return s}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}})}getSkillsDir(){const e=process.env.OPENCLAW_HOME||join(homedir(),".openclaw");return join(e,"skills")}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,i=t?.files;let n;try{n=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!i?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(this.getSkillsDir(),n);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(i?.length){const e=resolve(o);for(const t of i){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const i=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!i.ok)throw new Error(`Failed to download ${t.path}: ${i.status}`);const n=Buffer.from(await i.arrayBuffer());await writeFile(s,n)}}if(t?.iconUrl)try{await writeFile(join(o,".beeos-meta.json"),JSON.stringify({iconUrl:t.iconUrl}),"utf8")}catch{}this.logger.info?.(`[bridge-core] Installed skill "${n}" via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(n,o),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${n}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params;let s;try{s=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const i=resolveSkillInstallDir(this.getSkillsDir(),s),n=resolveSkillInstallDir(join(homedir(),".agents","skills"),s);try{let t=!1;try{const e=await lstat(i);if(e.isSymbolicLink()){const e=await readlink(i);await rm(i,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(n);(e.isDirectory()||e.isSymbolicLink())&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${s}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${s}"`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,i=t?.enabled;if(!s||"boolean"!=typeof i)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const n=this.getGateway();if(n?.isConnected)try{const t=await n.skillsUpdate(s,i);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${i}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:i}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:i,created:n}=await this.resolveOrCreateSession(t);n&&this.onSessionLoaded?.(i.sessionId,i.gatewaySessionKey,i.platformAgentId);const o=i.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(i);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!i.title||isPlaceholderTitle(i.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${o} existing="${i.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();i.title=e,i.titleSource="truncated",r?.isConnected&&e&&r.sessionsPatch(i.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(o,{sessionUpdate:"title_update",title:e}),this.generateTitleAsync(i,o,t)}}}const l=buildResolutionPlan(a);if(l.refs.length>0)try{const e=await resolveResources(l,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let d;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{d=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const c=toGatewayPrompt(a,{beeosFileResolutionsByUri:d});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:h,attachments:p}=c,g=resolveStandaloneSlashCommandName(a,h)??resolveSlashCmd(a,h),m="string"==typeof g;if(g&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(g)}catch(e){return t.logger.warn?.(`[bridge-core] /${g} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(i.sessionId,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${g}...`},timestamp:(new Date).toISOString()}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let f=m?h:withUserMessagePrefix(h);const y=this.config.canvasEnabled&&!m&&!1!==i.canvasActive&&!i.channelType;if(y){const e=i.promptsSinceCanvasInjection??0;!i.canvasPromptInjected||e>=10?(f=`${CANVAS_TERM_MAPPING}\n\n${f}`,i.canvasPromptInjected=!0,i.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):i.promptsSinceCanvasInjection=e+1}if(y){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(i.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const t=i.referencedSurfaceId;t&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,i.referencedSurfaceId=void 0)}if(!m){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){i.activeMentions=t;f=`${buildMentionInstruction(t)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else i.activeMentions=void 0}m&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${g}`),i.promptDone||void 0===i.pendingPromptId&&void 0===i.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${o} oldRunId=${i.currentRunId} oldPromptId=${i.pendingPromptId}`),(i.currentRunId||i.gatewayRequestId)&&r.agentCancel(i.gatewaySessionKey,{runId:i.currentRunId,requestId:i.gatewayRequestId}),this.endRun(i,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(i,o,this.transport),i.pendingPromptId=e.id,i.gatewayMethod=u,this.emitPromptAsUserSessionUpdates(o,a),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()}),this.schedulePromptTimeout(i);const S=p.length>0?p.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,w=`req_${randomUUID().replace(/-/g,"")}`,I={sessionKey:i.gatewaySessionKey,message:f,deliver:!1,idempotencyKey:randomUUID()};S?.length&&(I.attachments=S);if(!r.sendFrame({type:"req",id:w,method:u,params:I}))return this.state.clearPromptTimeout(i),i.pendingPromptId=void 0,i.assistantMq?.close(),void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});i.gatewayRequestId=w,this.state.promptsByGatewayRequestId.set(w,i)}replayUserTurnAsChunks(e,t,s,i){const n=splitUserReplayBlocks(t);for(const t of n){let n;if("text"===t.type){if(!t.text)continue;n={type:"text",text:t.text}}else n="resource_link"===t.type?{type:"resource_link",uri:t.uri??"",...t.fileName?{name:t.fileName}:{},...t.mimeType?{mimeType:t.mimeType}:{}}:{type:t.type,...t.mimeType?{mimeType:t.mimeType}:{},...t.fileName?{fileName:t.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:n,...s?{metadata:s}:{},...i?{timestamp:i}:{}})}}async replayAssistantTurnAsStream(e,t){if(!t.sourceMessages?.length){const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls,...t.timestamp?{timestamp:t.timestamp}:{}};return t.metadata&&(s.metadata=t.metadata),void this.transport.sendSessionUpdate(e,s)}for(const s of t.sourceMessages){switch(s.updateType??"agent_message_chunk"){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{}});break;case"agent_thought_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{}});break;case"tool_call":{const i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),o=s.toolName??"tool",r=s.content??"";let a={};if(r)try{a=JSON.parse(r)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:i,toolName:o,args:a,kind:n?.kind,status:n?.status??"in_progress",timestamp:s.timestamp}));break}case"tool_call_update":{const i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),o=s.toolName??n?.name??"tool",r=s.content??n?.output??"";let a=await readEnrichmentCache(i).catch(()=>{});if(!a){const e=extractBeeosFileUrisFromOutput(r);e.length>0&&(a=e.map(e=>({uri:e,name:"file"})))}if(!a&&"beeos_upload_file"===o){const e=await readUploadToolResult(i).catch(()=>{});if(e){const t=buildToolResultSessionContentBlocks(o,e).filter(e=>"resource_link"===e.content?.type&&e.content.uri).map(e=>({uri:e.content.uri,name:e.content.name??"file",...e.content.mimeType?{mimeType:e.content.mimeType}:{}}));t.length>0&&(a=t)}}this.transport.sendSessionUpdate(e,buildToolCallUpdatePayload({toolCallId:i,toolName:o,outputText:r,kind:n?.kind,status:n?.status??"completed",resourceLinks:a,timestamp:s.timestamp}));break}}}}emitPromptAsUserSessionUpdates(e,t){const s=(new Date).toISOString();for(const i of t){const t=this.toUserSessionUpdateContent(i);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t,timestamp:s})}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=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}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}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 t=e.mimeType??e.mime_type??"application/octet-stream",s=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}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,i=t.mimeType??t.mime_type??e.mimeType??e.mime_type,n=t.text??e.text,o=t.data??e.data,r=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!n&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...i?{mimeType:i}:{},...n?{text:n}:{},...o?{data:o}:{},...r?{fileName:r}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const i=this.state.sessions.get(s);if(!i)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const n=this.getGateway(),o=n?.isConnected,r=!(!i.currentRunId&&!i.gatewayRequestId);if(o&&r){n.agentCancel(i.gatewaySessionKey,{runId:i.currentRunId,requestId:i.gatewayRequestId})}this.endRun(i,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(i.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;e.promptDone=!0;const s=e.currentRunId;if("complete"===t.kind){if(s&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(s,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason})}else s&&this.onRunFailed&&this.onRunFailed(s,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message});return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const i=e.error,n=i?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${n}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,n)}const i=e.payload,n=i?.runId;n?(s.currentRunId=n,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(n,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${n} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}pushToolResultContent(e,t,s,i){if(0===s.length)return;const n=i?this.state.promptsByRunId.get(i):void 0;if(!n)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session for runId=${i??"(none)"}`);const o=n.sessionId,r=(new Date).toISOString();for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e,timestamp:r};this.transport.sendSessionUpdate(o,t),n.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const i=await t.request(s,e.params??{});this.transport.sendResult(e.id,i??{})}catch(t){const i=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${i}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:i})}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let i;for(let n=0;n<=s;n++){const o=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(o.ok)return Buffer.from(await o.arrayBuffer());if(429===o.status||o.status>=500){const r=Number(o.headers.get("retry-after"))||0,a=1e3*Math.max(r,2**n);if(i=new Error(`HTTP ${o.status} from ${e}`),n<s){t.logger?.info?.(`[bridge-core] Download ${o.status}, retry ${n+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${o.status}`)}throw i??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),i=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(i,{recursive:!0}),s.extractAllTo(i,!0);const e=await resolveSkillRoot(i),n=await readdir(e,{withFileTypes:!0});let o=0;for(const s of n){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const i=join(e,s.name),n=join(t,s.name);await rename(i,n).catch(async()=>{await cp(i,n,{recursive:!0})}),o++}if(0===o)throw new Error("Zip archive contained no extractable files")}finally{await rm(i,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),i=join(e,s),n=resolve(e),o=resolve(i),r=relative(n,o);if(!r||".."===r||r.startsWith(`..${sep}`)||path.isAbsolute(r))throw new Error("Invalid skill target path");return i}
|
|
1
|
+
import{randomUUID,createHash}from"node:crypto";import{mkdir,writeFile,readFile,rm,lstat,readlink,stat,readdir,rename,cp}from"node:fs/promises";import path from"node:path";import{join,resolve,relative,sep}from"node:path";import{homedir}from"node:os";import AdmZip from"adm-zip";import{sanitizePromptPayload,stripTransportMetadata,isHeartbeatPromptText,isHeartbeatOnlyText}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,extractBeeosFileUrisFromOutput,splitUserReplayBlocks}from"./history-replay.js";import{buildResolutionPlan,resolveResources,applyResolutions}from"./prompt-resource-resolver.js";import{readEnrichmentCache,readUploadToolResult}from"../upload/result-cache.js";import{buildToolResultSessionContentBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{resolveChannelFromSessionKey}from"../channel-detection.js";import{resolveTurnId,recordTurnIdForRole}from"./gateway-events.js";import{ACP_PROTOCOL_VERSION,DEFAULT_PROMPT_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-",":a2a:"];function isBackgroundSessionKey(e){return BACKGROUND_SESSION_PATTERNS.some(t=>e.includes(t))}function isSystemDefaultSessionKey(e){const t=e.split(":");if(t.length<3||"agent"!==t[0])return!1;const s=t.slice(2).join(":");return"main"===s||s===t[1]}function isPlaceholderTitle(e){return!e||"New chat"===e||(!!/^[0-9a-f]{6,12}\s*\(20\d{2}/i.test(e)||(!!/^Sender\s*\(untrusted/i.test(e)||!!/^Conversation\s+info\s*\(untrusted/i.test(e)))}function cleanGatewayDerivedTitle(e){if(!e)return;let t=stripTransportMetadata(e);return/^\[System\b/.test(t)?void 0:(t=t.trim(),t||void 0)}function resolveStandaloneSlashCommandName(e,t){if(0===e.length)return;if(e.some(e=>"text"!==e.type))return;const s=t.trim();if(!s||s.includes("\n")||!s.startsWith("/"))return;const n=s.match(/^\/([^\s:]+)(?::|\s|$)/)?.[1]?.trim();return n&&!n.includes("/")&&SLASH_COMMAND_NAME_RE.test(n)?n.toLowerCase():void 0}function deriveSessionId(e,t){const s=createHash("sha256").update(`${t}:${e}`).digest("hex").substring(0,32);return`session:${s.slice(0,8)}-${s.slice(8,12)}-${s.slice(12,16)}-${s.slice(16,20)}-${s.slice(20,32)}`}function parseAgentIdFromKey(e){const t=e.split(":");return(t.length>=3&&"agent"===t[0]?t[1]:"")||"main"}function createSessionObject(e){const t=parseAgentIdFromKey(e.gatewayKey);return{sessionId:deriveSessionId(e.gatewayKey,e.instanceId),gatewaySessionKey:e.gatewayKey,agentId:t,platformAgentId:e.resolvePlatformAgentId?.(t),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;beeosFileResolver;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_loadSessionLock=new Map;setAgentMapping(e){this.agentMapping=e;for(const t of this.state.sessions.values())!t.platformAgentId&&t.agentId&&(t.platformAgentId=e.get(t.agentId)?.id);for(const e of this._syncResolvers)e();this._syncResolvers=[];for(const e of this._mappingReadyCallbacks)e();this._mappingReadyCallbacks=[]}onceAgentMappingReady(e){this.agentMapping&&this.agentMapping.size>0?e():this._mappingReadyCallbacks.push(e)}markAgentSyncDone(){for(const e of this._syncResolvers)e();this._syncResolvers=[]}waitForAgentMapping(e=15e3){return this.agentMapping?Promise.resolve():new Promise(t=>{this._syncResolvers.push(t);const s=setTimeout(()=>{const e=this._syncResolvers.indexOf(t);-1!==e&&this._syncResolvers.splice(e,1),t()},e);"object"==typeof s&&"unref"in s&&s.unref()})}handleTerminalRequest=null;handleTerminalNotification=null;onRunComplete=null;onRunFailed=null;onRunEnded=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,this.beeosFileResolver=e.beeosFileResolver,this.transport.setSessionUpdateObserver((e,t,s)=>{const n=s?.turnId;if(!n)return;const i=this.state.sessions.get(e);i&&recordTurnIdForRole(i,t,n)})}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),this.ensureVerboseFull(e),this.subscribeSessionMessages(e)}async ensureVerboseFull(e){if(e.verbosePatched)return;const t=this.getGateway();if(t?.isConnected){e.verbosePatched=!0;try{await t.sessionsPatch(e.gatewaySessionKey,{verboseLevel:"full"})}catch(t){e.verbosePatched=!1,this.logger.debug?.(`[bridge-core] ensureVerboseFull failed: ${t instanceof Error?t.message:t}`)}}}ensureSessionForKey(e){const t=this.state.findSessionByKey(e);if(t)return t;const s=createSessionObject({gatewayKey:e,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels});return this.registerSession(s),this.logger.debug?.(`[bridge-core] ensureSessionForKey: lazily created session sid=${s.sessionId} key=${e}`),s}async preloadSessionsFromGateway(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.sessionsList();if(!t||"object"!=typeof t)return;const s=t.sessions;if(!Array.isArray(s))return;let n=0;for(const e of s){const t=e,s=t.sessionKey??t.key??t.id??t.name;if(!s||"string"!=typeof s)continue;if(this.state.findSessionByKey(s))continue;const i=t.label||t.derivedTitle||t.title||void 0,o=i&&!isPlaceholderTitle(i)?i:void 0,r=createSessionObject({gatewayKey:s,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:o});this.registerSession(r),n++}n>0&&this.logger.info?.(`[bridge-core] preloadSessions: loaded ${n} session(s) from gateway`)}catch(e){this.logger.debug?.("[bridge-core] preloadSessions failed:",e)}else this.logger.debug?.("[bridge-core] preloadSessions skipped: gateway not connected")}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"skills/install":await this.handleSkillsInstall(e);break;case"skills/uninstall":await this.handleSkillsUninstall(e);break;case"skills/update":await this.handleSkillsUpdate(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(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:t instanceof Error?t.message:"Internal error"})}}handleCanvasNotification=null;getCanvasState=null;onSessionLoaded=null;onSkillInstalled=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 t=e.params,s=t?.surfaceId,n=this.state.resolveSessionFromParams(t);if(!n){this.logger.warn?.(`[bridge-core] canvas/reference ignored — no session found for sessionId=${t?.sessionId}`);break}n.referencedSurfaceId=s||void 0,this.logger.debug?.(`[bridge-core] Canvas reference ${s?"set":"cleared"}: ${s??"(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 t=this.transport.forwardControl;!1===e.params?.forwardThinking&&(t.forwardThinking=!1),!1===e.params?.forwardToolCalls&&(t.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 t=`agent:${"string"==typeof e.params?.agentId&&e.params.agentId||this.config.gateway.agentId||this.getDefaultAgentId()}:${randomUUID()}`,s="string"==typeof e.params?.cwd?e.params.cwd:".",n=createSessionObject({gatewayKey:t,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s});this.registerSession(n),this.transport.sendResult(e.id,{sessionId:n.sessionId,modes:{availableModes:[{...DEFAULT_MODE}],currentModeId:DEFAULT_MODE.id},_meta:{sessionKey:t,agentId:n.agentId,platformAgentId:n.platformAgentId??null}}),this.fetchHistoryInBackground(t)}fetchHistoryInBackground(e){const t=this.getGateway();t?.isConnected&&loadHistory(t,e,50).catch(e=>{this.logger.debug?.(`[bridge-core] Background history fetch failed: ${e instanceof Error?e.message:e}`)})}async subscribeSessionMessages(e){if(e.messageSubscriptionActive)return;const t=this.getGateway();if(t?.isConnected)try{await t.sessionsMessagesSubscribe(e.gatewaySessionKey),e.messageSubscriptionActive=!0,this.logger.debug?.(`[bridge-core] session.message subscribed sessionKey=${e.gatewaySessionKey}`)}catch(t){this.logger.debug?.(`[bridge-core] session.message subscribe failed key=${e.gatewaySessionKey}: ${t instanceof Error?t.message:t}`)}}async resubscribeAllSessionMessages(){for(const e of this.state.sessions.values())e.messageSubscriptionActive=!1,await this.subscribeSessionMessages(e)}handleSessionMessage(e){const t=e?.sessionKey,s=e?.messageId;if(!t||!s)return;const n=this.state.sessionsByGatewayKey.get(t);if(!n)return;const i=e.message?.role,o="user"===i?"user":"system"===i?"system":"assistant",r=n.lastTurnIdByRole?.[o];if(!r)return void this.logger.debug?.(`[bridge-core] session.message skipped: no lastTurnId role=${o} sessionKey=${t} messageId=${s}`);const a=e.message?.timestamp,l="number"==typeof a&&a>0?new Date(a).toISOString():void 0,d="number"==typeof e.messageSeq?{seq:e.messageSeq}:void 0;this.transport.sendSessionUpdate(n.sessionId,{sessionUpdate:"message_id_bind",turnId:r,role:o,messageId:s,...l?{timestamp:l}:{}},d),n.lastTurnIdByRole&&(n.lastTurnIdByRole[o]=void 0)}async recoverSessionFromGateway(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.sessionsList();if(!s||"object"!=typeof s)return;const n=s.sessions;if(!Array.isArray(n))return;for(const t of n){const s=t,n=s.sessionKey??s.key??s.id??s.name;if(n&&"string"==typeof n&&deriveSessionId(n,this.instanceId)===e){const e=s.label||s.derivedTitle||s.title||void 0,t=e&&!isPlaceholderTitle(e)?e:void 0,i=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,title:t});return this.registerSession(i),i}}}catch(e){this.logger.debug?.("[bridge-core] recoverSessionFromGateway failed:",e)}}async resolveOrCreateSession(e){let t=this.state.sessions.get(e);if(!t&&e.startsWith("agent:")&&(t=this.state.findSessionByKey(e)),t||e.startsWith("agent:")||(t=await this.recoverSessionFromGateway(e)),t)return{session:t,created:!1};const s=e.startsWith("agent:"),n=this.config.gateway.agentId||this.getDefaultAgentId();return t=createSessionObject({gatewayKey:s?e:`agent:${n}:${e}`,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels}),this.registerSession(t),{session:t,created:!0}}async handleSessionLoad(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this._loadSessionLock.get(t);if(s)try{await s}catch{}const n=this._doSessionLoad(e,t);this._loadSessionLock.set(t,n);try{await n}finally{this._loadSessionLock.get(t)===n&&this._loadSessionLock.delete(t)}}async _doSessionLoad(e,t){const{session:s}=await this.resolveOrCreateSession(t),n=s.sessionId;this.state.lastActiveSessionId=n,s.chatReplayDedupKeys.clear();let i=[];try{const e=this.getGateway(),t=await loadHistory(e,s.gatewaySessionKey,100);i=groupIntoTurns(t);const o=i.filter(e=>"user"===e.role).length,r=i.filter(e=>"assistant"===e.role).length,a=i.filter(e=>e.toolCalls?.length).length,l=i.reduce((e,t)=>e+(t.toolCalls?.length??0),0);if(this.logger.debug?.(`[bridge-core] handleSessionLoad sid=${n} messages=${t.length} turns=${i.length} userTurns=${o} asstTurns=${r} turnsWithToolCalls=${a} totalToolCalls=${l}`),i.length>0&&isPlaceholderTitle(s.title)&&"manual"!==s.titleSource){const e=i.find(e=>{if("user"!==e.role)return!1;if("internal_system"===e.provenance?.kind)return!1;const t=stripTransportMetadata(e.content).trim();return t.length>0&&!/^Sender\s*\(untrusted/i.test(t)});if(e?.content){const t=stripTransportMetadata(e.content).trim().slice(0,64).replace(/\n/g," ").trim();if(t){s.title=t,s.titleSource="truncated";const e=this.getGateway();e?.isConnected&&e.sessionsPatch(s.gatewaySessionKey,{label:t}).catch(()=>{}),this.transport.sendSessionUpdate(n,{sessionUpdate:"title_update",title:t}),this.tryGenerateTitleFromHistory(s,n,i)}}}const d=e=>`history:${s.gatewaySessionKey}:${e}`;for(let e=0;e<i.length;e++){const t=i[e];"user"!==t.role||"internal_system"!==t.provenance?.kind?"user"===t.role&&/^\s*system\s*\(untrusted[^)]*\)/i.test(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] system-injected user message`):"user"===t.role&&isHeartbeatPromptText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat cron prompt`):"assistant"===t.role&&isHeartbeatOnlyText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] heartbeat assistant reply`):"system"!==t.role?(this.logger.debug?.(`[bridge-core] SENDING history_turn[${e}] role=${t.role} contentLen=${t.content.length} content=${JSON.stringify(t.content.slice(0,80))} tcCount=${t.toolCalls?.length??0}`),"user"===t.role?this.replayUserTurnAsChunks(n,t.content,t.metadata,t.timestamp,d(e),t.messageId,t._seq):await this.replayAssistantTurnAsStream(n,t,d(e))):(this.logger.debug?.(`[bridge-core] SENDING system_message[${e}] contentLen=${t.content.length}`),this.transport.sendSessionUpdate(n,{sessionUpdate:"system_message_chunk",content:{type:"text",text:t.content},metadata:t.metadata,...t.timestamp?{timestamp:t.timestamp}:{}},{turnId:d(e),...t.messageId?{messageId:t.messageId}:{},...void 0!==t._seq?{seq:t._seq}:{}})):this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] internal_system user message`)}}catch(e){this.logger.warn?.("[bridge-core] Failed to load session history:",e)}const o=new Set;for(const e of i)if(e.toolCalls)for(const t of e.toolCalls)t.id&&o.add(t.id),t.callId&&t.callId!==t.id&&o.add(t.callId);let r="",a="";for(let e=i.length-1;e>=0;e--)if("assistant"===i[e].role){r=i[e].content.slice(-200),a=(i[e].reasoning??"").slice(-200);break}if(void 0!==s.pendingPromptId&&!s.promptDone&&s.assistantMq&&!s.assistantMq.closed)try{this.replayCurrentAssistantStream(s,o,r,a)}catch(e){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}(()=>{this.transport.sendResult(e.id,{sessionId:n}),this.onSessionLoaded?.(n,s.gatewaySessionKey,s.platformAgentId),this.pushSkillsList().catch(e=>{this.logger.warn?.(`[bridge-core] pushSkillsList failed: ${e instanceof Error?e.message:e}`)})})()}replayCurrentAssistantStream(e,t,s="",n=""){if(!e.assistantMq||e.assistantMq.closed)return;const i=e.assistantMq.peek(0);if(0===i.length)return;let o=0,r=0,a="",l="";for(const d of i)if(t?.size&&d.toolCallId&&t.has(d.toolCallId)&&("tool_call"===d.sessionUpdate||"tool_call_update"===d.sessionUpdate))r++;else{if(s.length>0&&"agent_message_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(a+=e,a.length<=s.length)){r++;continue}}if(n.length>0&&"agent_thought_chunk"===d.sessionUpdate){const e=d.content?.text;if("string"==typeof e&&(l+=e,l.length<=n.length)){r++;continue}}this.transport.sendSessionUpdate(e.sessionId,d,{turnId:e.currentRunId}),o++}r>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${r} duplicate tool events (already in history)`),o>0&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:o}})}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||t.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 s=this.getGateway();if(s?.isConnected)try{const e=await s.sessionsList();if(e&&"object"==typeof e&&Array.isArray(e.sessions)){const s=e.sessions;for(const e of s){const s=e,n=s.sessionKey??s.key??s.id??s.name;if(!n||"string"!=typeof n)continue;if(isBackgroundSessionKey(n))continue;const i=deriveSessionId(n,this.instanceId),o=t.find(e=>e.sessionId===n||e.sessionId===i||this.state.sessions.get(e.sessionId)?.gatewaySessionKey===n),r=s.label||cleanGatewayDerivedTitle(s.derivedTitle)||s.title||void 0,a=r&&!isPlaceholderTitle(r)?r:void 0,l=s.model,d=s.modelProvider;if(o){if(a&&isPlaceholderTitle(o.title)){o.title=a;const e=this.state.sessions.get(o.sessionId);e&&isPlaceholderTitle(e.title)&&(e.title=a)}const e=s.updatedAt;e&&(o.updatedAt=e),l&&(o.model=l),d&&(o.modelProvider=d)}else{const e=createSessionObject({gatewayKey:n,instanceId:this.instanceId,resolvePlatformAgentId:e=>this.resolvePlatformAgentId(e),thirdPartyChannels:this.thirdPartyChannels,cwd:s.cwd,title:a});this.registerSession(e),t.push({sessionId:e.sessionId,title:a||"",cwd:s.cwd,updatedAt:s.updatedAt,agentId:e.agentId,platformAgentId:e.platformAgentId,deletable:!isSystemDefaultSessionKey(n),model:l,modelProvider:d})}}}}catch(e){this.logger.debug?.("[bridge-core] sessions.list enrichment failed:",e)}t.sort((e,t)=>(t.updatedAt??0)-(e.updatedAt??0)||e.sessionId.localeCompare(t.sessionId)),this.transport.sendResult(e.id,{sessions:t})}handleSessionRename(e){const t=e.params?.sessionId,s=e.params?.title;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, title"});const n=s.slice(0,64),i=this.state.sessions.get(t);if(!i)return void this.transport.sendAcpError(e.id,Errors.SessionNotFound);i.title=n,i.titleSource="manual";const o=i.gatewaySessionKey,r=this.getGateway();r?.isConnected&&r.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:t,title:n})}static TITLE_TEMPLATES=[{match:["今日日报","日报"],zh:"今日工作日报",en:"Daily Work Log"},{match:["周报","周报总结","本周周报"],zh:"本周工作周报",en:"Weekly Work Report"},{match:["月报","月度汇报","月度总结"],zh:"本月工作月报",en:"Monthly Work Report"},{match:["工作日志","日志"],zh:"工作日志整理",en:"Work Log Review"},{match:["晨会","今日晨会"],zh:"今日晨会纪要",en:"Daily Standup Notes"},{match:["复盘","今日复盘"],zh:"今日工作复盘",en:"Daily Retrospective"},{match:["新会话","new chat","new conversation"],zh:"新会话",en:"New Conversation"}];static TITLE_SYSTEM_PROMPT='You are a conversation title generator. Your task is to generate a concise, human-readable title summarizing the topic of the user\'s question together with the assistant\'s answer.\n\n### Rules:\n- Length: 3-6 words in English, or 6-20 characters in Chinese (up to 24 if needed to keep all key domain terms intact).\n- Language: Match the user\'s input language exactly. Mixed zh/en input → use the dominant language.\n- Extract the core topic (noun-centric) of the Q&A exchange, not the action verb.\n- Use the assistant\'s answer to disambiguate or enrich the topic when the question alone is vague (e.g. "帮我看下" + an answer about x402 → "x402 支付集成").\n- No punctuation, no quotation marks, no emojis, no markdown.\n- No prefix like "Title:" or "Here is". Output the title only.\n- Avoid generic words: "help", "question", "chat", "problem", "task".\n- If both the question and answer are too vague to summarize, return "新会话" (for zh) or "New Conversation" (for en).\n\n### Input Format:\nThe user message will contain both the question and the assistant\'s answer, delimited like this:\n[User Question]\n...question text...\n\n[Assistant Answer]\n...answer text (may be truncated with "..." in the middle)...\n\n### Output Format:\nRespond ONLY with valid JSON, no other text:\n{"title": "<your title here>"}\n\n### Examples:\nInput:\n[User Question]\n帮我写一个关于BeeOS设备接入的PRD\n\n[Assistant Answer]\n好的,这是 BeeOS 设备接入 PRD 的草稿:1. 目标 ...\nOutput: {"title": "BeeOS 设备接入 PRD"}\n\nInput:\n[User Question]\n帮我看下\n\n[Assistant Answer]\n你说的 x402 支付协议是一个基于 HTTP 402 的链上支付标准 ...\nOutput: {"title": "x402 支付协议解析"}\n\nInput:\n[User Question]\nWhat\'s the best way to integrate x402 payment protocol?\n\n[Assistant Answer]\nYou have three options: 1. Use the official SDK ...\nOutput: {"title": "x402 Payment Integration"}\n\nInput:\n[User Question]\nhi\n\n[Assistant Answer]\nHello! How can I help you today?\nOutput: {"title": "New Conversation"}';matchTitleTemplate(e){const t=e.trim().toLowerCase();for(const s of AcpGatewayBridgeCore.TITLE_TEMPLATES)if(s.match.some(e=>e.toLowerCase()===t))return/[\u4e00-\u9fff]/.test(e)?s.zh:s.en}isGuardMessage(e){const t=e.trim();return[...t].length<3||[...t].every(e=>/[\p{P}\p{S}\s]/u.test(e))}truncateQuestionForTitle(e){return e.length>2e3?e.slice(0,1500):e}truncateAnswerForTitle(e){return e.length<=1e3?e:`${e.slice(0,500)}\n...\n${e.slice(-500)}`}generateTitleFromQA(e,t,s,n){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen SKIP guard message sid=${t}`);const i=this.matchTitleTemplate(s);if(i){e.title=i,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:i}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:i})}const o=(process.env.AROUTER_BASE_URL||"").replace(/\/$/,""),r=process.env.AROUTER_API_KEY||"";if(!o||!r)return void this.logger.info?.(`[bridge-core] title-gen SKIP sid=${t}: no AROUTER env vars`);const a=`[User Question]\n${this.truncateQuestionForTitle(s)}\n\n[Assistant Answer]\n${this.truncateAnswerForTitle(n)}`;this.logger.info?.(`[bridge-core] title-gen CALLING LLM sid=${t} q="${s.slice(0,60)}" aLen=${n.length}`),fetch(`${o}/v1/chat/completions`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r}`},body:JSON.stringify({model:"openai/gpt-4o-mini",max_tokens:32,temperature:.3,messages:[{role:"system",content:AcpGatewayBridgeCore.TITLE_SYSTEM_PROMPT},{role:"user",content:a}]})}).then(async s=>{if(!s.ok){const e=await s.text().catch(()=>"");return void this.logger.info?.(`[bridge-core] title-gen FAILED sid=${t} status=${s.status} body=${e.slice(0,300)}`)}const n=await s.json(),i=n.choices?.[0]?.message?.content?.trim();if(!i)return;const o=i.replace(/^```json\s*/,"").replace(/```$/,"").trim();let r;try{r=JSON.parse(o).title?.trim()??""}catch{return void this.logger.debug?.(`[bridge-core] title-gen JSON parse failed: ${o}`)}if(!r)return;if("manual"===e.titleSource)return void this.logger.debug?.("[bridge-core] title-gen discarded: user already renamed");this.logger.info?.(`[bridge-core] title-gen OK sid=${t} title="${r}"`),e.title=r,e.titleSource="auto";const a=this.getGateway();a?.isConnected&&a.sessionsPatch(e.gatewaySessionKey,{label:r}).catch(()=>{}),this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:r})}).catch(e=>{this.logger.info?.(`[bridge-core] title-gen ERROR sid=${t}: ${e instanceof Error?e.message:e}`)})}async tryGenerateTitleFromHistory(e,t,s){if("manual"===e.titleSource||"auto"===e.titleSource)return;const n=this.getGateway();if(!n?.isConnected)return;let i=s;if(!i)try{const t=await loadHistory(n,e.gatewaySessionKey);i=groupIntoTurns(t)}catch(e){return void this.logger.debug?.(`[bridge-core] title-gen loadHistory failed sid=${t}: ${e instanceof Error?e.message:e}`)}const o=i.find(e=>{if("user"!==e.role)return!1;if(!e.content?.trim())return!1;if("internal_system"===e.provenance?.kind)return!1;const t=stripTransportMetadata(e.content).trim();return!!t&&!/^Sender\s*\(untrusted/i.test(t)});if(!o)return;const r=i.indexOf(o),a=i.slice(r+1).find(e=>"assistant"===e.role&&!!e.content?.trim());if(!a)return;const l=stripTransportMetadata(o.content).trim(),d=a.content.trim();l&&d&&this.generateTitleFromQA(e,t,l,d)}async handleSessionDelete(e){const t=e.params?.sessionId;if(!t)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.info?.(`[bridge-core] session/delete sessionId=${t} NOT FOUND in local state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);const n=s.gatewaySessionKey;this.logger.info?.(`[bridge-core] session/delete sessionId=${t} 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(t){const s=t instanceof Error?t.message:String(t);if(this.logger.info?.(`[bridge-core] Gateway sessions.delete FAILED for key=${n}: ${s}`),!s.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(t){return this.logger.warn?.(`[bridge-core] Gateway sessions.reset also failed for key=${n}: ${t instanceof Error?t.message:t}`),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!==s.pendingPromptId&&this.failPrompt(s,Errors.SessionDeleted.rpcCode,Errors.SessionDeleted.message),this.state.clearPromptTimeout(s),this.state.clearStreamIdleTimer(s),s.assistantMq?.close(),s.currentRunId&&this.state.promptsByRunId.delete(s.currentRunId);const r=new Set;for(const[e,s]of this.state.toolCallToSessionMap)s===t&&(this.state.toolCallToSessionMap.delete(e),r.add(e));for(const[e,t]of this.state.toolCallIdMap)r.has(t)&&this.state.toolCallIdMap.delete(e);this.state.sessions.delete(t),this.state.sessionsByGatewayKey.delete(n),this.transport.resetSessionUpdateCount(t),i&&this.onSessionDeleted?.(n),this.logger.info?.(`[bridge-core] session/delete DONE sessionId=${t} 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 t=e.params?.sessionId;if(!t)return this.logger.warn?.("[bridge-core] session/clear missing sessionId"),void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId"});const s=this.state.sessions.get(t);if(!s)return this.logger.warn?.(`[bridge-core] session/clear sessionId=${t} not found in state`),void this.transport.sendAcpError(e.id,Errors.SessionNotFound);this.logger.info?.(`[bridge-core] session/clear sessionId=${t} gatewayKey=${s.gatewaySessionKey}`);const n=this.getGateway(),i=n?.isConnected;if(s.promptDone||void 0===s.pendingPromptId&&void 0===s.currentRunId||(this.logger.info?.(`[bridge-core] session/clear: cancelling active run sid=${t} runId=${s.currentRunId}`),i&&(s.currentRunId||s.gatewayRequestId)&&n.agentCancel(s.gatewaySessionKey,{runId:s.currentRunId,requestId:s.gatewayRequestId}),this.endRun(s,{kind:"complete",stopReason:"cancelled"})),i&&s.gatewaySessionKey)try{await n.sessionsReset(s.gatewaySessionKey),this.logger.info?.(`[bridge-core] Gateway sessions.reset SUCCESS for key=${s.gatewaySessionKey}`)}catch(e){this.logger.warn?.(`[bridge-core] Gateway sessions.reset FAILED for key=${s.gatewaySessionKey}: ${e instanceof Error?e.message:e}`)}else this.logger.warn?.(`[bridge-core] session/clear skipped gateway reset: connected=${i}, key=${s.gatewaySessionKey}`);s.chatReplayDedupKeys.clear(),this.logger.info?.("[bridge-core] session/clear dedupKeys cleared, calling onSessionCleared"),this.onSessionCleared?.(t,s.gatewaySessionKey),this.transport.sendResult(e.id,{cleared:!0}),this.logger.info?.("[bridge-core] session/clear DONE, sent cleared=true")}async handleSessionSetMode(e){const t=e.params?.sessionId,s=e.params?.mode,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();if(n&&i?.isConnected&&s){const e={};if("verbose"===s||"detailed"===s?e.verboseLevel="full":"concise"!==s&&"default"!==s||(e.verboseLevel="default"),"reasoning"!==s&&"thinking"!==s||(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:s??"default"})}async handleSessionSetModel(e){const t=e.params?.sessionId,s=e.params?.model,n=t?this.state.sessions.get(t):void 0,i=this.getGateway();let o=s??null,r=null;if(n&&i?.isConnected&&s)try{const e=await i.sessionsPatch(n.gatewaySessionKey,{model:s}),t=e?.resolved;t?.model&&(o=t.model),t?.modelProvider&&(r=t.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:r})}async handleModelsList(e){const t=this.getGateway();if(t?.isConnected)try{const s=await t.request("models.list",{});this.transport.sendResult(e.id,s??{models:[]})}catch(t){this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Failed to list models: ${t instanceof Error?t.message:t}`})}else this.transport.sendAcpError(e.id,Errors.GatewayNotConnected)}async handleSkillsList(e){const t=await this.fetchSimplifiedSkills();this.transport.sendResult(e.id,{skills:t})}async fetchSimplifiedSkills(){const e=this.getGateway();if(!e?.isConnected)return[];try{const t=await e.skillsStatus();if(!t?.skills)return[];const s=t.skills.filter(e=>!1!==e.eligible||!0===e.disabled).sort((e,t)=>e.always&&!t.always?-1:!e.always&&t.always?1:e.name.localeCompare(t.name)).map(e=>({name:e.name,description:e.description,emoji:e.emoji,always:e.always||void 0,bundled:e.bundled||void 0,disabled:e.disabled||void 0,skillKey:e.skillKey||void 0,iconUrl:void 0})),n=this.getSkillsDir();for(const e of s){const t=e.skillKey??e.name;try{const s=await readFile(join(n,t,".beeos-meta.json"),"utf8"),i=JSON.parse(s);i?.iconUrl&&(e.iconUrl=i.iconUrl)}catch{}}return s}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}})}getSkillsDir(){const e=process.env.OPENCLAW_HOME||join(homedir(),".openclaw");return join(e,"skills")}async handleSkillsInstall(e){const t=e.params,s=t?.zipUrl,n=t?.files;let i;try{i=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill slug"})}if(!n?.length&&!s)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skill files or zipUrl"});const o=resolveSkillInstallDir(this.getSkillsDir(),i);try{if(await mkdir(o,{recursive:!0}),s){const e=await fetchWithRetry(s,{maxRetries:4,logger:this.logger});await extractZipToDir(e,o)}else if(n?.length){const e=resolve(o);for(const t of n){const s=join(o,t.path);if(!resolve(s).startsWith(e+sep))throw new Error(`File path escapes skill directory: ${t.path}`);await mkdir(join(s,".."),{recursive:!0});const n=await fetch(t.downloadUrl,{signal:AbortSignal.timeout(12e4)});if(!n.ok)throw new Error(`Failed to download ${t.path}: ${n.status}`);const i=Buffer.from(await n.arrayBuffer());await writeFile(s,i)}}if(t?.iconUrl)try{await writeFile(join(o,".beeos-meta.json"),JSON.stringify({iconUrl:t.iconUrl}),"utf8")}catch{}this.logger.info?.(`[bridge-core] Installed skill "${i}" via ${s?"zip":"files"}`),this.transport.sendResult(e.id,{success:!0}),this.onSkillInstalled?.(i,o),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to install skill "${i}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Install failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUninstall(e){const t=e.params;let s;try{s=validateSkillSlug(t?.slug||t?.name||"")}catch{return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Invalid or missing skill name"})}const n=resolveSkillInstallDir(this.getSkillsDir(),s),i=resolveSkillInstallDir(join(homedir(),".agents","skills"),s);try{let t=!1;try{const e=await lstat(n);if(e.isSymbolicLink()){const e=await readlink(n);await rm(n,{force:!0}),await rm(e,{recursive:!0,force:!0}),t=!0}else e.isDirectory()&&(await rm(n,{recursive:!0,force:!0}),t=!0)}catch{}try{const e=await lstat(i);(e.isDirectory()||e.isSymbolicLink())&&(await rm(i,{recursive:!0,force:!0}),t=!0)}catch{}t||this.logger.warn?.(`[bridge-core] Skill "${s}" not found in any known location`),this.logger.info?.(`[bridge-core] Uninstalled skill "${s}"`),this.transport.sendResult(e.id,{success:!0}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to uninstall skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Uninstall failed: ${t instanceof Error?t.message:String(t)}`})}}async handleSkillsUpdate(e){const t=e.params,s=t?.skillKey,n=t?.enabled;if(!s||"boolean"!=typeof n)return void this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing skillKey or enabled"});const i=this.getGateway();if(i?.isConnected)try{const t=await i.skillsUpdate(s,n);this.logger.info?.(`[bridge-core] Updated skill "${s}" enabled=${n}`),this.transport.sendResult(e.id,{ok:t?.ok??!0,skillKey:s,enabled:n}),await this.pushSkillsList()}catch(t){this.logger.error?.(`[bridge-core] Failed to update skill "${s}": ${t instanceof Error?t.message:t}`),this.transport.sendAcpError(e.id,Errors.InternalError,{message:`Update failed: ${t instanceof Error?t.message:String(t)}`})}else this.transport.sendAcpError(e.id,Errors.InternalError,{message:"Gateway not connected"})}async handleSessionPrompt(e){const t=e.params?.sessionId,s=e.params?.prompt;if(!t||!s)return void this.transport.sendAcpError(e.id,Errors.MissingParam,{param:"sessionId, prompt"});const{session:n,created:i}=await this.resolveOrCreateSession(t);i&&this.onSessionLoaded?.(n.sessionId,n.gatewaySessionKey,n.platformAgentId);const o=n.sessionId;this.state.lastActiveSessionId=o,this.onPromptStart?.();const r=this.getGateway();if(!r?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);await this.ensureVerboseFull(n);let a=extractPromptBlocks(sanitizePromptPayload(s));if(!n.title||isPlaceholderTitle(n.title)){const e=a.find(e=>"text"===e.type&&e.text);if(e&&"text"in e&&e.text){const t=stripTransportMetadata(e.text).trim();if(this.logger.info?.(`[bridge-core] title-gen sid=${o} existing="${n.title}" cleanText="${t.slice(0,80)}" isPlaceholder=${isPlaceholderTitle(t)}`),t&&!isPlaceholderTitle(t)){const e=t.slice(0,64).replace(/\n/g," ").trim();n.title=e,n.titleSource="truncated",r?.isConnected&&e&&r.sessionsPatch(n.gatewaySessionKey,{label:e}).catch(e=>{this.logger.debug?.(`[bridge-core] sessionsPatch (title) failed: ${e instanceof Error?e.message:e}`)}),this.transport.sendSessionUpdate(o,{sessionUpdate:"title_update",title:e})}}}const l=buildResolutionPlan(a);if(l.refs.length>0)try{const e=await resolveResources(l,this.writeObs);a=applyResolutions(a,e)}catch(e){this.logger.warn?.(`[bridge-core] Resource pre-resolution failed: ${e instanceof Error?e.message:e}`)}let d;if(this.beeosFileResolver){const e=this.beeosFileResolver.buildResolutionPlan(a);if(e.refs.length>0)try{d=await this.beeosFileResolver.resolveMetadataForPrompt(e)}catch(e){this.logger.warn?.(`[bridge-core] BeeOS file resolution failed: ${e instanceof Error?e.message:e}`)}}const c=toGatewayPrompt(a,{beeosFileResolutionsByUri:d});if("error"in c)return void this.transport.sendAcpError(e.id,Errors.PromptConvertFailed,{message:c.error});const{message:h,attachments:g}=c,m=resolveStandaloneSlashCommandName(a,h)??resolveSlashCmd(a,h),p="string"==typeof m;if(m&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(m)}catch(e){return t.logger.warn?.(`[bridge-core] /${m} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(n.sessionId,a,resolveTurnId(n)),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${m}...`},timestamp:(new Date).toISOString()},{turnId:resolveTurnId(n)}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let f=p?h:withUserMessagePrefix(h);const y=this.config.canvasEnabled&&!p&&!1!==n.canvasActive&&!n.channelType;if(y){const e=n.promptsSinceCanvasInjection??0;!n.canvasPromptInjected||e>=10?(f=`${CANVAS_TERM_MAPPING}\n\n${f}`,n.canvasPromptInjected=!0,n.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):n.promptsSinceCanvasInjection=e+1}if(y){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(n.gatewaySessionKey);f=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${f}`}const t=n.referencedSurfaceId;t&&(f=`${CANVAS_UPDATE_HINT}\n[IMPORTANT: The user selected canvas surface "${t}" for editing. You MUST call render_ui with id="${t}" to UPDATE this existing surface. Do NOT create a new surface with a different id.]\n${f}`,n.referencedSurfaceId=void 0)}if(!p){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){n.activeMentions=t;f=`${buildMentionInstruction(t)}\n${f}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else n.activeMentions=void 0}p&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${m}`),n.promptDone||void 0===n.pendingPromptId&&void 0===n.currentRunId||(this.logger.warn?.(`[bridge-core] handleSessionPrompt: force-ending stale run sid=${o} oldRunId=${n.currentRunId} oldPromptId=${n.pendingPromptId}`),(n.currentRunId||n.gatewayRequestId)&&r.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId}),this.endRun(n,{kind:"complete",stopReason:"cancelled"})),this.state.resetForNewRun(n,o,this.transport),n.pendingPromptId=e.id,n.gatewayMethod=u;const S=resolveTurnId(n);this.emitPromptAsUserSessionUpdates(o,a,S),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()},S?{turnId:S}:void 0),this.schedulePromptTimeout(n);const w=g.length>0?g.map(e=>({type:e.type,content:e.content,...e.mimeType?{mimeType:e.mimeType}:{},...e.fileName?{fileName:e.fileName}:{}})):void 0,I=`req_${randomUUID().replace(/-/g,"")}`,b={sessionKey:n.gatewaySessionKey,message:f,deliver:!1,idempotencyKey:randomUUID()};w?.length&&(b.attachments=w);if(!r.sendFrame({type:"req",id:I,method:u,params:b}))return this.state.clearPromptTimeout(n),n.pendingPromptId=void 0,n.assistantMq?.close(),void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});n.gatewayRequestId=I,this.state.promptsByGatewayRequestId.set(I,n)}replayUserTurnAsChunks(e,t,s,n,i,o,r){const a=splitUserReplayBlocks(t),l=i||o||void 0!==r?{...i?{turnId:i}:{},...o?{messageId:o}:{},...void 0!==r?{seq:r}:{}}:void 0;for(const t of a){let i;if("text"===t.type){if(!t.text)continue;i={type:"text",text:t.text}}else i="resource_link"===t.type?{type:"resource_link",uri:t.uri??"",...t.fileName?{name:t.fileName}:{},...t.mimeType?{mimeType:t.mimeType}:{}}:{type:t.type,...t.mimeType?{mimeType:t.mimeType}:{},...t.fileName?{fileName:t.fileName}:{}};this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:i,...s?{metadata:s}:{},...n?{timestamp:n}:{}},l)}}async replayAssistantTurnAsStream(e,t,s){const n=(e,t)=>{if(s||e||void 0!==t)return{...s?{turnId:s}:{},...e?{messageId:e}:{},...void 0!==t?{seq:t}:{}}},i=n(t.messageId,t._seq);if(!t.sourceMessages?.length){const s={sessionUpdate:"history_turn",role:t.role,content:t.content,reasoning:t.reasoning,toolCalls:t.toolCalls,...t.timestamp?{timestamp:t.timestamp}:{}};return t.metadata&&(s.metadata=t.metadata),void this.transport.sendSessionUpdate(e,s,i)}for(const s of t.sourceMessages){const i=s.updateType??"agent_message_chunk",o=n(s.messageId,s._seq);switch(i){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{}},o);break;case"agent_thought_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{}},o);break;case"tool_call":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),r=s.toolName??"tool",a=s.content??"";let l={};if(a)try{l=JSON.parse(a)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:n,toolName:r,args:l,kind:i?.kind,status:i?.status??"in_progress",timestamp:s.timestamp}),o);break}case"tool_call_update":{const n=s.toolCallId??"",i=t.toolCalls?.find(e=>e.id===n),r=s.toolName??i?.name??"tool",a=s.content??i?.output??"";let l=await readEnrichmentCache(n).catch(()=>{});if(!l){const e=extractBeeosFileUrisFromOutput(a);e.length>0&&(l=e.map(e=>({uri:e,name:"file"})))}if(!l&&"beeos_upload_file"===r){const e=await readUploadToolResult(n).catch(()=>{});if(e){const t=buildToolResultSessionContentBlocks(r,e).filter(e=>"resource_link"===e.content?.type&&e.content.uri).map(e=>({uri:e.content.uri,name:e.content.name??"file",...e.content.mimeType?{mimeType:e.content.mimeType}:{}}));t.length>0&&(l=t)}}this.transport.sendSessionUpdate(e,buildToolCallUpdatePayload({toolCallId:n,toolName:r,outputText:a,kind:i?.kind,status:i?.status??"completed",resourceLinks:l,timestamp:s.timestamp}),o);break}}}}emitPromptAsUserSessionUpdates(e,t,s){const n=(new Date).toISOString(),i=s?{turnId:s}:void 0;for(const s of t){const t=this.toUserSessionUpdateContent(s);t&&this.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:t,timestamp:n},i)}}toUserSessionUpdateContent(e){if("text"===e.type){if(!e.text)return;return{type:"text",text:e.text}}if("image"===e.type){const t=e.mimeType??e.mime_type??"application/octet-stream",s=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}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}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 t=e.mimeType??e.mime_type??"application/octet-stream",s=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}:{},...t?{mimeType:t}:{},...s?{fileName:s}:{}}}if("resource"===e.type){const t=e.resource??{},s=t.uri??e.uri,n=t.mimeType??t.mime_type??e.mimeType??e.mime_type,i=t.text??e.text,o=t.data??e.data,r=t.fileName??t.file_name??t.filename??t.name??e.fileName??e.file_name??e.filename??e.name;if(!s&&!i&&!o)return;return{type:"resource",resource:{...s?{uri:s}:{},...n?{mimeType:n}:{},...i?{text:i}:{},...o?{data:o}:{},...r?{fileName:r}:{}}}}}handleSessionCancel(e,t){const s=t?.sessionId;if(!s)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.MissingParam,{param:"sessionId"}));const n=this.state.sessions.get(s);if(!n)return void(void 0!==e&&this.transport.sendAcpError(e,Errors.SessionNotFound));const i=this.getGateway(),o=i?.isConnected,r=!(!n.currentRunId&&!n.gatewayRequestId);if(o&&r){i.agentCancel(n.gatewaySessionKey,{runId:n.currentRunId,requestId:n.gatewayRequestId})}this.endRun(n,{kind:"complete",stopReason:"cancelled"})||this.transport.sendSessionUpdate(n.sessionId,{sessionUpdate:"stream_complete",stopReason:"cancelled"},{turnId:resolveTurnId(n)}),void 0!==e&&this.transport.sendResult(e,{})}endRun(e,t){if(e.promptDone)return this.logger.debug?.(`[bridge-core] endRun skipped (promptDone=true) sessionId=${e.sessionId} outcome=${t.kind}`),!1;if(void 0===e.pendingPromptId&&void 0===e.currentRunId)return this.logger.debug?.(`[bridge-core] endRun skipped (no pendingPromptId and no currentRunId) sessionId=${e.sessionId} outcome=${t.kind}`),!1;const s=resolveTurnId(e),n=s?{turnId:s}:void 0;e.promptDone=!0;const i=e.currentRunId;if("complete"===t.kind){if(i&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(i,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason},n),this.tryGenerateTitleFromHistory(e,e.sessionId)}else i&&this.onRunFailed&&this.onRunFailed(i,t.message),void 0!==e.pendingPromptId&&this.transport.sendError(e.pendingPromptId,t.code,t.message),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:"error",error:t.message},n);return this.cleanupPrompt(e),this.onRunEnded?.(e,t),!0}schedulePromptTimeout(e){this.state.clearPromptTimeout(e);const t=this.config.promptTimeoutMs??DEFAULT_PROMPT_TIMEOUT_MS;t<=0||(e.promptTimeoutTimer=setTimeout(()=>{void 0!==e.pendingPromptId&&(this.logger.warn?.(`[bridge-core] Prompt timed out sessionId=${e.sessionId} after ${t}ms`),this.failPrompt(e,Errors.PromptTimeout.rpcCode,`Prompt timed out after ${t}ms`))},t),"object"==typeof e.promptTimeoutTimer&&"unref"in e.promptTimeoutTimer&&e.promptTimeoutTimer.unref())}cleanupPrompt(e){this.state.clearPromptTimeout(e),this.state.clearStreamIdleTimer(e);const t=e.currentRunId;if(t){this.state.promptsByRunId.delete(t),this.state.markRunCompleted(t);const e=`tc_${t}_`;for(const[t,s]of this.state.toolCallIdMap)s.startsWith(e)&&this.state.toolCallIdMap.delete(t)}e.gatewayRequestId&&this.state.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,e.assistantMq?.close()}completePrompt(e,t){this.endRun(e,{kind:"complete",stopReason:t})}failPrompt(e,t,s){this.endRun(e,{kind:"error",code:t,message:s})}cleanupAllPendingPrompts(){for(const e of this.state.sessions.values())this.failPrompt(e,Errors.GatewayDisconnected.rpcCode,Errors.GatewayDisconnected.message)}handleGatewayResponse(e){const t=e.id;if(!t)return;const s=this.state.promptsByGatewayRequestId.get(t);if(!s||s.promptDone)return;if(!e.ok){const n=e.error,i=n?.message??"gateway returned error";return this.logger.warn?.(`[bridge-core] handleGatewayResponse: gateway rejected prompt gatewayRequestId=${t} error=${i}`),void this.failPrompt(s,Errors.SendFailed.rpcCode,i)}const n=e.payload,i=n?.runId;i?(s.currentRunId=i,s.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(i,s),this.logger.debug?.(`[bridge-core] handleGatewayResponse: bound runId=${i} gatewayRequestId=${t} sessionId=${s.sessionId}`)):this.logger.debug?.(`[bridge-core] handleGatewayResponse: no runId in payload gatewayRequestId=${t} sessionId=${s.sessionId}`)}maybeCompletePromptFromChatState(e,t){if(e.promptDone||"chat.send"!==e.gatewayMethod)return;const s="string"==typeof t.state?t.state.toLowerCase():void 0;if(s)if("final"===s)this.completePrompt(e,"end_turn");else if("aborted"===s||"cancelled"===s||"cancel"===s)this.completePrompt(e,"cancelled");else if("error"===s){const s=("string"==typeof t.summary?t.summary:void 0)??("string"==typeof t.message?t.message:void 0)??"gateway chat error";this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}pushToolResultContent(e,t,s,n){if(0===s.length)return;const i=n?this.state.promptsByRunId.get(n):void 0;if(!i)return void this.logger.warn?.(`[bridge-core] pushToolResultContent dropped — no session for runId=${n??"(none)"}`);const o=i.sessionId,r=(new Date).toISOString(),a={turnId:resolveTurnId(i)};for(const e of s){const t={sessionUpdate:"agent_message_chunk",content:e,timestamp:r};this.transport.sendSessionUpdate(o,t,a),i.assistantMq?.push(t)}this.writeObs({component:"bridge-core",domain:"tool",name:"tool_result.direct_push",severity:"info",payload:{toolCallId:e,toolName:t,blockCount:s.length,sessionId:o}})}async handleCronRequest(e){const t=this.getGateway();if(!t?.isConnected)return void this.transport.sendAcpError(e.id,Errors.GatewayNotConnected);const s=e.method.replace("/",".");try{const n=await t.request(s,e.params??{});this.transport.sendResult(e.id,n??{})}catch(t){const n=t instanceof Error?t.message:"cron request failed";this.logger.warn?.(`[bridge-core] ${s} failed: ${n}`),this.transport.sendAcpError(e.id,Errors.SendFailed,{message:n})}}}async function fetchWithRetry(e,t={}){const s=t.maxRetries??3;let n;for(let i=0;i<=s;i++){const o=await fetch(e,{signal:AbortSignal.timeout(12e4),redirect:"follow"});if(o.ok)return Buffer.from(await o.arrayBuffer());if(429===o.status||o.status>=500){const r=Number(o.headers.get("retry-after"))||0,a=1e3*Math.max(r,2**i);if(n=new Error(`HTTP ${o.status} from ${e}`),i<s){t.logger?.info?.(`[bridge-core] Download ${o.status}, retry ${i+1}/${s} in ${(a/1e3).toFixed(0)}s`),await new Promise(e=>setTimeout(e,a));continue}}throw new Error(`Failed to download zip: ${o.status}`)}throw n??new Error("Download failed after retries")}const SKILL_ROOT_MARKERS=["SKILL.md","skill.md","SKILL.MD"];async function resolveSkillRoot(e){for(const t of SKILL_ROOT_MARKERS)try{return await stat(join(e,t)),e}catch{}const t=(await readdir(e,{withFileTypes:!0})).filter(e=>e.isDirectory()&&!e.name.startsWith(".")&&"__MACOSX"!==e.name);if(1===t.length){const s=join(e,t[0].name);for(const e of SKILL_ROOT_MARKERS)try{return await stat(join(s,e)),s}catch{}return s}return e}async function extractZipToDir(e,t){const s=new AdmZip(e),n=join(t,".beeos-extract-tmp-"+Date.now());try{await mkdir(n,{recursive:!0}),s.extractAllTo(n,!0);const e=await resolveSkillRoot(n),i=await readdir(e,{withFileTypes:!0});let o=0;for(const s of i){if(s.name.startsWith(".beeos-extract-tmp")||"__MACOSX"===s.name)continue;const n=join(e,s.name),i=join(t,s.name);await rename(n,i).catch(async()=>{await cp(n,i,{recursive:!0})}),o++}if(0===o)throw new Error("Zip archive contained no extractable files")}finally{await rm(n,{recursive:!0,force:!0}).catch(()=>{})}}const VALID_SLUG_PATTERN=/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i,NON_ASCII_PATTERN=/[^\x00-\x7F]/;function validateSkillSlug(e){const t=e.trim();if(!t||t.includes("/")||t.includes("\\")||t.includes(".."))throw new Error(`Invalid skill slug: ${e}`);if(NON_ASCII_PATTERN.test(t)||!VALID_SLUG_PATTERN.test(t))throw new Error(`Invalid skill slug: ${e}`);return t}function safeDirName(e){const t=e.trim();return t?t.replaceAll("/","__").replaceAll("\\","__"):t}function resolveSkillInstallDir(e,t){const s=safeDirName(t),n=join(e,s),i=resolve(e),o=resolve(n),r=relative(i,o);if(!r||".."===r||r.startsWith(`..${sep}`)||path.isAbsolute(r))throw new Error("Invalid skill target path");return n}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if(!isRecord(t)||!Array.isArray(t.files))return;const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;onStreamDelta;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.onStreamDelta=t.onStreamDelta}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.transport.sendSessionUpdate(t.sessionId,s),t.assistantMq?.push(s),this.onStreamDelta?.(t,s)}resolveToolCallId(t,e,s){const o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const a=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(a,t.sessionId),a}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}const s=e.sessionId,o=t.message;if(o){this.touchStreamActivity(e);if("assistant"===o.role){if(!Array.isArray(o.content))return;for(const a of o.content){const n=a;if(!isRecord(n))continue;const r=asString(n.type);if(!r)continue;const i=toISO(o.timestamp);if("thinking"===r){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const o=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...i?{timestamp:i}:{}};this.transport.sendSessionUpdate(s,a)&&(e.assistantMq?.push(a),this.onStreamDelta?.(e,a));continue}if("toolCall"===r){if(this.sendToolCallSpacer(e,i),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=resolveToolArgs(n),r=dedupKey("chat:tool_start",t,JSON.stringify(a));if(e.chatReplayDedupKeys.has(r))continue;e.chatReplayDedupKeys.add(r);const l=buildToolCallPayload({toolCallId:t,toolName:o,args:a,timestamp:i});this.transport.sendSessionUpdate(s,l)&&(e.assistantMq?.push(l),this.onStreamDelta?.(e,l));continue}if("toolResult"===r){if(this.sendToolCallSpacer(e,i),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),o=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",a=n.result,r=normalizeText(n.text)??normalizeText(n.result)??(isRecord(a)||Array.isArray(a)?JSON.stringify(a,null,2):void 0);if(!r)continue;const l=dedupKey("chat:tool_result",t,r);if(e.chatReplayDedupKeys.has(l))continue;e.chatReplayDedupKeys.add(l);const c=buildToolCallUpdatePayload({toolCallId:t,toolName:o,outputText:r,rawOutput:a,resourceLinks:extractResourceLinksFromRawResult(a),timestamp:i});this.transport.sendSessionUpdate(s,c)&&(e.assistantMq?.push(c),this.onStreamDelta?.(e,c));continue}if("text"===r&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const o=stripTransportMetadata(n.text);if(!o)continue;const a=dedupKey("chat:text",o);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const r=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=o,o.startsWith(r))l=o.slice(r.length);else{if(r.startsWith(o))continue;l=o}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${r.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...i?{timestamp:i}:{}};this.transport.sendSessionUpdate(s,c),e.assistantMq?.push(c),this.onStreamDelta?.(e,c)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}const s=e.sessionId;this.touchStreamActivity(e);const o=toISO(t.ts),a=t=>{this.transport.sendSessionUpdate(s,t),e.assistantMq?.push(t),this.onStreamDelta?.(e,t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const e=normalizeTransportText(n);e&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${e.length} source=${t.data?.delta?"delta":"cumulative"}`),a({sessionUpdate:"agent_message_chunk",content:{type:"text",text:e},...o?{timestamp:o}:{}}))}const r=[],i=t.data;i&&(Array.isArray(i.content)?r.push(...i.content):isRecord(i.content)?r.push(i.content):"string"==typeof i.type&&r.push(i));for(const t of r){if(!isRecord(t))continue;const n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const a=normalizeText(t.thinking)??normalizeText(t.text);if(a){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:a},...o?{timestamp:o}:{}};this.transport.sendSessionUpdate(s,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}continue}if("toolCall"===n){this.sendToolCallSpacer(e,o),e.hasAgentToolStartStream=!0;const s=this.resolveToolCallId(e,t,"start"),n=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);a(buildToolCallPayload({toolCallId:s,toolName:n,args:r,timestamp:o}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,o),e.hasAgentToolResultStream=!0;const s=this.resolveToolCallId(e,t,"result"),n=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&a(buildToolCallUpdatePayload({toolCallId:s,toolName:n,outputText:i,rawOutput:r,resourceLinks:extractResourceLinksFromRawResult(r),timestamp:o}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const a=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(a){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:a},...o?{timestamp:o}:{}};this.transport.sendSessionUpdate(s,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}}else if("tool"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0;if("start"===n){this.sendToolCallSpacer(e,o),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,s,"start"),n=asString(s?.name)??asString(s?.toolName)??asString(s?.tool_name)??"tool",r=resolveToolArgs(s);a(buildToolCallPayload({toolCallId:t,toolName:n,args:r,timestamp:o}))}else if("result"===n)this.sendToolCallSpacer(e,o),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,o).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=s?.toolCall;if(t){this.sendToolCallSpacer(e,o),e.hasAgentToolStartStream=!0;const n=asString(t?.name)??asString(s?.name)??asString(s?.toolName)??"tool",r={...s,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);a(buildToolCallPayload({toolCallId:i,toolName:n,args:l,timestamp:o}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o)this.completePrompt(e,"end_turn");else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,a=o&&"object"==typeof o?o:void 0,n=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof a?.message?a.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(a?JSON.stringify(a):void 0)??"gateway lifecycle error",r=formatFriendlyError(n);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const o=e.data,a=o?.toolCall,n=("string"==typeof a?.id?a.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(n?this.state.toolCallIdMap.get(n):void 0)??n??`tc_${e.runId}_${e.seq}`,i=("string"==typeof a?.name?a.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,c=n??r,d=o?.result;let m;try{m=await resolveToolResultBlocks({toolCallId:c,toolName:i,rawText:l,rawResult:d})}catch{m=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===m.length&&l&&(m=[{type:"content",content:{type:"text",text:l}}]);const p=[],u=[];for(const t of m)t.content&&("resource_link"===t.content.type&&t.content.uri?u.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&p.push(t.content.text));const h=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:p.join("\n"),rawOutput:d,resourceLinks:u,timestamp:s});this.transport.sendSessionUpdate(t.sessionId,h)&&(t.assistantMq?.push(h),this.onStreamDelta?.(t,h))}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear()}handleCronEvent(t){if(this.transport.sendNotification("_beeos/cron_event",{payload:t}),this.config.agentGatewayUrl&&this.agentFetchFn){const e=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:t})}).catch(t=>{this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)})}}}
|
|
1
|
+
import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText}from"../message-filter.js";import{resolveToolResultBlocks}from"./tool-result-payload-strategies.js";import{buildToolCallPayload,buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}function extractResourceLinksFromRawResult(t){if(!isRecord(t)||!Array.isArray(t.files))return;const e=[];for(const s of t.files){if(!isRecord(s))continue;const t=asString(s.uri)??(asString(s.fileId)?`beeos-file://${s.fileId}`:void 0);t&&e.push({uri:t,name:asString(s.name)??"file",...asString(s.mimeType)?{mimeType:asString(s.mimeType)}:{}})}return e.length>0?e:void 0}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const o=roleFromSessionUpdate(e);o&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[o]=s)}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;maybeCompletePromptFromChatState;agentFetchFn;onStreamDelta;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.completePrompt=t.completePrompt,this.failPrompt=t.failPrompt,this.maybeCompletePromptFromChatState=t.maybeCompletePromptFromChatState,this.agentFetchFn=t.agentFetch,this.onStreamDelta=t.onStreamDelta}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.completePrompt(t,"end_turn"))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);return recordTurnIdForRole(t,e,s),this.transport.sendSessionUpdate(t.sessionId,e,{turnId:s})}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s),this.onStreamDelta?.(t,s)}resolveToolCallId(t,e,s){const o=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(o){const e=this.state.toolCallIdMap.get(o);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(o,o),t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o)}const n=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);if("assistant"===s.role){if(!Array.isArray(s.content))return;for(const o of s.content){const n=o;if(!isRecord(n))continue;const a=asString(n.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(e.hasAgentThinkingStream)continue;const t=normalizeText(n.thinking)??normalizeText(n.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,o)&&(e.assistantMq?.push(o),this.onStreamDelta?.(e,o));continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",t,JSON.stringify(o));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:o,timestamp:r});this.sendTurnUpdate(e,i)&&(e.assistantMq?.push(i),this.onStreamDelta?.(e,i));continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,n,"result"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=n.result,a=normalizeText(n.text)??normalizeText(n.result)??(isRecord(o)||Array.isArray(o)?JSON.stringify(o,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i);const l=buildToolCallUpdatePayload({toolCallId:t,toolName:s,outputText:a,rawOutput:o,resourceLinks:extractResourceLinksFromRawResult(o),timestamp:r});this.sendTurnUpdate(e,l)&&(e.assistantMq?.push(l),this.onStreamDelta?.(e,l));continue}if("text"===a&&n.text){if(e.hasAgentAssistantStream||e.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(n.text);if(!s)continue;const o=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(o))continue;e.chatReplayDedupKeys.add(o);const a=e.chatAssistantTextSoFar??"";let i;if(e.chatAssistantTextSoFar=s,s.startsWith(a))i=s.slice(a.length);else{if(a.startsWith(s))continue;i=s}if(!i)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${i.length} prevLen=${a.length}`);const l={sessionUpdate:"agent_message_chunk",content:{type:"text",text:i},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,l),e.assistantMq?.push(l),this.onStreamDelta?.(e,l)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(this.maybeCompletePromptFromChatState(e,t),!e.promptDone)if("final"===t.state)this.completePrompt(e,t.stopReason||"end_turn");else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.completePrompt(e,"cancelled");else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message;this.failPrompt(e,Errors.AgentError.rpcCode,formatFriendlyError(s))}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;this.adoptBackgroundRun(e,t.runId)}e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),o=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t),this.onStreamDelta?.(e,t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(e,t.data??{});if(n){const e=normalizeTransportText(n);e&&(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${e.length} source=${t.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:e},...s?{timestamp:s}:{}}))}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const n=asString(t.type);if("thinking"===n){e.hasAgentThinkingStream=!0;const o=normalizeText(t.thinking)??normalizeText(t.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}continue}if("toolCall"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);o(buildToolCallPayload({toolCallId:n,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const n=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&o(buildToolCallUpdatePayload({toolCallId:n,toolName:a,outputText:i,rawOutput:r,resourceLinks:extractResourceLinksFromRawResult(r),timestamp:s}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const o=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(o){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&(e.assistantMq?.push(t),this.onStreamDelta?.(e,t))}}else if("tool"===t.stream){const n=t.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",r=resolveToolArgs(n);o(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)});else{const t=n?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(n?.name)??asString(n?.toolName)??"tool",r={...n,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);o(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,o="string"==typeof s?.phase?s.phase:void 0;if("end"===o)this.completePrompt(e,"end_turn");else if("cancelled"===o||"cancel"===o)this.completePrompt(e,"cancelled");else if("error"===o){const o=s?.error,n=o&&"object"==typeof o?o:void 0,a=("string"==typeof s?.message?s.message:void 0)??("string"==typeof o?o:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error",r=formatFriendlyError(a);this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${a} data=${JSON.stringify(s)}`),this.failPrompt(e,Errors.AgentError.rpcCode,r)}}}async handleToolResult(t,e,s){const o=e.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${e.runId}_${e.seq}`,i=("string"==typeof n?.name?n.name:void 0)??("string"==typeof o?.name?o.name:void 0)??"tool",l="string"==typeof o?.text?o.text:void 0,d=a??r,c=o?.result;let u;try{u=await resolveToolResultBlocks({toolCallId:d,toolName:i,rawText:l,rawResult:c})}catch{u=l?[{type:"content",content:{type:"text",text:l}}]:[]}0===u.length&&l&&(u=[{type:"content",content:{type:"text",text:l}}]);const m=[],p=[];for(const t of u)t.content&&("resource_link"===t.content.type&&t.content.uri?p.push({uri:t.content.uri,name:t.content.name??"file",...t.content.mimeType?{mimeType:t.content.mimeType}:{}}):t.content.text&&m.push(t.content.text));const h=buildToolCallUpdatePayload({toolCallId:r,toolName:i,outputText:m.join("\n"),rawOutput:c,resourceLinks:p,timestamp:s});this.sendTurnUpdate(t,h)&&(t.assistantMq?.push(h),this.onStreamDelta?.(t,h))}adoptBackgroundRun(t,e){this.logger.debug?.(`[gateway-events] Adopting background run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`),t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear()}handleCronEvent(t){if(this.transport.sendNotification("_beeos/cron_event",{payload:t}),this.config.agentGatewayUrl&&this.agentFetchFn){const e=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:t})}).catch(t=>{this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)})}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{stripTransportMetadata}from"../message-filter.js";function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function inferToolKindFromName(t){if(!t)return;const e=t.toLowerCase();return e.includes("read")?"read":e.includes("write")||e.includes("edit")?"edit":e.includes("delete")||e.includes("remove")?"delete":e.includes("move")||e.includes("rename")?"move":e.includes("search")||e.includes("find")?"search":e.includes("exec")||e.includes("run")||e.includes("bash")?"execute":e.includes("fetch")||e.includes("http")?"fetch":"other"}const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(t){const e=BEEOS_USER_MSG_MARKER_RE.exec(t);if(e){return t.slice(e.index+e[0].length).trim()}let o=t;return o=o.replace(CANVAS_STATE_RE,"").trimStart(),o=o.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(o).trim()}export async function loadHistory(t,e,o=100){if(!t?.isConnected)return[];try{const n=parseGatewayHistory(await t.chatHistory(e,o));if(n.length>0){n.filter(t=>"user"===t.role).length,n.filter(t=>"assistant"===t.role).length}return n}catch(t){return[]}}export function groupIntoTurns(t){const e=[];let o=null;const n=()=>{o&&(e.push(o),o=null)};for(const s of t){if("assistant"===s.role&&(s.reasoning||s.toolCalls?.length)){n(),o={role:"assistant",content:s.content,reasoning:s.reasoning,files:s.files,sourceMessages:[s],...s.timestamp?{timestamp:s.timestamp}:{}},s.toolCalls?.length&&(o.toolCalls=s.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"in_progress",output:t.output,isError:"error"===t.status||"failed"===t.status,kind:inferToolKindFromName(t.name)})));continue}if("system"===s.role){n();const t={role:"system",content:s.content};s.metadata&&(t.metadata=s.metadata),s.timestamp&&(t.timestamp=s.timestamp),e.push(t);continue}if("user"===s.role){n();const t={role:"user",content:s.content};s.provenance&&(t.provenance=s.provenance),s.metadata&&(t.metadata=s.metadata),s.timestamp&&(t.timestamp=s.timestamp),e.push(t);continue}o||(o={role:"assistant",content:"",sourceMessages:[]}),o.sourceMessages||(o.sourceMessages=[]),o.sourceMessages.push(s),!o.timestamp&&s.timestamp&&(o.timestamp=s.timestamp);switch(s.updateType??"agent_message_chunk"){case"agent_thought_chunk":o.reasoning=(o.reasoning??"")+s.content;break;case"tool_call":o.toolCalls||(o.toolCalls=[]),o.toolCalls.push({id:s.toolCallId??`tc_${o.toolCalls.length}`,callId:s.toolCallId??`tc_${o.toolCalls.length}`,name:s.toolName??"tool",arguments:s.content||"",status:"in_progress",kind:inferToolKindFromName(s.toolName)});break;case"tool_call_update":{o.toolCalls||(o.toolCalls=[]);const t=s.toolCallId?o.toolCalls.find(t=>t.id===s.toolCallId):void 0;t?(t.output=s.content,t.status="completed"):o.toolCalls.push({id:s.toolCallId??`tc_${o.toolCalls.length}`,callId:s.toolCallId??`tc_${o.toolCalls.length}`,name:s.toolName??"tool",arguments:"",status:"completed",output:s.content,kind:inferToolKindFromName(s.toolName)});break}default:o.content+=s.content,s.files?.length&&(o.files=[...o.files??[],...s.files])}}return n(),e}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let e="";for(const o of t)"string"!=typeof o?o&&"object"==typeof o&&"string"==typeof o.text&&(e+=o.text):e+=o;return e}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const e=t.messages;if(!Array.isArray(e))return[];const o=[];let n=null;for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,s="string"==typeof e.role?e.role:"assistant";if("toolResult"===s||"tool"===s){const t=String(e.toolCallId??e.tool_call_id??e.toolUseId??e.tool_use_id??""),s=extractBlockText(e.content),i=!0===e.is_error||!0===e.isError;if(n?.toolCalls?.length){const e=(t?n.toolCalls.find(e=>e.id===t):void 0)??n.toolCalls.find(t=>void 0===t.output);e&&(e.output=s,e.status=i?"failed":"completed")}else o.push({role:"assistant",content:s,updateType:"tool_call_update",toolCallId:t||void 0});continue}if("user"===s&&Array.isArray(e.content)){let t=!1,s=!1;for(const o of e.content){if(!o||"object"!=typeof o)continue;const e=o,n="string"==typeof e.type?e.type:"";TOOL_RESULT_BLOCK_TYPES.has(n)?t=!0:"text"===n&&"string"==typeof e.text&&e.text.trim()&&(s=!0)}if(t&&!s){for(const t of e.content){if(!t||"object"!=typeof t)continue;const e=t,s="string"==typeof e.type?e.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(s))continue;const i=String(e.toolUseId??e.tool_use_id??e.toolCallId??e.tool_call_id??""),l="string"==typeof e.output?e.output:extractBlockText(e.content),a=!0===e.is_error||!0===e.isError;if(n?.toolCalls?.length){const t=(i?n.toolCalls.find(t=>t.id===i):void 0)??n.toolCalls.find(t=>void 0===t.output);t&&(t.output=l,t.status=a?"failed":"completed")}else o.push({role:"assistant",content:l,updateType:"tool_call_update",toolCallId:i||void 0})}continue}}const i="user"===s?"user":"assistant";let l,a,r,c,u,p="";if("string"==typeof e.content)p=e.content;else if(Array.isArray(e.content)){if("assistant"===i&&e.content.some(t=>{if(!t||"object"!=typeof t)return!1;const e="string"==typeof t.type?t.type:"";return TOOL_CALL_BLOCK_TYPES.has(e)||TOOL_RESULT_BLOCK_TYPES.has(e)})&&"assistant"===i){const t=new Map;for(const n of e.content){if("string"==typeof n){n&&o.push({role:"assistant",content:n,updateType:"agent_message_chunk"});continue}if(!n||"object"!=typeof n)continue;const e=n,s="string"==typeof e.type?e.type:"";if("text"===s&&"string"==typeof e.text&&e.text)o.push({role:"assistant",content:e.text,updateType:"agent_message_chunk"});else if("thinking"===s&&"string"==typeof e.thinking)o.push({role:"assistant",content:e.thinking,updateType:"agent_thought_chunk"});else if("resource_link"===s&&"string"==typeof e.uri&&e.uri)r||(r=[]),r.push({uri:e.uri,..."string"==typeof e.name&&e.name?{name:e.name}:{},..."string"==typeof e.mimeType&&e.mimeType?{mimeType:e.mimeType}:{}});else if(TOOL_CALL_BLOCK_TYPES.has(s)){const n=e.arguments??e.input,s="string"==typeof n?n:null!=n?JSON.stringify(n):"",i=String(e.id??e.toolCallId??e.tool_call_id??""),l={role:"assistant",content:s,updateType:"tool_call",toolCallId:i,toolName:String(e.name??"tool")};o.push(l),i&&t.set(i,l)}else if(TOOL_RESULT_BLOCK_TYPES.has(s)){const n=String(e.toolUseId??e.tool_use_id??e.toolCallId??e.tool_call_id??""),s="string"==typeof e.output?e.output:extractBlockText(e.content),i=!0===e.is_error||!0===e.isError;if(o.push({role:"assistant",content:s,updateType:"tool_call_update",toolCallId:n||void 0,toolName:t.get(n)?.toolName}),n&&t.has(n)){const e=t.get(n);e.toolCalls||(e.toolCalls=[{id:n,name:e.toolName??"tool",arguments:e.content,status:i?"failed":"completed",output:s}])}}}r?.length&&o.push({role:"assistant",content:"",files:r}),"string"==typeof e.reasoning&&e.reasoning?o.push({role:"assistant",content:e.reasoning,updateType:"agent_thought_chunk"}):"string"==typeof e.thinking&&e.thinking&&o.push({role:"assistant",content:e.thinking,updateType:"agent_thought_chunk"}),o.length>0&&"assistant"===o[o.length-1].role&&(n=o[o.length-1]);continue}for(const t of e.content){if("string"==typeof t){p+=t;continue}if(!t||"object"!=typeof t)continue;const e=t,o="string"==typeof e.type?e.type:"";"text"===o&&"string"==typeof e.text?p+=e.text:"thinking"===o&&"string"==typeof e.thinking?a=(a??"")+e.thinking:"resource_link"===o&&"string"==typeof e.uri&&e.uri&&(r||(r=[]),r.push({uri:e.uri,..."string"==typeof e.name&&e.name?{name:e.name}:{},..."string"==typeof e.mimeType&&e.mimeType?{mimeType:e.mimeType}:{}}))}}if(a||("string"==typeof e.reasoning&&e.reasoning?a=e.reasoning:"string"==typeof e.thinking&&e.thinking&&(a=e.thinking)),!l?.length){const t=e.tool_calls??e.toolCalls;if(Array.isArray(t)){l=[];for(const e of t){if(!e||"object"!=typeof e)continue;const t=e,o=t.function;l.push({id:String(t.id??t.toolCallId??""),name:String(o?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof o?.arguments?o.arguments:"",status:String(t.status??"in_progress"),output:"string"==typeof t.output?t.output:void 0})}0===l.length&&(l=void 0)}}if("user"===i&&p){p=stripBeeosUserEnvelope(p);const t=CANVAS_INTERACTION_RE.exec(p);if(t){const e=t[1];c={kind:"canvas_action"};const o={type:"canvas_action",actionName:e,surfaceId:t[2]},n=ACTION_CONTEXT_RE.exec(p);if(n)try{o.actionPayload=JSON.parse(n[1])}catch{}u=o,p=`Clicked "${e}"`}}if(!(p||a||l?.length||r?.length))continue;if(!c&&"user"===i&&e.provenance&&"object"==typeof e.provenance){const t=e.provenance;"string"==typeof t.kind&&(c={kind:t.kind},"string"==typeof t.sourceTool&&(c.sourceTool=t.sourceTool))}const f=toISO("number"==typeof e.timestamp?e.timestamp:"number"==typeof e.message?.timestamp?e.message.timestamp:void 0),m={role:i,content:p,reasoning:a,toolCalls:l,provenance:c,metadata:u,files:r,...f?{timestamp:f}:{}};o.push(m),"assistant"===i&&(n=m)}o.filter(t=>t.toolCalls?.length),o.reduce((t,e)=>t+(e.toolCalls?.length??0),0),o.reduce((t,e)=>t+(e.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return o}const BEEOS_FILE_URI_RE=/beeos-file:\/\/([a-zA-Z0-9_-]+)/g;export function extractBeeosFileUrisFromOutput(t){if(!t)return[];const e=[];for(const o of t.matchAll(BEEOS_FILE_URI_RE))e.push(o[0]);return[...new Set(e)]}const BEEOS_REF_RE=/<BEEOS_REF\s+((?:[^"]|"[^"]*")*?)\s*\/>/g;function parseBeeosRefAttrs(t){const e=new Map,o=/(\w+)="([^"]*)"/g;for(const n of t.matchAll(o))e.set(n[1],n[2]);return e}function guessMimeFromName(t){if(!t)return;const e=t.split(".").pop()?.toLowerCase();return e?{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf",mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",txt:"text/plain",json:"application/json",csv:"text/csv"}[e]:void 0}export function splitBeeosRefReplayText(t){if(!t)return[];if(!t.includes("<BEEOS_REF "))return[{type:"text",text:t}];const e=[];let o=0;for(const n of t.matchAll(BEEOS_REF_RE)){const s=t.slice(o,n.index).trim();s&&e.push({type:"text",text:s});const i=parseBeeosRefAttrs(n[1]),l=i.get("id"),a=i.get("name"),r=i.get("status");l&&"download_failed"!==r?e.push({type:"resource_link",uri:`beeos-file://${l}`,fileName:a,mimeType:guessMimeFromName(a)??"application/octet-stream"}):l&&e.push({type:"resource_link",uri:`beeos-file://${l}`,fileName:a??"file",mimeType:guessMimeFromName(a)??"application/octet-stream"}),o=(n.index??0)+n[0].length}const n=t.slice(o).trim();return n&&e.push({type:"text",text:n}),e}const ATTACHMENT_MARKER_RE=/\[(image|file)\s+([^\]]*)\]/g;export function splitUserAttachmentMarkers(t){if(!t)return[];if(!t.includes("[image ")&&!t.includes("[file "))return[{type:"text",text:t}];const e=[];let o=0;for(const n of t.matchAll(ATTACHMENT_MARKER_RE)){const s=t.slice(o,n.index).trim();s&&e.push({type:"text",text:s});const i=n[1],l=n[2],a=new Map;for(const t of l.split(/\s+/)){const e=t.indexOf("=");e>0&&a.set(t.slice(0,e),t.slice(e+1))}e.push({type:i,mimeType:a.get("mime"),fileName:a.get("name")||("image"===i?"Image":"File")}),o=(n.index??0)+n[0].length}const n=t.slice(o).trim();return n&&e.push({type:"text",text:n}),e}export function splitUserReplayBlocks(t){return t?t.includes("<BEEOS_REF ")?splitBeeosRefReplayText(t):t.includes("[image ")||t.includes("[file ")?splitUserAttachmentMarkers(t):[{type:"text",text:t}]:[]}
|
|
1
|
+
import{stripTransportMetadata}from"../message-filter.js";function toISO(t){if(null!=t){if("string"==typeof t){if(!t)return;const e=new Date(t),o=e.getTime();return Number.isFinite(o)?e.toISOString():void 0}if(!(t<=0))try{return new Date(t).toISOString()}catch{return}}}function inferToolKindFromName(t){if(!t)return;const e=t.toLowerCase();return e.includes("read")?"read":e.includes("write")||e.includes("edit")?"edit":e.includes("delete")||e.includes("remove")?"delete":e.includes("move")||e.includes("rename")?"move":e.includes("search")||e.includes("find")?"search":e.includes("exec")||e.includes("run")||e.includes("bash")?"execute":e.includes("fetch")||e.includes("http")?"fetch":"other"}const BEEOS_USER_MSG_MARKER_RE=/(?:^|\n)\s*User Message From \w+:\s*\n/i,CANVAS_STATE_RE=/^\[Canvas State\][^\n]*/,SYSTEM_NOTE_RE=/^\[System Note\][^\n]*/,CANVAS_INTERACTION_RE=/^\[Canvas Interaction\] The user clicked "([^"]*)" on surface "([^"]*)"/,ACTION_CONTEXT_RE=/\nAction context: (.+?)(?:\n|$)/s;function stripBeeosUserEnvelope(t){const e=BEEOS_USER_MSG_MARKER_RE.exec(t);if(e){return t.slice(e.index+e[0].length).trim()}let o=t;return o=o.replace(CANVAS_STATE_RE,"").trimStart(),o=o.replace(SYSTEM_NOTE_RE,"").trimStart(),stripTransportMetadata(o).trim()}export async function loadHistory(t,e,o=100){if(!t?.isConnected)return[];try{const s=parseGatewayHistory(await t.chatHistory(e,o));if(s.length>0){s.filter(t=>"user"===t.role).length,s.filter(t=>"assistant"===t.role).length}return s}catch(t){return[]}}export function groupIntoTurns(t){const e=[];let o=null;const s=()=>{o&&(e.push(o),o=null)};for(const n of t){if("assistant"===n.role&&(n.reasoning||n.toolCalls?.length)){s(),o={role:"assistant",content:n.content,reasoning:n.reasoning,files:n.files,sourceMessages:[n],...n.timestamp?{timestamp:n.timestamp}:{},...n.messageId?{messageId:n.messageId}:{},...void 0!==n._seq?{_seq:n._seq}:{}},n.toolCalls?.length&&(o.toolCalls=n.toolCalls.map(t=>({id:t.id,callId:t.id,name:t.name,arguments:t.arguments??"",status:t.status??"in_progress",output:t.output,isError:"error"===t.status||"failed"===t.status,kind:inferToolKindFromName(t.name)})));continue}if("system"===n.role){s();const t={role:"system",content:n.content};n.metadata&&(t.metadata=n.metadata),n.timestamp&&(t.timestamp=n.timestamp),n.messageId&&(t.messageId=n.messageId),void 0!==n._seq&&(t._seq=n._seq),e.push(t);continue}if("user"===n.role){s();const t={role:"user",content:n.content};n.provenance&&(t.provenance=n.provenance),n.metadata&&(t.metadata=n.metadata),n.timestamp&&(t.timestamp=n.timestamp),n.messageId&&(t.messageId=n.messageId),void 0!==n._seq&&(t._seq=n._seq),e.push(t);continue}o||(o={role:"assistant",content:"",sourceMessages:[]}),o.sourceMessages||(o.sourceMessages=[]),o.sourceMessages.push(n),!o.timestamp&&n.timestamp&&(o.timestamp=n.timestamp),!o.messageId&&n.messageId&&(o.messageId=n.messageId),void 0===o._seq&&void 0!==n._seq&&(o._seq=n._seq);switch(n.updateType??"agent_message_chunk"){case"agent_thought_chunk":o.reasoning=(o.reasoning??"")+n.content;break;case"tool_call":o.toolCalls||(o.toolCalls=[]),o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:n.content||"",status:"in_progress",kind:inferToolKindFromName(n.toolName)});break;case"tool_call_update":{o.toolCalls||(o.toolCalls=[]);const t=n.toolCallId?o.toolCalls.find(t=>t.id===n.toolCallId):void 0;t?(t.output=n.content,t.status="completed"):o.toolCalls.push({id:n.toolCallId??`tc_${o.toolCalls.length}`,callId:n.toolCallId??`tc_${o.toolCalls.length}`,name:n.toolName??"tool",arguments:"",status:"completed",output:n.content,kind:inferToolKindFromName(n.toolName)});break}default:o.content+=n.content,n.files?.length&&(o.files=[...o.files??[],...n.files])}}return s(),e}const TOOL_CALL_BLOCK_TYPES=new Set(["toolCall","toolUse","functionCall","tool_use","tool_call","toolcall"]),TOOL_RESULT_BLOCK_TYPES=new Set(["tool_result","tool_result_error","toolResult"]);function readOpenClawMeta(t){const e=t.__openclaw;if(!e||"object"!=typeof e)return{};const o={},s=e.id,n=e.seq;return"string"==typeof s&&s.length>0&&(o.id=s),"number"==typeof n&&Number.isFinite(n)&&(o.seq=n),o}function extractBlockText(t){if("string"==typeof t)return t;if(!Array.isArray(t))return"";let e="";for(const o of t)"string"!=typeof o?o&&"object"==typeof o&&"string"==typeof o.text&&(e+=o.text):e+=o;return e}function parseGatewayHistory(t){if(!t||"object"!=typeof t)return[];const e=t.messages;if(!Array.isArray(e))return[];const o=[];let s=null;for(const t of e){if(!t||"object"!=typeof t)continue;const e=t,n="string"==typeof e.role?e.role:"assistant",i=readOpenClawMeta(e),a=i.id,l=i.seq,r=e.message?.timestamp,c=toISO("number"==typeof e.timestamp||"string"==typeof e.timestamp?e.timestamp:"number"==typeof r||"string"==typeof r?r:void 0),u=t=>(a&&(t.messageId=a),void 0!==l&&(t._seq=l),c&&!t.timestamp&&(t.timestamp=c),t);if("toolResult"===n||"tool"===n){const t=String(e.toolCallId??e.tool_call_id??e.toolUseId??e.tool_use_id??""),n=extractBlockText(e.content),i=!0===e.is_error||!0===e.isError;if(s?.toolCalls?.length){const e=(t?s.toolCalls.find(e=>e.id===t):void 0)??s.toolCalls.find(t=>void 0===t.output);e&&(e.output=n,e.status=i?"failed":"completed")}else o.push(u({role:"assistant",content:n,updateType:"tool_call_update",toolCallId:t||void 0}));continue}if("user"===n&&Array.isArray(e.content)){let t=!1,n=!1;for(const o of e.content){if(!o||"object"!=typeof o)continue;const e=o,s="string"==typeof e.type?e.type:"";TOOL_RESULT_BLOCK_TYPES.has(s)?t=!0:"text"===s&&"string"==typeof e.text&&e.text.trim()&&(n=!0)}if(t&&!n){for(const t of e.content){if(!t||"object"!=typeof t)continue;const e=t,n="string"==typeof e.type?e.type:"";if(!TOOL_RESULT_BLOCK_TYPES.has(n))continue;const i=String(e.toolUseId??e.tool_use_id??e.toolCallId??e.tool_call_id??""),a="string"==typeof e.output?e.output:extractBlockText(e.content),l=!0===e.is_error||!0===e.isError;if(s?.toolCalls?.length){const t=(i?s.toolCalls.find(t=>t.id===i):void 0)??s.toolCalls.find(t=>void 0===t.output);t&&(t.output=a,t.status=l?"failed":"completed")}else o.push(u({role:"assistant",content:a,updateType:"tool_call_update",toolCallId:i||void 0}))}continue}}const p="user"===n?"user":"assistant";let m,f,g,d,_,y="";if("string"==typeof e.content)y=e.content;else if(Array.isArray(e.content)){if("assistant"===p&&e.content.some(t=>{if(!t||"object"!=typeof t)return!1;const e="string"==typeof t.type?t.type:"";return TOOL_CALL_BLOCK_TYPES.has(e)||TOOL_RESULT_BLOCK_TYPES.has(e)})&&"assistant"===p){const t=new Map;for(const s of e.content){if("string"==typeof s){s&&o.push(u({role:"assistant",content:s,updateType:"agent_message_chunk"}));continue}if(!s||"object"!=typeof s)continue;const e=s,n="string"==typeof e.type?e.type:"";if("text"===n&&"string"==typeof e.text&&e.text)o.push(u({role:"assistant",content:e.text,updateType:"agent_message_chunk"}));else if("thinking"===n&&"string"==typeof e.thinking)o.push(u({role:"assistant",content:e.thinking,updateType:"agent_thought_chunk"}));else if("resource_link"===n&&"string"==typeof e.uri&&e.uri)g||(g=[]),g.push({uri:e.uri,..."string"==typeof e.name&&e.name?{name:e.name}:{},..."string"==typeof e.mimeType&&e.mimeType?{mimeType:e.mimeType}:{}});else if(TOOL_CALL_BLOCK_TYPES.has(n)){const s=e.arguments??e.input,n="string"==typeof s?s:null!=s?JSON.stringify(s):"",i=String(e.id??e.toolCallId??e.tool_call_id??""),a=u({role:"assistant",content:n,updateType:"tool_call",toolCallId:i,toolName:String(e.name??"tool")});o.push(a),i&&t.set(i,a)}else if(TOOL_RESULT_BLOCK_TYPES.has(n)){const s=String(e.toolUseId??e.tool_use_id??e.toolCallId??e.tool_call_id??""),n="string"==typeof e.output?e.output:extractBlockText(e.content),i=!0===e.is_error||!0===e.isError;if(o.push(u({role:"assistant",content:n,updateType:"tool_call_update",toolCallId:s||void 0,toolName:t.get(s)?.toolName})),s&&t.has(s)){const e=t.get(s);e.toolCalls||(e.toolCalls=[{id:s,name:e.toolName??"tool",arguments:e.content,status:i?"failed":"completed",output:n}])}}}g?.length&&o.push(u({role:"assistant",content:"",files:g})),"string"==typeof e.reasoning&&e.reasoning?o.push(u({role:"assistant",content:e.reasoning,updateType:"agent_thought_chunk"})):"string"==typeof e.thinking&&e.thinking&&o.push(u({role:"assistant",content:e.thinking,updateType:"agent_thought_chunk"})),o.length>0&&"assistant"===o[o.length-1].role&&(s=o[o.length-1]);continue}for(const t of e.content){if("string"==typeof t){y+=t;continue}if(!t||"object"!=typeof t)continue;const e=t,o="string"==typeof e.type?e.type:"";"text"===o&&"string"==typeof e.text?y+=e.text:"thinking"===o&&"string"==typeof e.thinking?f=(f??"")+e.thinking:"resource_link"===o&&"string"==typeof e.uri&&e.uri&&(g||(g=[]),g.push({uri:e.uri,..."string"==typeof e.name&&e.name?{name:e.name}:{},..."string"==typeof e.mimeType&&e.mimeType?{mimeType:e.mimeType}:{}}))}}if(f||("string"==typeof e.reasoning&&e.reasoning?f=e.reasoning:"string"==typeof e.thinking&&e.thinking&&(f=e.thinking)),!m?.length){const t=e.tool_calls??e.toolCalls;if(Array.isArray(t)){m=[];for(const e of t){if(!e||"object"!=typeof e)continue;const t=e,o=t.function;m.push({id:String(t.id??t.toolCallId??""),name:String(o?.name??t.name??t.toolName??"tool"),arguments:"string"==typeof t.arguments?t.arguments:"string"==typeof o?.arguments?o.arguments:"",status:String(t.status??"in_progress"),output:"string"==typeof t.output?t.output:void 0})}0===m.length&&(m=void 0)}}if("user"===p&&y){y=stripBeeosUserEnvelope(y);const t=CANVAS_INTERACTION_RE.exec(y);if(t){const e=t[1];d={kind:"canvas_action"};const o={type:"canvas_action",actionName:e,surfaceId:t[2]},s=ACTION_CONTEXT_RE.exec(y);if(s)try{o.actionPayload=JSON.parse(s[1])}catch{}_=o,y=`Clicked "${e}"`}}if(!(y||f||m?.length||g?.length))continue;if(!d&&"user"===p&&e.provenance&&"object"==typeof e.provenance){const t=e.provenance;"string"==typeof t.kind&&(d={kind:t.kind},"string"==typeof t.sourceTool&&(d.sourceTool=t.sourceTool))}const h=u({role:p,content:y,reasoning:f,toolCalls:m,provenance:d,metadata:_,files:g});o.push(h),"assistant"===p&&(s=h)}o.filter(t=>t.toolCalls?.length),o.reduce((t,e)=>t+(e.toolCalls?.length??0),0),o.reduce((t,e)=>t+(e.toolCalls?.filter(t=>void 0!==t.output).length??0),0);return o}const BEEOS_FILE_URI_RE=/beeos-file:\/\/([a-zA-Z0-9_-]+)/g;export function extractBeeosFileUrisFromOutput(t){if(!t)return[];const e=[];for(const o of t.matchAll(BEEOS_FILE_URI_RE))e.push(o[0]);return[...new Set(e)]}const BEEOS_REF_RE=/<BEEOS_REF\s+((?:[^"]|"[^"]*")*?)\s*\/>/g;function parseBeeosRefAttrs(t){const e=new Map,o=/(\w+)="([^"]*)"/g;for(const s of t.matchAll(o))e.set(s[1],s[2]);return e}function guessMimeFromName(t){if(!t)return;const e=t.split(".").pop()?.toLowerCase();return e?{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",gif:"image/gif",webp:"image/webp",svg:"image/svg+xml",pdf:"application/pdf",mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",txt:"text/plain",json:"application/json",csv:"text/csv"}[e]:void 0}export function splitBeeosRefReplayText(t){if(!t)return[];if(!t.includes("<BEEOS_REF "))return[{type:"text",text:t}];const e=[];let o=0;for(const s of t.matchAll(BEEOS_REF_RE)){const n=t.slice(o,s.index).trim();n&&e.push({type:"text",text:n});const i=parseBeeosRefAttrs(s[1]),a=i.get("id"),l=i.get("name"),r=i.get("status");a&&"download_failed"!==r?e.push({type:"resource_link",uri:`beeos-file://${a}`,fileName:l,mimeType:guessMimeFromName(l)??"application/octet-stream"}):a&&e.push({type:"resource_link",uri:`beeos-file://${a}`,fileName:l??"file",mimeType:guessMimeFromName(l)??"application/octet-stream"}),o=(s.index??0)+s[0].length}const s=t.slice(o).trim();return s&&e.push({type:"text",text:s}),e}const ATTACHMENT_MARKER_RE=/\[(image|file)\s+([^\]]*)\]/g;export function splitUserAttachmentMarkers(t){if(!t)return[];if(!t.includes("[image ")&&!t.includes("[file "))return[{type:"text",text:t}];const e=[];let o=0;for(const s of t.matchAll(ATTACHMENT_MARKER_RE)){const n=t.slice(o,s.index).trim();n&&e.push({type:"text",text:n});const i=s[1],a=s[2],l=new Map;for(const t of a.split(/\s+/)){const e=t.indexOf("=");e>0&&l.set(t.slice(0,e),t.slice(e+1))}e.push({type:i,mimeType:l.get("mime"),fileName:l.get("name")||("image"===i?"Image":"File")}),o=(s.index??0)+s[0].length}const s=t.slice(o).trim();return s&&e.push({type:"text",text:s}),e}export function splitUserReplayBlocks(t){return t?t.includes("<BEEOS_REF ")?splitBeeosRefReplayText(t):t.includes("[image ")||t.includes("[file ")?splitUserAttachmentMarkers(t):[{type:"text",text:t}]:[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect}from"vitest";import{groupIntoTurns}from"./history-replay.js";describe("groupIntoTurns — _seq propagation",()=>{it("copies _seq onto user turns",()=>{const e=groupIntoTurns([{role:"user",content:"hi",messageId:"u1",_seq:1,timestamp:"2024-01-01T00:00:00.000Z"}]);expect(e).toHaveLength(1),expect(e[0]._seq).toBe(1),expect(e[0].messageId).toBe("u1")}),it("copies _seq onto system turns",()=>{const e=groupIntoTurns([{role:"system",content:"sys",messageId:"s1",_seq:2}]);expect(e[0]._seq).toBe(2)}),it("assistant turn with reasoning uses its own _seq and keeps sourceMessage intact",()=>{const e=groupIntoTurns([{role:"user",content:"q",messageId:"u1",_seq:1},{role:"assistant",content:"thinking done",reasoning:"step-by-step",messageId:"a1",_seq:2}]);expect(e).toHaveLength(2);const s=e[1];expect(s._seq).toBe(2),expect(s.messageId).toBe("a1"),expect(s.sourceMessages).toHaveLength(1),expect(s.sourceMessages[0]._seq).toBe(2)}),it("assistant turn merged from multiple splits inherits first sourceMessage's _seq",()=>{const e=groupIntoTurns([{role:"assistant",updateType:"agent_message_chunk",content:"hello ",messageId:"a1",_seq:5},{role:"assistant",updateType:"agent_message_chunk",content:"world",messageId:"a1",_seq:5},{role:"assistant",updateType:"tool_call",content:"{}",toolCallId:"T1",toolName:"search",messageId:"a1",_seq:5}]);expect(e).toHaveLength(1);const s=e[0];expect(s._seq).toBe(5),expect(s.messageId).toBe("a1"),expect(s.sourceMessages).toHaveLength(3);for(const e of s.sourceMessages)expect(e._seq).toBe(5)}),it("omits _seq when upstream did not provide it",()=>{const e=groupIntoTurns([{role:"user",content:"hi",messageId:"u1"}]);expect(e[0]._seq).toBeUndefined()}),it("successive assistant turns with different _seq do not cross-contaminate",()=>{const e=groupIntoTurns([{role:"assistant",reasoning:"r1",content:"first",messageId:"a1",_seq:10},{role:"assistant",reasoning:"r2",content:"second",messageId:"a2",_seq:11}]);expect(e).toHaveLength(2),expect(e[0]._seq).toBe(10),expect(e[1]._seq).toBe(11)})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{acpError}from"../errors/index.js";export class AcpTransport{debugIndex=0;sendBridgeMessage;obs;forward;sessionUpdateCounts=new Map;_isReady;constructor(e,t,
|
|
1
|
+
import{acpError}from"../errors/index.js";function normalizeOpts(e){if(null!=e)return"string"==typeof e?{turnId:e}:e}export class AcpTransport{debugIndex=0;sendBridgeMessage;obs;forward;sessionUpdateCounts=new Map;_isReady;sessionUpdateObserver;constructor(e,s,t,r){this.sendBridgeMessage=e,this.obs=s??(()=>{}),this.forward=t??{forwardThinking:!0,forwardToolCalls:!0},this._isReady=r}setSessionUpdateObserver(e){this.sessionUpdateObserver=e}isReady(){return!this._isReady||this._isReady()}get forwardControl(){return this.forward}nextMeta(){return{_debug_index:this.debugIndex++,_ts:Date.now()}}sendResult(e,s){const t=this.nextMeta();this.detectProtocolError(e,s,t._debug_index);const r={jsonrpc:"2.0",id:e,result:this.attachMeta(s,t),_debug_index:t._debug_index,_ts:t._ts};this.sendBridgeMessage(r),this.obs({component:"acp-transport",domain:"reply",name:"rpc.result",severity:"debug",payload:{id:e,_debug_index:t._debug_index}})}sendError(e,s,t,r){const n=this.nextMeta(),o={code:s,message:t};o.data=void 0!==r?this.attachMeta(r,n):{_meta:n};const d={jsonrpc:"2.0",id:e,error:o,_debug_index:n._debug_index,_ts:n._ts};this.sendBridgeMessage(d),this.obs({component:"acp-transport",domain:"reply",name:"rpc.error",severity:"warn",payload:{id:e,code:s,message:t,_debug_index:n._debug_index}})}sendAcpError(e,s,t){const r=acpError(s,t);this.sendError(e,r.code,r.message,r.data)}sendNotification(e,s){const t=this.nextMeta(),r={jsonrpc:"2.0",method:e,params:s,_debug_index:t._debug_index,_ts:t._ts};this.sendBridgeMessage(r)}enrich(e,s){if(!s||!s.turnId&&!s.messageId&&void 0===s.seq)return e;const t={...e};return s.turnId&&(t._turnId=s.turnId),s.messageId&&(t.messageId=s.messageId),void 0!==s.seq&&(t._seq=s.seq),t}sendSessionUpdate(e,s,t){const r=normalizeOpts(t),n=s,o=n?.sessionUpdate;if("agent_thought_chunk"===o&&!this.forward.forwardThinking)return!1;if(("tool_call"===o||"tool_call_update"===o)&&!this.forward.forwardToolCalls)return!1;const d=(this.sessionUpdateCounts.get(e)??0)+1;this.sessionUpdateCounts.set(e,d),d%50==0&&this.obs({component:"acp-transport",domain:"session-update",name:"send.rate",severity:"debug",payload:{sessionId:e,totalSent:d,lastType:o,_debug_index:this.debugIndex}}),this.sendNotification("session/update",{sessionId:e,update:this.enrich(s,r)});try{this.sessionUpdateObserver?.(e,s,r)}catch{}return!0}resetSessionUpdateCount(e){this.sessionUpdateCounts.delete(e)}sendCronSessionUpdate(e,s,t){const r={turnId:t.requestId,messageId:t.messageId,seq:t.seq};this.sendNotification("session/update",{sessionId:e,update:this.enrich(s,r),_meta:{...this.nextMeta(),...t}});try{this.sessionUpdateObserver?.(e,s,r)}catch{}return!0}detectProtocolError(e,s,t){if(!s||"object"!=typeof s)return;const r=s,n="error"in r&&null!=r.error,o="ok"in r&&!1===r.ok;(n||o)&&this.obs({component:"acp-transport",domain:"reply",name:"protocol.error_in_result",severity:"warn",payload:{id:e,_debug_index:t,hasError:n,notOk:o,errorSummary:n?"string"==typeof r.error?r.error.slice(0,200):JSON.stringify(r.error).slice(0,200):void 0}})}attachMeta(e,s){return!e||"object"!=typeof e||Array.isArray(e)?e:{...e,_meta:s}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpTransport}from"./transport.js";function makeTransport(){const e=[];return{tx:new AcpTransport(t=>{e.push(t)}),sent:e}}function getUpdate(e){expect(e.length).toBe(1);return e[0].params.update}describe("AcpTransport session/update _seq injection",()=>{let e,t;beforeEach(()=>{const s=makeTransport();e=s.tx,t=s.sent}),it("injects _seq from opts.seq when provided",()=>{e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}},{turnId:"t1",messageId:"m1",seq:42});const s=getUpdate(t);expect(s._seq).toBe(42),expect(s._turnId).toBe("t1"),expect(s.messageId).toBe("m1")}),it("omits _seq when opts.seq is undefined",()=>{e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}},{turnId:"t1",messageId:"m1"});const s=getUpdate(t);expect("_seq"in s).toBe(!1)}),it("preserves seq=0 (not treated as missing)",()=>{e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}},{seq:0});const s=getUpdate(t);expect(s._seq).toBe(0)}),it("does not enrich when opts omits all identifiers",()=>{const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}};e.sendSessionUpdate("s1",s);const n=getUpdate(t);expect(n).toEqual(s),expect("_seq"in n).toBe(!1),expect("_turnId"in n).toBe(!1),expect("messageId"in n).toBe(!1)}),it("multiple splits sharing the same seq all emit _seq (client dedups)",()=>{e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"text"}},{turnId:"t1",messageId:"m1",seq:7}),e.sendSessionUpdate("s1",{sessionUpdate:"tool_call",toolCallId:"T1"},{turnId:"t1",messageId:"m1",seq:7}),e.sendSessionUpdate("s1",{sessionUpdate:"tool_call_update",toolCallId:"T1"},{turnId:"t1",messageId:"m1",seq:7}),expect(t.length).toBe(3);for(const e of t){const t=e.params.update;expect(t._seq).toBe(7),expect(t.messageId).toBe("m1"),expect(t._turnId).toBe("t1")}}),it("forwards seq through sendCronSessionUpdate meta",()=>{e.sendCronSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"cron"}},{requestId:"req-1",messageType:"cron",timestamp:12345,messageId:"m1",seq:9});const s=getUpdate(t);expect(s._seq).toBe(9),expect(s.messageId).toBe("m1"),expect(s._turnId).toBe("req-1")}),it("legacy positional turnId signature still works and omits _seq",()=>{e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}},"t1");const s=getUpdate(t);expect(s._turnId).toBe("t1"),expect("_seq"in s).toBe(!1)}),it("forwardThinking=false still suppresses agent_thought_chunk regardless of seq",()=>{const e=[],t=new AcpTransport(t=>e.push(t),void 0,{forwardThinking:!1,forwardToolCalls:!0}).sendSessionUpdate("s1",{sessionUpdate:"agent_thought_chunk",content:{type:"text",text:"thinking"}},{seq:1});expect(t).toBe(!1),expect(e.length).toBe(0)}),it("observer receives the opts including seq",()=>{const t=vi.fn();e.setSessionUpdateObserver(t),e.sendSessionUpdate("s1",{sessionUpdate:"agent_message_chunk",content:{type:"text",text:"hi"}},{turnId:"t1",messageId:"m1",seq:3}),expect(t).toHaveBeenCalledTimes(1);const s=t.mock.calls[0][2];expect(s).toEqual({turnId:"t1",messageId:"m1",seq:3})})});
|
package/dist/acp-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";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{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.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{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;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}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,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,o;if(s?(r=this.state.findSessionByKey(s),o=s,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(o),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${r.sessionId}`))):(o=`agent:${a}:a2a:${randomUUID()}`,r=this.core.ensureSessionForKey(o),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${o}`)),i&&r&&(r.channelId=i),r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length);const c=await this.canvasOrch.enrichMessageWithCanvasContext(e,o,this.thirdPartyChannels),l=await this.gateway.agentSend(o,c,a,t,n);return l?.runId&&(r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=l.runId,r.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,r)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:o,channelId:i})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}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,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;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,beeosFileResolver:r}),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.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),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,onStreamDelta:(e,t)=>{if(!this.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",i)).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},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)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),this.messageSdk&&this.msgPrincipalId&&n){const e={session_key:n,text:t};(i&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(i,"agent_reply",e):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply",e)).then(()=>{this.logger.info?.(`[acp-server] agent_reply published ok session_key=${n} channel=${i??"personal"}`)}).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))}else n||this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`)},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i}`),this.messageSdk&&this.msgPrincipalId&&n){const e={session_key:n,error:t||"execution failed"};(i&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(i,"agent_reply",e):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply",e)).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))}},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.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},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){const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}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.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,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;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}ms (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)},s=Date.now();try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);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.gateway.on("response",e=>this.core.handleGatewayResponse(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.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),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)??this.core.ensureSessionForKey(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)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}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}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,a=await n(i,"utf-8"),r=JSON.parse(a);if(r.formSchemas?.length){for(const e of r.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${r.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.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";import{randomUUID}from"node:crypto";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{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.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{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;messageSdk=null;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;bridgeInstance=null;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey,t.inputProvenance).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}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,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const a=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,o;if(s?(r=this.state.findSessionByKey(s),o=s,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(o),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${r.sessionId}`))):(o=`agent:${a}:a2a:${randomUUID()}`,r=this.core.ensureSessionForKey(o),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${o}`)),i&&r&&(r.channelId=i),r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length);const c=await this.canvasOrch.enrichMessageWithCanvasContext(e,o,this.thirdPartyChannels),l=await this.gateway.agentSend(o,c,a,t,n);return l?.runId&&(r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=l.runId,r.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,r)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:o,channelId:i})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}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,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const r=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;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,beeosFileResolver:r}),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.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),failPrompt:(e,t,s)=>this.core.failPrompt(e,t,s),maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,onStreamDelta:(e,t)=>{if(!this.messageSdk||!this.msgPrincipalId)return;const s=e.gatewaySessionKey;if(!s)return;const n=e.channelId,i={session_key:s,update:t};(n&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(n,"agent_reply_delta",i):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply_delta",i)).catch(e=>this.logger.debug?.(`[acp-server] agent_reply_delta publish failed: ${e}`))}}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},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)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),this.messageSdk&&this.msgPrincipalId&&n){const e={session_key:n,text:t};(i&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(i,"agent_reply",e):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply",e)).then(()=>{this.logger.info?.(`[acp-server] agent_reply published ok session_key=${n} channel=${i??"personal"}`)}).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))}else n||this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`)},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i}`),this.messageSdk&&this.msgPrincipalId&&n){const e={session_key:n,error:t||"execution failed"};(i&&this.messageSdk.sendToChannel?this.messageSdk.sendToChannel(i,"agent_reply",e):this.messageSdk.sendToPersonal(this.msgPrincipalId,"agent_reply",e)).catch(e=>this.logger.warn?.(`[acp-server] agent_reply error publish failed: ${e instanceof Error?e.message:String(e)}`))}},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.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},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){const t=Date.now();this.logger.info?.("[acp-server] start() begin"),e&&(this.config.bridge.url=e);const s=this.config.bridge.url,n=this.config.keyFile;if(s&&!BridgeClientCtor)try{const e=Date.now(),t=await import("@beeos-ai/bridge-client");BridgeClientCtor=t.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-e}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}const i=Date.now();if(await this.connectGateway(),this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-i}ms`),s&&n&&BridgeClientCtor){Date.now();await this.startBridgeMode(s,n)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-t}ms`)}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.url!==e&&(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${n.url}, switching`),this.config.bridge.url=n.url,writeDiscoveryCache({bridge:{url:n.url,discoveredAt:Date.now()}}),await s.close(!1),await this.startBridgeMode(n.url,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;const i=Date.now();try{await s.connect(),this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-i}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-i}ms (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)},s=Date.now();try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:!0,fileSystem:!0,cron:!0,streaming:!0},agentFetch:this.agentFetchFn,logger:this.logger});e?(this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`)):t()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),t()}}async connectGateway(){this.gateway&&(this.gateway.stop(),this.gateway=null);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.gateway.on("sessionMessage",e=>this.core.handleSessionMessage(e)),this.gateway.on("response",e=>this.core.handleGatewayResponse(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.core.resubscribeAllSessionMessages()}),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.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),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)??this.core.ensureSessionForKey(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)??this.core.ensureSessionForKey(t);s?.sessionId&&this.state.toolCallToSessionMap.set(e,s.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}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}registerSkillFormSchemas(e,t){const s=this.canvasOrch.schemaRegistry;s&&import("node:fs/promises").then(async({readFile:n})=>{try{const i=`${t}/skill.json`,a=await n(i,"utf-8"),r=JSON.parse(a);if(r.formSchemas?.length){for(const e of r.formSchemas)s.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${r.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}async stop(){this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),this.state.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"})}}
|
package/dist/gateway-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect",MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let i=null;const o=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),r=t=>{clearTimeout(o),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{clearTimeout(o),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),s.close(),t(new Error(e))};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let s;this.emitMessageHash(t);try{s=JSON.parse(t)}catch{return}const o=s.method;o&&!s.id&&(this.isReconnectNotification(o)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",o,s.params));const a=s.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=s.event,t=s.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n});let o=0;const r=setInterval(()=>{o++},200),c=Date.now(),a=setInterval(()=>{const e=Date.now()-c,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200),h=()=>{clearInterval(r),clearInterval(a)};setTimeout(h,3e4),this.once("connected",h),this.once("disconnected",h)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),r(s);else if("hello-error"===a||"connect-error"===a)c("Authentication rejected by Gateway");else if("res"===a){const e=s.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!s.ok,durationMs:Date.now()-n}),s.ok){const e=s.payload;r(e??s)}else{const e=s.error;c(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),s.ok)t.resolve(s.payload);else{const e=s.error;t.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",s)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=this.connected;this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(o),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(o),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",i=["operator.admin","operator.approvals","operator.pairing"],o={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),r=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:i,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();o.nonce=e,o.signature=signDeviceAuth(this.deviceIdentity,r);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const r=randomUUID(),c={type:"req",id:r,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:i,auth:{token:this.config.token},device:o}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const m=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:m}),r}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),i={type:"req",id:s,method:e,params:t};return new Promise((t,o)=>{const r=setTimeout(()=>{this.pending.delete(s),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:o,timer:r}),this.ws.send(JSON.stringify(i))})}async agentSend(e,t,n,s,i){const o={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(o.attachments=s),i&&(o.inputProvenance=i);return await this.request("agent",o,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async skillsStatus(){return this.request("skills.status",{})}async skillsUpdate(e,t){return this.request("skills.update",{skillKey:e,enabled:t})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
|
|
1
|
+
import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const PROTOCOL_VERSION=3,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect",MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let i=null;const o=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),r=t=>{clearTimeout(o),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{clearTimeout(o),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),s.close(),t(new Error(e))};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const t="string"==typeof e?e:e.toString("utf-8");if("pong"===t)return void(this.lastPongTs=Date.now());let s;this.emitMessageHash(t);try{s=JSON.parse(t)}catch{return}const o=s.method;o&&!s.id&&(this.isReconnectNotification(o)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",o,s.params));const a=s.type;if("event"===a&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===a){const e=s.event,t=s.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n});let o=0;const r=setInterval(()=>{o++},200),c=Date.now(),a=setInterval(()=>{const e=Date.now()-c,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200),h=()=>{clearInterval(r),clearInterval(a)};setTimeout(h,3e4),this.once("connected",h),this.once("disconnected",h)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):"session.message"===e?this.emit("sessionMessage",t):this.emit("gateway-event",e,t)}else if("hello-ok"===a)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),r(s);else if("hello-error"===a||"connect-error"===a)c("Authentication rejected by Gateway");else if("res"===a){const e=s.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!s.ok,durationMs:Date.now()-n}),s.ok){const e=s.payload;r(e??s)}else{const e=s.error;c(e?.message||"Gateway connect rejected")}return}const t=this.pending.get(e);if(t)if(this.pending.delete(e),clearTimeout(t.timer),s.ok)t.resolve(s.payload);else{const e=s.error;t.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",s)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=this.connected;this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(o),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.connected||(clearTimeout(o),t(e)),this.emit("error",e)})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);const t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs);this.reconnectAttempt++,this.emit("reconnecting",{attempt:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(e)))}))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",i=["operator.admin","operator.approvals","operator.pairing"],o={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),r=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:i,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();o.nonce=e,o.signature=signDeviceAuth(this.deviceIdentity,r);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const r=randomUUID(),c={type:"req",id:r,method:"connect",params:{minProtocol:3,maxProtocol:3,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:i,auth:{token:this.config.token},device:o}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const m=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:m}),r}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),i={type:"req",id:s,method:e,params:t};return new Promise((t,o)=>{const r=setTimeout(()=>{this.pending.delete(s),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:o,timer:r}),this.ws.send(JSON.stringify(i))})}async agentSend(e,t,n,s,i){const o={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(o.attachments=s),i&&(o.inputProvenance=i);return await this.request("agent",o,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){await this.request("chat.abort",{sessionKey:e,runId:t})}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async sessionsMessagesSubscribe(e){return this.request("sessions.messages.subscribe",{key:e})}async sessionsMessagesUnsubscribe(e){return this.request("sessions.messages.unsubscribe",{key:e})}async skillsStatus(){return this.request("skills.status",{})}async skillsUpdate(e,t){return this.request("skills.update",{skillKey:e,enabled:t})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
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>",
|