beeos-claw 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -179,6 +179,56 @@ Logs are written to rolling files in `log.dir` with automatic rotation and sensi
179
179
  - Node.js >= 18
180
180
  - OpenClaw runtime
181
181
 
182
+ ## Release Channels
183
+
184
+ `beeos-claw` publishes to two npm dist-tags so that staging validation never affects production agent pods:
185
+
186
+ | Channel | Git branch | Version shape | npm dist-tag | Docker image tag |
187
+ |---|---|---|---|---|
188
+ | **production** | `main` | `X.Y.Z` (plain SemVer, e.g. `0.2.41`) | `latest` | `openclaw-desktop:latest`, `production-<sha>` |
189
+ | **staging** | `staging` | `X.Y.Z-staging.N` (pre-release, e.g. `0.2.41-staging.0`) | `staging` | `openclaw-desktop:staging-latest`, `staging-<sha>` |
190
+
191
+ The publish workflow [`.github/workflows/publish.yml`](.github/workflows/publish.yml) inspects the `version` in [`package.json`](./package.json):
192
+
193
+ - If the version contains a `-staging.` / `-rc.` / `-beta.` / `-alpha.` suffix → published with `--tag staging`, downstream image built with `channel=staging`.
194
+ - Otherwise → published with `--tag latest`, downstream image built with `channel=production`.
195
+
196
+ ### Release to staging
197
+
198
+ ```bash
199
+ git checkout staging
200
+ git merge main # or cherry-pick the commits you want to ship
201
+ npm version prerelease --preid=staging --no-git-tag-version
202
+ git commit -am "chore: release $(node -p "require('./package.json').version")"
203
+ git push origin staging
204
+ ```
205
+
206
+ CI publishes `beeos-claw@0.X.Y-staging.N` with `dist-tag=staging`, then triggers `beeos-openclaw` to rebuild `openclaw-desktop:staging-latest`. Consumers pulling `beeos-claw@latest` from npm or `openclaw-desktop:latest` from ECR are **not** affected.
207
+
208
+ ### Promote to production
209
+
210
+ After staging validation:
211
+
212
+ ```bash
213
+ git checkout main
214
+ git merge staging # brings pre-release commits into main
215
+ npm version 0.X.Y --no-git-tag-version # strip the -staging.N suffix
216
+ git commit -am "chore: release $(node -p "require('./package.json').version")"
217
+ git push origin main
218
+ ```
219
+
220
+ CI publishes `beeos-claw@0.X.Y` with `dist-tag=latest`, then triggers `beeos-openclaw` to rebuild `openclaw-desktop:latest` (floating production tag, Docker convention) plus an immutable `openclaw-desktop:production-<sha>` for audit/rollback.
221
+
222
+ ### Install from a specific channel (for local dev / debugging)
223
+
224
+ ```bash
225
+ npm install beeos-claw # always resolves to production (dist-tag=latest)
226
+ npm install beeos-claw@staging # latest staging pre-release
227
+ npm install beeos-claw@0.2.41-staging.3 # pin an exact pre-release build
228
+ ```
229
+
230
+ Pre-release versions never match semver ranges like `^0.2.40`, so production consumers are not silently upgraded to staging builds.
231
+
182
232
  ## License
183
233
 
184
234
  MIT
@@ -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,isCronPromptText}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{LocalSessionHistory}from"./local-session-history.js";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.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{DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS,DEFAULT_ASSISTANTMQ_LATECLOSE_TIMEOUT_MS}from"./types.js";import{ACP_PROTOCOL_VERSION}from"@beeos-ai/beeos-types";function narrowContentBlock(e){switch(e.type){case"text":return"string"==typeof e.text?{type:"text",text:e.text}:void 0;case"resource_link":return"string"==typeof e.uri?{type:"resource_link",uri:e.uri,...void 0!==e.name?{name:e.name}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}}:void 0;case"file":return{type:"file",...void 0!==e.text?{text:e.text}:{},...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};case"image":return{type:"image",...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};default:return}}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;localSessionHistory;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_loadSessionLock=new Map;cronIsolatedSessionKeys=new Set;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.localSessionHistory=new LocalSessionHistory({logger:this.logger}),this.transport.setSessionUpdateObserver((e,t,s)=>{const i=s?.turnId;if(!i)return;const n=this.state.sessions.get(e);n&&recordTurnIdForRole(n,t,i)})}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,t={}){const{ensureVerbose:s=!0}=t,i=this.state.sessions.has(e.sessionId);if(this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e),!i){const t=(new Error).stack?.split("\n")[2]?.trim()??"(unknown)";this.logger.info?.(`[new-chat-stampede] registerSession sid=${e.sessionId.slice(0,12)} title="${(e.title??"").slice(0,24)}" ensureVerbose=${s} stateSize=${this.state.sessions.size} from: ${t}`)}s&&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 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,{ensureVerbose:!1}),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}});const t=e;try{switch(t.method){case"initialize":this.handleInitialize(t);break;case"ping":this.transport.sendResult(t.id,{pong:!0});break;case"session/new":await this.handleSessionNew(t);break;case"session/load":await this.handleSessionLoad(t);break;case"session/list":await this.handleSessionList(t);break;case"session/rename":this.handleSessionRename(t);break;case"session/delete":await this.handleSessionDelete(t);break;case"session/clear":await this.handleSessionClear(t);break;case"session/prompt":await this.handleSessionPrompt(t);break;case"session/set_mode":await this.handleSessionSetMode(t);break;case"session/set_model":await this.handleSessionSetModel(t);break;case"skills/list":await this.handleSkillsList(t);break;case"skills/install":await this.handleSkillsInstall(t);break;case"skills/uninstall":await this.handleSkillsUninstall(t);break;case"skills/update":await this.handleSkillsUpdate(t);break;case"models/list":await this.handleModelsList(t);break;case"session/cancel":this.handleSessionCancel(t.id,t.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(t);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,multipleSessions:!0,sessionCapabilities:{"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled},promptCapabilities:{image:!0,audio:!1,embeddedContext:!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}),n=this.state.sessions.size;this.registerSession(i,{ensureVerbose:!1}),this.logger.info?.(`[new-chat-stampede] session/new sid=${i.sessionId.slice(0,12)} gwKey=${t.slice(0,40)} stateSize ${n}→${this.state.sessions.size}`),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 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 i=this.state.sessionsByGatewayKey.get(t);if(!i)return;const n=e.message?.role,o="user"===n?"user":"system"===n?"system":"assistant",r=i.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={turnId:r};let c;if("number"==typeof e.messageSeq&&(d.seq=e.messageSeq),this.transport.sendSessionUpdate(i.sessionId,{sessionUpdate:"message_id_bind",role:o,messageId:s,...l?{timestamp:l}:{}},d),i.lastTurnIdByRole&&(i.lastTurnIdByRole[o]=void 0),"assistant"===o&&i.assistantMq&&!i.assistantMq.closed&&void 0!==i.assistantMqAwaitingRunId){c=i.assistantMqAwaitingRunId;const e=void 0!==i.assistantMqArmedAt?Date.now()-i.assistantMqArmedAt:0;this.clearAssistantMqCloseTimer(i),i.assistantMq.close(),this.logger.info?.(`[bridge-core] assistantMq late-close settled sessionId=${i.sessionId} runId=${c??"n/a"} messageId=${s} awaitedMs=${e}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.settled",severity:"info",payload:{sessionId:i.sessionId,runId:c,messageId:s,awaitedMs:e}})}"assistant"===o&&reconcileMissingArtifacts({localHistory:this.localSessionHistory,transport:this.transport,logger:this.logger,writeObs:this.writeObs,isSessionAlive:e=>this.state.sessions.has(e)},{session:i,turnId:r,messageId:s,runId:c})}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,{localHistory:this.localSessionHistory,agentId:s.agentId,sessionId:s.sessionId,logger:this.logger,emitObs:e=>{this.writeObs({component:"history-replay",domain:"lifecycle",name:e.name,severity:e.severity,payload:e.payload})}});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;if(isCronPromptText(e.content))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(i,{sessionUpdate:"title_update",title:t}),this.tryGenerateTitleFromHistory(s,i,n)}}}const d=e=>`history:${s.gatewaySessionKey}:${e}`;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`):"user"===t.role&&isCronPromptText(t.content)?this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] cron-triggered user message`):"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,d(e),t.messageId,t._seq):await this.replayAssistantTurnAsStream(i,t,d(e))):(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}:{}},{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 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}const l=void 0!==s.assistantMq&&!s.assistantMq.closed,d=()=>{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}`)})};if(l)return this.replayCurrentAssistantStream(s,o,r,a).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}).finally(()=>d());d()}async replayCurrentAssistantStream(e,t,s="",i=""){if(!e.assistantMq||e.assistantMq.closed)return;const n=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,o=DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS,r=e.assistantMq.peek(0).length;this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_start",severity:"debug",payload:{sessionId:e.sessionId,mqPendingCount:r}});let a=0,l=Date.now();const d={sent:0,skipped:0,cumulativeText:"",cumulativeReasoning:""};for(;e.assistantMq;){const r=e.assistantMq.peek(a);if(r.length>0){l=Date.now();a+=(await this.flushMqItems(e,r,a,t,s,i,d)).total;continue}if(e.assistantMq.closed)break;const c=Date.now()-l;if(c>=o){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream hard-idle-timeout sessionId=${e.sessionId} idleMs=${c} offset=${a}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_hardtimeout",severity:"warn",payload:{sessionId:e.sessionId,idleMs:c,offset:a}});break}if(c>=n){if(e.promptDone||void 0===e.pendingPromptId){this.logger.info?.(`[bridge-core] replayCurrentAssistantStream soft-idle-stop sessionId=${e.sessionId} idleMs=${c} offset=${a} promptDone=${!!e.promptDone}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_softidle_stop",severity:"info",payload:{sessionId:e.sessionId,idleMs:c,offset:a,promptDone:!!e.promptDone}});break}this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream soft-idle-continue sessionId=${e.sessionId} idleMs=${c} offset=${a}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_softidle_continue",severity:"debug",payload:{sessionId:e.sessionId,idleMs:c,offset:a}})}await new Promise(e=>{const t=setTimeout(e,200);"object"==typeof t&&"unref"in t&&t.unref()})}d.skipped>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${d.skipped} duplicate tool events (already in history)`),(d.sent>0||d.skipped>0)&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:d.sent,skipped:d.skipped}})}async flushMqItems(e,t,s,i,n,o,r){const a=r??{sent:0,skipped:0,cumulativeText:"",cumulativeReasoning:""};let l=0;for(const s of t)if(l++,!i?.size||"tool_call"!==s.sessionUpdate&&"tool_call_update"!==s.sessionUpdate||!i.has(s.toolCallId)){if(n.length>0&&"agent_message_chunk"===s.sessionUpdate){const e="text"===s.content.type?s.content.text:void 0;if("string"==typeof e&&(a.cumulativeText+=e,a.cumulativeText.length<=n.length)){a.skipped++;continue}}if(o.length>0&&"agent_thought_chunk"===s.sessionUpdate){const e="text"===s.content.type?s.content.text:void 0;if("string"==typeof e&&(a.cumulativeReasoning+=e,a.cumulativeReasoning.length<=o.length)){a.skipped++;continue}}this.transport.sendSessionUpdate(e.sessionId,s),a.sent++}else a.skipped++;return{total:l}}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||this.cronIsolatedSessionKeys.has(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;if(this.cronIsolatedSessionKeys.has(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,{ensureVerbose:!1}),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));try{const e=t.map(e=>{const t=this.state.sessions.get(e.sessionId);return{sid:e.sessionId.slice(0,12),title:(e.title||"").slice(0,24),titleSource:t?.titleSource??"(none)",hasActivity:!!t?.lastStreamActivityTs,createdAt:t?.createdAt,updatedAt:e.updatedAt,gwKey:t?.gatewaySessionKey?.slice(0,40)}});this.logger.info?.(`[new-chat-stampede] session/list n=${t.length} total_state=${this.state.sessions.size} entries=${JSON.stringify(e)}`)}catch{}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 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,i){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}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(i)}`;this.logger.info?.(`[bridge-core] title-gen CALLING LLM sid=${t} q="${s.slice(0,60)}" aLen=${i.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 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 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 i=this.getGateway();if(!i?.isConnected)return;let n=s;if(!n)try{const t=await loadHistory(i,e.gatewaySessionKey);n=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=n.find(e=>{if("user"!==e.role)return!1;if(!e.content?.trim())return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return!!t&&!/^Sender\s*\(untrusted/i.test(t)});if(!o)return;const r=n.indexOf(o),a=n.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 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),this.clearAssistantMqCloseTimer(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})}}}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:m,attachments:g}=c,h=resolveStandaloneSlashCommandName(a,m)??resolveSlashCmd(a,m),p="string"==typeof h;if(h&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(h)}catch(e){return t.logger.warn?.(`[bridge-core] /${h} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(i.sessionId,a,resolveTurnId(i)),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${h}...`},timestamp:(new Date).toISOString()},{turnId:resolveTurnId(i)}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let y=p?m:withUserMessagePrefix(m);const f=this.config.canvasEnabled&&!p&&!1!==i.canvasActive&&!i.channelType;if(f){const e=i.promptsSinceCanvasInjection??0;!i.canvasPromptInjected||e>=10?(y=`${CANVAS_TERM_MAPPING}\n\n${y}`,i.canvasPromptInjected=!0,i.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):i.promptsSinceCanvasInjection=e+1}if(f){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(i.gatewaySessionKey);y=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${y}`}const t=i.referencedSurfaceId;t&&(y=`${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${y}`,i.referencedSurfaceId=void 0)}if(!p){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){i.activeMentions=t;y=`${buildMentionInstruction(t)}\n${y}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else i.activeMentions=void 0}p&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${h}`),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;const I=resolveTurnId(i);this.emitPromptAsUserSessionUpdates(o,a,I),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()},I?{turnId:I}:void 0),this.schedulePromptTimeout(i);const S=g.length>0?g.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,"")}`,b={sessionKey:i.gatewaySessionKey,message:y,deliver:!1,idempotencyKey:randomUUID()};S?.length&&(b.attachments=S);if(!r.sendFrame({type:"req",id:w,method:u,params:b}))return this.state.clearPromptTimeout(i),i.pendingPromptId=void 0,this.clearAssistantMqCloseTimer(i),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,n,o,r){const a=splitUserReplayBlocks(t),l=n||o||void 0!==r?{...n?{turnId:n}:{},...o?{messageId:o}:{},...void 0!==r?{seq:r}:{}}:void 0;for(const t of a){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}:{}},l)}}async replayAssistantTurnAsStream(e,t,s){const i=(e,t)=>{if(s||e||void 0!==t)return{...s?{turnId:s}:{},...e?{messageId:e}:{},...void 0!==t?{seq:t}:{}}},n=i(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,n)}for(const s of t.sourceMessages){const n=s.updateType??"agent_message_chunk",o=i(s.messageId,s._seq);switch(n){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{},...s.metadata?{metadata:s.metadata}:{}},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 i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),r=s.toolName??"tool",a=s.content??"";let l={};if(a)try{l=JSON.parse(a)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:i,toolName:r,args:l,kind:n?.kind,status:n?.status??"in_progress",timestamp:s.timestamp}),o);break}case"tool_call_update":{const i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),r=s.toolName??n?.name??"tool",a=s.content??n?.output??"";let l=await readEnrichmentCache(i).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(i).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:i,toolName:r,outputText:a,kind:n?.kind,status:n?.status??"completed",resourceLinks:l,timestamp:s.timestamp}),o);break}}}}emitPromptAsUserSessionUpdates(e,t,s){const i=(new Date).toISOString(),n=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:i},n)}}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&&"object"==typeof t&&"sessionId"in t?t.sessionId:void 0;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"},{turnId:resolveTurnId(i)}),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),i=s?{turnId:s}:void 0;e.promptDone=!0;const n=e.currentRunId;if("complete"===t.kind){if(n&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(n,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason},i),this.tryGenerateTitleFromHistory(e,e.sessionId)}else n&&this.onRunFailed&&this.onRunFailed(n,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},i);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);const s=e.currentRunId;e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,this.armAssistantMqLateClose(e,s)}armAssistantMqLateClose(e,t){if(!e.assistantMq||e.assistantMq.closed)return;this.clearAssistantMqCloseTimer(e),e.assistantMqAwaitingRunId=t,e.assistantMqArmedAt=Date.now();const s=DEFAULT_ASSISTANTMQ_LATECLOSE_TIMEOUT_MS;this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.armed",severity:"debug",payload:{sessionId:e.sessionId,runId:t,timeoutMs:s}}),e.assistantMqCloseTimer=setTimeout(()=>{e.assistantMq&&!e.assistantMq.closed&&(this.logger.warn?.(`[bridge-core] assistantMq late-close timeout sessionId=${e.sessionId} runId=${t??"n/a"} timeoutMs=${s}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.timeout",severity:"warn",payload:{sessionId:e.sessionId,runId:t,timeoutMs:s}}),e.assistantMq.close()),e.assistantMqCloseTimer=void 0,e.assistantMqAwaitingRunId=void 0,e.assistantMqArmedAt=void 0},s),"object"==typeof e.assistantMqCloseTimer&&"unref"in e.assistantMqCloseTimer&&e.assistantMqCloseTimer.unref()}clearAssistantMqCloseTimer(e){e.assistantMqCloseTimer&&(clearTimeout(e.assistantMqCloseTimer),e.assistantMqCloseTimer=void 0),e.assistantMqAwaitingRunId=void 0,e.assistantMqArmedAt=void 0}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(),a={turnId:resolveTurnId(n)};for(const e of s){const t=narrowContentBlock(e);if(!t)continue;const s={sessionUpdate:"agent_message_chunk",content:t,timestamp:r};this.transport.sendSessionUpdate(o,s,a),n.assistantMq?.push(s)}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??{}),"cron.add"!==s&&"cron.update"!==s&&"cron.remove"!==s||this.refreshCronIsolatedSessionKeys()}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 refreshCronIsolatedSessionKeys(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.request("cron.list",{}),s=t?.jobs;if(!Array.isArray(s))return;const i=new Set;for(const e of s){if(!e||"object"!=typeof e)continue;const t=e;if("isolated"!==t.sessionTarget)continue;const s=t.sessionKey;"string"==typeof s&&s.length>0&&i.add(s)}this.cronIsolatedSessionKeys=i}catch(e){this.logger.debug?.(`[bridge-core] refreshCronIsolatedSessionKeys failed: ${e instanceof Error?e.message:e}`)}}}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,isCronPromptText,classifyHeartbeatFilter,previewFilteredText}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{LocalSessionHistory}from"./local-session-history.js";import{reconcileMissingArtifacts}from"./missing-artifacts-reconciler.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{McpService}from"./mcp-service.js";import{DEFAULT_PROMPT_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS,DEFAULT_ASSISTANTMQ_LATECLOSE_TIMEOUT_MS}from"./types.js";import{ACP_PROTOCOL_VERSION}from"@beeos-ai/beeos-types";function narrowContentBlock(e){switch(e.type){case"text":return"string"==typeof e.text?{type:"text",text:e.text}:void 0;case"resource_link":return"string"==typeof e.uri?{type:"resource_link",uri:e.uri,...void 0!==e.name?{name:e.name}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}}:void 0;case"file":return{type:"file",...void 0!==e.text?{text:e.text}:{},...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};case"image":return{type:"image",...void 0!==e.uri?{uri:e.uri}:{},...void 0!==e.mimeType?{mimeType:e.mimeType}:{}};default:return}}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;localSessionHistory;mcp;initialized=!1;agentMapping=null;_syncResolvers=[];_mappingReadyCallbacks=[];_loadSessionLock=new Map;cronIsolatedSessionKeys=new Set;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.localSessionHistory=new LocalSessionHistory({logger:this.logger}),this.mcp=new McpService({transport:this.transport,logger:this.logger,state:this.state}),this.transport.setSessionUpdateObserver((e,t,s)=>{const i=s?.turnId;if(!i)return;const n=this.state.sessions.get(e);n&&recordTurnIdForRole(n,t,i)})}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,t={}){const{ensureVerbose:s=!0}=t,i=this.state.sessions.has(e.sessionId);if(this.state.sessions.set(e.sessionId,e),this.state.sessionsByGatewayKey.set(e.gatewaySessionKey,e),!i){const t=(new Error).stack?.split("\n")[2]?.trim()??"(unknown)";this.logger.info?.(`[new-chat-stampede] registerSession sid=${e.sessionId.slice(0,12)} title="${(e.title??"").slice(0,24)}" ensureVerbose=${s} stateSize=${this.state.sessions.size} from: ${t}`)}s&&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 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,{ensureVerbose:!1}),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}});const t=e;try{switch(t.method){case"initialize":this.handleInitialize(t);break;case"ping":this.transport.sendResult(t.id,{pong:!0});break;case"session/new":await this.handleSessionNew(t);break;case"session/load":await this.handleSessionLoad(t);break;case"session/list":await this.handleSessionList(t);break;case"session/rename":this.handleSessionRename(t);break;case"session/delete":await this.handleSessionDelete(t);break;case"session/clear":await this.handleSessionClear(t);break;case"session/prompt":await this.handleSessionPrompt(t);break;case"session/set_mode":await this.handleSessionSetMode(t);break;case"session/set_model":await this.handleSessionSetModel(t);break;case"skills/list":await this.handleSkillsList(t);break;case"skills/install":await this.handleSkillsInstall(t);break;case"skills/uninstall":await this.handleSkillsUninstall(t);break;case"skills/update":await this.handleSkillsUpdate(t);break;case"models/list":await this.handleModelsList(t);break;case"session/cancel":this.handleSessionCancel(t.id,t.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(t);break;default:"mcp/list"===e.method?await this.mcp.handleList(e):"mcp/set"===e.method?await this.mcp.handleSet(e):"mcp/unset"===e.method?await this.mcp.handleUnset(e):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,multipleSessions:!0,sessionCapabilities:{"web-ssh":!!this.config.bridge.shell.enabled,canvas:!!this.config.canvasEnabled},promptCapabilities:{image:!0,audio:!1,embeddedContext:!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}),n=this.state.sessions.size;this.registerSession(i,{ensureVerbose:!1}),this.logger.info?.(`[new-chat-stampede] session/new sid=${i.sessionId.slice(0,12)} gwKey=${t.slice(0,40)} stateSize ${n}→${this.state.sessions.size}`),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 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 i=this.state.sessionsByGatewayKey.get(t);if(!i)return;const n=e.message?.role,o="user"===n?"user":"system"===n?"system":"assistant",r=i.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={turnId:r};let c;if("number"==typeof e.messageSeq&&(d.seq=e.messageSeq),this.transport.sendSessionUpdate(i.sessionId,{sessionUpdate:"message_id_bind",role:o,messageId:s,...l?{timestamp:l}:{}},d),i.lastTurnIdByRole&&(i.lastTurnIdByRole[o]=void 0),"assistant"===o&&i.assistantMq&&!i.assistantMq.closed&&void 0!==i.assistantMqAwaitingRunId){c=i.assistantMqAwaitingRunId;const e=void 0!==i.assistantMqArmedAt?Date.now()-i.assistantMqArmedAt:0;this.clearAssistantMqCloseTimer(i),i.assistantMq.close(),this.logger.info?.(`[bridge-core] assistantMq late-close settled sessionId=${i.sessionId} runId=${c??"n/a"} messageId=${s} awaitedMs=${e}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.settled",severity:"info",payload:{sessionId:i.sessionId,runId:c,messageId:s,awaitedMs:e}})}"assistant"===o&&reconcileMissingArtifacts({localHistory:this.localSessionHistory,transport:this.transport,logger:this.logger,writeObs:this.writeObs,isSessionAlive:e=>this.state.sessions.has(e)},{session:i,turnId:r,messageId:s,runId:c})}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,{localHistory:this.localSessionHistory,agentId:s.agentId,sessionId:s.sessionId,logger:this.logger,emitObs:e=>{this.writeObs({component:"history-replay",domain:"lifecycle",name:e.name,severity:e.severity,payload:e.payload})}});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;if(isCronPromptText(e.content))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(i,{sessionUpdate:"title_update",title:t}),this.tryGenerateTitleFromHistory(s,i,n)}}}const d=e=>`history:${s.gatewaySessionKey}:${e}`;for(let e=0;e<n.length;e++){const t=n[e];if("user"!==t.role||"internal_system"!==t.provenance?.kind)if("user"===t.role&&/^\s*system\s*\(untrusted[^)]*\)/i.test(t.content))this.logger.debug?.(`[bridge-core] SKIP history_turn[${e}] system-injected user message`);else if("user"===t.role&&isHeartbeatPromptText(t.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${e}] heartbeat cron prompt sessionId=${s.sessionId} contentLen=${t.content.length} preview="${previewFilteredText(t.content)}"`);else if("user"===t.role&&isCronPromptText(t.content))this.logger.info?.(`[bridge-core] SKIP history_turn[${e}] cron-triggered user message sessionId=${s.sessionId} contentLen=${t.content.length} preview="${previewFilteredText(t.content)}"`);else{if("assistant"===t.role){const i=classifyHeartbeatFilter(t.content);if(i.filtered){this.logger.info?.(`[bridge-core] SKIP history_turn[${e}] heartbeat assistant reply reason=${i.reason} sessionId=${s.sessionId} contentLen=${t.content.length} preview="${previewFilteredText(t.content)}"`);continue}}"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,d(e),t.messageId,t._seq):await this.replayAssistantTurnAsStream(i,t,d(e))):(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}:{}},{turnId:d(e),...t.messageId?{messageId:t.messageId}:{},...void 0!==t._seq?{seq:t._seq}:{}}))}else 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}const l=AcpGatewayBridgeCore.shouldReplayAssistantMq(s,n);l||void 0===s.assistantMq||s.assistantMq.closed||void 0===s.assistantMqAwaitingRunId||this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.mq_skip_history_covers",severity:"info",payload:{sessionId:i,awaitingRunId:s.assistantMqAwaitingRunId,reason:"late-close window, history tail assistant covers the turn"}});const d=()=>{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}`)})};if(l)return this.replayCurrentAssistantStream(s,o,r,a).catch(e=>{this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream failed: ${e instanceof Error?e.message:e}`)}).finally(()=>d());d()}static shouldReplayAssistantMq(e,t){if(!e.assistantMq||e.assistantMq.closed)return!1;if(void 0!==e.currentRunId)return!0;if(void 0===e.assistantMqAwaitingRunId)return!0;let s=!1;for(let e=t.length-1;e>=0;e--){const i=t[e]?.role;if("system"!==i){s="assistant"===i;break}}return!s}async replayCurrentAssistantStream(e,t,s="",i=""){if(!e.assistantMq||e.assistantMq.closed)return;const n=this.config.loadReplayIdleTimeoutMs??DEFAULT_LOAD_REPLAY_IDLE_TIMEOUT_MS,o=DEFAULT_LOAD_REPLAY_HARD_TIMEOUT_MS,r=e.assistantMq.peek(0).length;this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_start",severity:"debug",payload:{sessionId:e.sessionId,mqPendingCount:r}});let a=0,l=Date.now();const d={sent:0,skipped:0,cumulativeText:"",cumulativeReasoning:""};for(;e.assistantMq;){const r=e.assistantMq.peek(a);if(r.length>0){l=Date.now();a+=(await this.flushMqItems(e,r,a,t,s,i,d)).total;continue}if(e.assistantMq.closed)break;const c=Date.now()-l;if(c>=o){this.logger.warn?.(`[bridge-core] replayCurrentAssistantStream hard-idle-timeout sessionId=${e.sessionId} idleMs=${c} offset=${a}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_hardtimeout",severity:"warn",payload:{sessionId:e.sessionId,idleMs:c,offset:a}});break}if(c>=n){if(e.promptDone||void 0===e.pendingPromptId){this.logger.info?.(`[bridge-core] replayCurrentAssistantStream soft-idle-stop sessionId=${e.sessionId} idleMs=${c} offset=${a} promptDone=${!!e.promptDone}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_softidle_stop",severity:"info",payload:{sessionId:e.sessionId,idleMs:c,offset:a,promptDone:!!e.promptDone}});break}this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream soft-idle-continue sessionId=${e.sessionId} idleMs=${c} offset=${a}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_softidle_continue",severity:"debug",payload:{sessionId:e.sessionId,idleMs:c,offset:a}})}await new Promise(e=>{const t=setTimeout(e,200);"object"==typeof t&&"unref"in t&&t.unref()})}d.skipped>0&&this.logger.debug?.(`[bridge-core] replayCurrentAssistantStream skipped ${d.skipped} duplicate tool events (already in history)`),(d.sent>0||d.skipped>0)&&this.writeObs({component:"bridge-core",domain:"lifecycle",name:"session.load.replay_complete",severity:"debug",payload:{sessionId:e.sessionId,replayed:d.sent,skipped:d.skipped}})}async flushMqItems(e,t,s,i,n,o,r){const a=r??{sent:0,skipped:0,cumulativeText:"",cumulativeReasoning:""};let l=0;for(const s of t)if(l++,!i?.size||"tool_call"!==s.sessionUpdate&&"tool_call_update"!==s.sessionUpdate||!i.has(s.toolCallId)){if(n.length>0&&"agent_message_chunk"===s.sessionUpdate){const e="text"===s.content.type?s.content.text:void 0;if("string"==typeof e&&(a.cumulativeText+=e,a.cumulativeText.length<=n.length)){a.skipped++;continue}}if(o.length>0&&"agent_thought_chunk"===s.sessionUpdate){const e="text"===s.content.type?s.content.text:void 0;if("string"==typeof e&&(a.cumulativeReasoning+=e,a.cumulativeReasoning.length<=o.length)){a.skipped++;continue}}this.transport.sendSessionUpdate(e.sessionId,s),a.sent++}else a.skipped++;return{total:l}}async handleSessionList(e){const t=[];for(const[,e]of this.state.sessions)isBackgroundSessionKey(e.gatewaySessionKey)||this.cronIsolatedSessionKeys.has(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;if(this.cronIsolatedSessionKeys.has(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,{ensureVerbose:!1}),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));try{const e=t.map(e=>{const t=this.state.sessions.get(e.sessionId);return{sid:e.sessionId.slice(0,12),title:(e.title||"").slice(0,24),titleSource:t?.titleSource??"(none)",hasActivity:!!t?.lastStreamActivityTs,createdAt:t?.createdAt,updatedAt:e.updatedAt,gwKey:t?.gatewaySessionKey?.slice(0,40)}});this.logger.info?.(`[new-chat-stampede] session/list n=${t.length} total_state=${this.state.sessions.size} entries=${JSON.stringify(e)}`)}catch{}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 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,i){if(this.isGuardMessage(s))return void this.logger.info?.(`[bridge-core] title-gen SKIP guard message sid=${t}`);const n=this.matchTitleTemplate(s);if(n){e.title=n,e.titleSource="template";const s=this.getGateway();return s?.isConnected&&s.sessionsPatch(e.gatewaySessionKey,{label:n}).catch(()=>{}),void this.transport.sendSessionUpdate(t,{sessionUpdate:"title_update",title:n})}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(i)}`;this.logger.info?.(`[bridge-core] title-gen CALLING LLM sid=${t} q="${s.slice(0,60)}" aLen=${i.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 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 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 i=this.getGateway();if(!i?.isConnected)return;let n=s;if(!n)try{const t=await loadHistory(i,e.gatewaySessionKey);n=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=n.find(e=>{if("user"!==e.role)return!1;if(!e.content?.trim())return!1;if("internal_system"===e.provenance?.kind)return!1;if(isCronPromptText(e.content))return!1;const t=stripTransportMetadata(e.content).trim();return!!t&&!/^Sender\s*\(untrusted/i.test(t)});if(!o)return;const r=n.indexOf(o),a=n.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 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),this.clearAssistantMqCloseTimer(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})}}}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:m,attachments:g}=c,p=resolveStandaloneSlashCommandName(a,m)??resolveSlashCmd(a,m),h="string"==typeof p;if(p&&this.handleSlashCommand){const t=this;if(await(async()=>{try{return await t.handleSlashCommand(p)}catch(e){return t.logger.warn?.(`[bridge-core] /${p} handler failed: ${e instanceof Error?e.message:e}`),!1}})())return this.emitPromptAsUserSessionUpdates(i.sessionId,a,resolveTurnId(i)),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`Loading ${p}...`},timestamp:(new Date).toISOString()},{turnId:resolveTurnId(i)}),void this.transport.sendResult(e.id,{stopReason:"end_turn"})}const u="chat.send";let y=h?m:withUserMessagePrefix(m);const f=this.config.canvasEnabled&&!h&&!1!==i.canvasActive&&!i.channelType;if(f){const e=i.promptsSinceCanvasInjection??0;!i.canvasPromptInjected||e>=10?(y=`${CANVAS_TERM_MAPPING}\n\n${y}`,i.canvasPromptInjected=!0,i.promptsSinceCanvasInjection=0,this.logger.debug?.("[bridge-core] Canvas term mapping injected (re-inject cycle)")):i.promptsSinceCanvasInjection=e+1}if(f){const e=this.getCanvasState?.();if(e){const t=e.resolveCanvasForSession(i.gatewaySessionKey);y=`${buildActiveSurfacesContext(e,t)??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${y}`}const t=i.referencedSurfaceId;t&&(y=`${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${y}`,i.referencedSurfaceId=void 0)}if(!h){const t=parseMentionsParam(e.params?.mentions);if(t.length>0){i.activeMentions=t;y=`${buildMentionInstruction(t)}\n${y}`,this.logger.debug?.(`[bridge-core] A2A mention instruction injected for ${t.length} agent(s)`)}else i.activeMentions=void 0}h&&this.logger.info?.(`[bridge-core] slash command passthrough sessionId=${o} command=/${p}`),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;const I=resolveTurnId(i);this.emitPromptAsUserSessionUpdates(o,a,I),this.transport.sendSessionUpdate(o,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:""},timestamp:(new Date).toISOString()},I?{turnId:I}:void 0),this.schedulePromptTimeout(i);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,S=`req_${randomUUID().replace(/-/g,"")}`,v={sessionKey:i.gatewaySessionKey,message:y,deliver:!1,idempotencyKey:randomUUID()};w?.length&&(v.attachments=w);if(!r.sendFrame({type:"req",id:S,method:u,params:v}))return this.state.clearPromptTimeout(i),i.pendingPromptId=void 0,this.clearAssistantMqCloseTimer(i),i.assistantMq?.close(),void this.transport.sendAcpError(e.id,Errors.SendFailed,{message:"failed to send prompt to gateway"});i.gatewayRequestId=S,this.state.promptsByGatewayRequestId.set(S,i)}replayUserTurnAsChunks(e,t,s,i,n,o,r){const a=splitUserReplayBlocks(t),l=n||o||void 0!==r?{...n?{turnId:n}:{},...o?{messageId:o}:{},...void 0!==r?{seq:r}:{}}:void 0;for(const t of a){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}:{}},l)}}async replayAssistantTurnAsStream(e,t,s){const i=(e,t)=>{if(s||e||void 0!==t)return{...s?{turnId:s}:{},...e?{messageId:e}:{},...void 0!==t?{seq:t}:{}}},n=i(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,n)}for(const s of t.sourceMessages){const n=s.updateType??"agent_message_chunk",o=i(s.messageId,s._seq);switch(n){case"agent_message_chunk":this.transport.sendSessionUpdate(e,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:s.content},...s.timestamp?{timestamp:s.timestamp}:{},...s.metadata?{metadata:s.metadata}:{}},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 i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),r=s.toolName??"tool",a=s.content??"";let l={};if(a)try{l=JSON.parse(a)}catch{}this.transport.sendSessionUpdate(e,buildToolCallPayload({toolCallId:i,toolName:r,args:l,kind:n?.kind,status:n?.status??"in_progress",timestamp:s.timestamp}),o);break}case"tool_call_update":{const i=s.toolCallId??"",n=t.toolCalls?.find(e=>e.id===i),r=s.toolName??n?.name??"tool",a=s.content??n?.output??"";let l=await readEnrichmentCache(i).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(i).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:i,toolName:r,outputText:a,kind:n?.kind,status:n?.status??"completed",resourceLinks:l,timestamp:s.timestamp}),o);break}}}}emitPromptAsUserSessionUpdates(e,t,s){const i=(new Date).toISOString(),n=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:i},n)}}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&&"object"==typeof t&&"sessionId"in t?t.sessionId:void 0;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"},{turnId:resolveTurnId(i)}),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),i=s?{turnId:s}:void 0;e.promptDone=!0;const n=e.currentRunId;if("complete"===t.kind){if(n&&this.onRunComplete){const t=e.agentAssistantTextSoFar??e.chatAssistantTextSoFar??"";this.onRunComplete(n,t)}void 0!==e.pendingPromptId&&this.transport.sendResult(e.pendingPromptId,{stopReason:t.stopReason}),this.transport.sendSessionUpdate(e.sessionId,{sessionUpdate:"stream_complete",stopReason:t.stopReason},i),this.tryGenerateTitleFromHistory(e,e.sessionId)}else n&&this.onRunFailed&&this.onRunFailed(n,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},i);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);const s=e.currentRunId;e.pendingPromptId=void 0,e.promptDone=void 0,e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantStreamStartedAt=void 0,e.gatewayMethod=void 0,this.armAssistantMqLateClose(e,s)}armAssistantMqLateClose(e,t){if(!e.assistantMq||e.assistantMq.closed)return;this.clearAssistantMqCloseTimer(e),e.assistantMqAwaitingRunId=t,e.assistantMqArmedAt=Date.now();const s=DEFAULT_ASSISTANTMQ_LATECLOSE_TIMEOUT_MS;this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.armed",severity:"debug",payload:{sessionId:e.sessionId,runId:t,timeoutMs:s}}),e.assistantMqCloseTimer=setTimeout(()=>{e.assistantMq&&!e.assistantMq.closed&&(this.logger.warn?.(`[bridge-core] assistantMq late-close timeout sessionId=${e.sessionId} runId=${t??"n/a"} timeoutMs=${s}`),this.writeObs({component:"bridge-core",domain:"lifecycle",name:"assistantMq.lateclose.timeout",severity:"warn",payload:{sessionId:e.sessionId,runId:t,timeoutMs:s}}),e.assistantMq.close()),e.assistantMqCloseTimer=void 0,e.assistantMqAwaitingRunId=void 0,e.assistantMqArmedAt=void 0},s),"object"==typeof e.assistantMqCloseTimer&&"unref"in e.assistantMqCloseTimer&&e.assistantMqCloseTimer.unref()}clearAssistantMqCloseTimer(e){e.assistantMqCloseTimer&&(clearTimeout(e.assistantMqCloseTimer),e.assistantMqCloseTimer=void 0),e.assistantMqAwaitingRunId=void 0,e.assistantMqArmedAt=void 0}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(),a={turnId:resolveTurnId(n)};for(const e of s){const t=narrowContentBlock(e);if(!t)continue;const s={sessionUpdate:"agent_message_chunk",content:t,timestamp:r};this.transport.sendSessionUpdate(o,s,a),n.assistantMq?.push(s)}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??{}),"cron.add"!==s&&"cron.update"!==s&&"cron.remove"!==s||this.refreshCronIsolatedSessionKeys()}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 refreshCronIsolatedSessionKeys(){const e=this.getGateway();if(e?.isConnected)try{const t=await e.request("cron.list",{}),s=t?.jobs;if(!Array.isArray(s))return;const i=new Set;for(const e of s){if(!e||"object"!=typeof e)continue;const t=e;if("isolated"!==t.sessionTarget)continue;const s=t.sessionKey;"string"==typeof s&&s.length>0&&i.add(s)}this.cronIsolatedSessionKeys=i}catch(e){this.logger.debug?.(`[bridge-core] refreshCronIsolatedSessionKeys failed: ${e instanceof Error?e.message:e}`)}}}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 +1 @@
1
- import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeDeps(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),s=vi.fn().mockReturnValue(!0),n={sendSessionUpdate:s,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},a=vi.fn(),o=vi.fn(),i=vi.fn(),r=vi.fn();return{events:new AcpGatewayEvents({state:t,transport:n,config:{gateway:{agentId:"main"}},logger:e,writeObs:vi.fn(),completePrompt:a,failPrompt:o,maybeCompletePromptFromChatState:i,onStreamDelta:r}),state:t,transport:n,sendSessionUpdate:s,onStreamDelta:r,logger:e}}function makeSession(e,t){const s={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:t,pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return e.sessions.set(s.sessionId,s),e.promptsByRunId.set(t,s),s}describe("Bug-Z: heartbeat live-path filtering",()=>{let e,t;beforeEach(()=>{e=makeDeps(),t=makeSession(e.state,"run-heartbeat-1")}),it("drops agent.assistant delta whose cumulative text is HEARTBEAT_OK",()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEARTBEAT_OK"}};e.events.handleAgentEvent(t);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(s).toHaveLength(0),expect(e.onStreamDelta).not.toHaveBeenCalled()}),it('drops heartbeat ack across split deltas ("HEART" + "BEAT_OK")',()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEART"}},s={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:2,ts:Date.now(),data:{delta:"BEAT_OK"}};e.events.handleAgentEvent(t),e.events.handleAgentEvent(s);const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate).map(e=>e.content?.text).filter(Boolean);expect(n).not.toContain("BEAT_OK")}),it("does NOT drop normal assistant text that happens to contain HEARTBEAT as substring",()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"Let's design a HEARTBEAT_OK-like ack for our API."}};e.events.handleAgentEvent(t);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(s).toHaveLength(1),expect(s[0].content.text).toContain("HEARTBEAT_OK-like")}),it("drops chat.text heartbeat when no agent.assistant stream is active",()=>{const t={runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,message:{role:"assistant",content:[{type:"text",text:"HEARTBEAT_OK"}],timestamp:Date.now()}};e.events.handleChatEvent(t);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(s).toHaveLength(0)}),it("does NOT drop a real chat.text that only mentions the token inline",()=>{const t={runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,message:{role:"assistant",content:[{type:"text",text:"Use the sentinel HEARTBEAT_OK in your ack handler."}],timestamp:Date.now()}};e.events.handleChatEvent(t);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(s).toHaveLength(1)})});
1
+ import{describe,it,expect,beforeEach,vi}from"vitest";import{AcpGatewayEvents}from"./gateway-events.js";import{AcpGatewaySessionState}from"./session-state.js";function makeDeps(){const e={info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()},t=new AcpGatewaySessionState(e),n=vi.fn().mockReturnValue(!0),s={sendSessionUpdate:n,sendResult:vi.fn(),sendAcpError:vi.fn(),sendNotification:vi.fn(),resetSessionUpdateCount:vi.fn(),setSessionUpdateObserver:vi.fn(),isReady:()=>!0,get forwardControl(){return{forwardThinking:!0,forwardToolCalls:!0}}},a=vi.fn(),o=vi.fn(),i=vi.fn(),r=vi.fn(),d=vi.fn();return{events:new AcpGatewayEvents({state:t,transport:s,config:{gateway:{agentId:"main"}},logger:e,writeObs:d,completePrompt:a,failPrompt:o,maybeCompletePromptFromChatState:i,onStreamDelta:r}),state:t,transport:s,sendSessionUpdate:n,onStreamDelta:r,logger:e,writeObs:d}}function makeSession(e,t){const n={sessionId:"s1",gatewaySessionKey:"main/s1",agentId:"main",currentRunId:t,pendingPromptId:1,promptDone:!1,createdAt:Date.now(),chatReplayDedupKeys:new Set};return e.sessions.set(n.sessionId,n),e.promptsByRunId.set(t,n),n}describe("Bug-Z: heartbeat live-path filtering",()=>{let e,t;beforeEach(()=>{e=makeDeps(),t=makeSession(e.state,"run-heartbeat-1")}),it("drops agent.assistant delta whose cumulative text is HEARTBEAT_OK",()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEARTBEAT_OK"}};e.events.handleAgentEvent(t);const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(n).toHaveLength(0),expect(e.onStreamDelta).not.toHaveBeenCalled()}),it("emits a structured SKIP log + obs event with reason + preview on prefix drop",()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEARTBEAT_"}};e.events.handleAgentEvent(t);const n=e.logger.info.mock.calls.map(e=>String(e[0])),s=n.find(e=>e.includes("SKIP heartbeat assistant-delta"));expect(s,`expected a SKIP log, got info=${JSON.stringify(n)}`).toBeDefined(),expect(s).toContain("reason=prefix"),expect(s).toContain("sessionId=s1"),expect(s).toContain("runId=run-heartbeat-1"),expect(s).toContain('cumulative="HEARTBEAT_"'),expect(s).toContain('delta="HEARTBEAT_"');const a=e.writeObs.mock.calls.map(e=>e[0]).find(e=>"heartbeat_filter_skip"===e.name);expect(a).toBeDefined(),expect(a.component).toBe("gateway-events"),expect(a.severity).toBe("info");const o=a.payload;expect(o.source).toBe("assistant-delta"),expect(o.reason).toBe("prefix"),expect(o.sessionId).toBe("s1"),expect(o.runId).toBe("run-heartbeat-1"),expect(o.cumulativePreview).toBe("HEARTBEAT_"),expect(o.deltaPreview).toBe("HEARTBEAT_")}),it('drops heartbeat ack across split deltas ("HEART" + "BEAT_OK") — no underscore or full token leaks',()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEART"}},n={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:2,ts:Date.now(),data:{delta:"BEAT_OK"}};e.events.handleAgentEvent(t),e.events.handleAgentEvent(n);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate).map(e=>e.content?.text??"").join("");expect(s).not.toContain("_"),expect(s).not.toContain("HEARTBEAT_OK")}),it('drops real-world split ("HEARTBEAT_" + "OK") — the ghost bubble case',()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"HEARTBEAT_"}},n={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:2,ts:Date.now(),data:{delta:"OK"}};e.events.handleAgentEvent(t),e.events.handleAgentEvent(n);const s=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(s).toHaveLength(0),expect(e.onStreamDelta).not.toHaveBeenCalled()}),it('drops fine-grained split ("H" + "EARTBEAT" + "_OK") at least at the underscore boundary',()=>{const t=["H","EARTBEAT","_OK"];for(let n=0;n<t.length;n++)e.events.handleAgentEvent({stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:n+1,ts:Date.now(),data:{delta:t[n]}});const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate).map(e=>e.content?.text??"").join("");expect(n).not.toContain("_"),expect(n).not.toContain("HEARTBEAT_OK")}),it("does NOT drop normal assistant text that happens to contain HEARTBEAT as substring",()=>{const t={stream:"assistant",runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,ts:Date.now(),data:{delta:"Let's design a HEARTBEAT_OK-like ack for our API."}};e.events.handleAgentEvent(t);const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(n).toHaveLength(1),expect(n[0].content.text).toContain("HEARTBEAT_OK-like")}),it("drops chat.text heartbeat when no agent.assistant stream is active",()=>{const t={runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,message:{role:"assistant",content:[{type:"text",text:"HEARTBEAT_OK"}],timestamp:Date.now()}};e.events.handleChatEvent(t);const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(n).toHaveLength(0)}),it("does NOT drop a real chat.text that only mentions the token inline",()=>{const t={runId:"run-heartbeat-1",sessionKey:"main/s1",seq:1,message:{role:"assistant",content:[{type:"text",text:"Use the sentinel HEARTBEAT_OK in your ack handler."}],timestamp:Date.now()}};e.events.handleChatEvent(t);const n=e.sendSessionUpdate.mock.calls.map(e=>e[1]).filter(e=>"agent_message_chunk"===e.sessionUpdate);expect(n).toHaveLength(1)})});
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{Errors}from"../errors/index.js";import{formatFriendlyError}from"../errors/classify.js";import{stripTransportMetadata,normalizeTransportText,isHeartbeatOnlyText}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 s&&void 0===e._turnId&&(e._turnId=s),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;if(isHeartbeatOnlyText(s)){this.logger.debug?.(`[gateway-events] SKIP heartbeat chat.text runId=${t.runId} len=${s.length}`);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 a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a;isHeartbeatOnlyText(n)?this.logger.debug?.(`[gateway-events] SKIP heartbeat assistant delta runId=${t.runId} len=${a.length}`):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...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)return;const e=t??{},s="string"==typeof e.sessionKey?e.sessionKey:void 0,o=s?this.state.findSessionByKey(s)?.sessionId:void 0,n={...e};s&&null==n.sessionKey&&(n.sessionKey=s),o&&(n.sessionId=o);const a=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:n})}).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,classifyHeartbeatFilter,previewFilteredText}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 s&&void 0===e._turnId&&(e._turnId=s),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=classifyHeartbeatFilter(s);if(o.filtered){this.logHeartbeatSkip({source:"chat-text",reason:o.reason,sessionId:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const d={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,d),e.assistantMq?.push(d),this.onStreamDelta?.(e,d)}}}}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 a=normalizeTransportText(n);if(a){const n=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(n);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:n,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...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)return;const e=t??{},s="string"==typeof e.sessionKey?e.sessionKey:void 0,o=s?this.state.findSessionByKey(s)?.sessionId:void 0,n={...e};s&&null==n.sessionKey&&(n.sessionKey=s),o&&(n.sessionId=o);const a=`${this.config.agentGatewayUrl}/api/v1/agent/events`;this.agentFetchFn(a,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:n})}).catch(t=>{this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)})}logHeartbeatSkip(t){const e=previewFilteredText(t.cumulative),s=void 0!==t.delta?previewFilteredText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}
@@ -0,0 +1 @@
1
+ import{listMcpServers,setMcpServer,unsetMcpServer,OpenClawCliError}from"../openclaw-cli.js";import{Errors}from"../errors/index.js";const RESTART_DEBOUNCE_MS=750;export class McpService{transport;logger;state;exit;restartTimer;constructor(r){this.transport=r.transport,this.logger=r.logger,this.state=r.state,this.exit=r.exit??(r=>process.exit(r))}async handleList(r){try{const e=await listMcpServers();this.transport.sendResult(r.id,{servers:e})}catch(e){this.logger.warn?.(`[mcp-service] list failed: ${e instanceof Error?e.message:e}`),this.sendMappedError(r,e,!0)}}async handleSet(r){const e=r.params,s=e?.name,t=e?.server;if(s&&t&&"object"==typeof t){if(!this.rejectIfInFlight(r))try{await setMcpServer(s,t),this.logger.info?.(`[mcp-service] set applied name=${s}`),this.transport.sendResult(r.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(e){this.logger.error?.(`[mcp-service] set failed name=${s}: ${e instanceof Error?e.message:e}`),this.sendMappedError(r,e,!1)}}else this.transport.sendAcpError(r.id,Errors.InvalidParams,{message:"Missing 'name' or 'server' config"})}async handleUnset(r){const e=r.params,s=e?.name;if(s){if(!this.rejectIfInFlight(r))try{await unsetMcpServer(s),this.logger.info?.(`[mcp-service] unset applied name=${s}`),this.transport.sendResult(r.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(e){this.logger.error?.(`[mcp-service] unset failed name=${s}: ${e instanceof Error?e.message:e}`),this.sendMappedError(r,e,!1)}}else this.transport.sendAcpError(r.id,Errors.InvalidParams,{message:"Missing 'name'"})}hasInFlightPrompts(){for(const r of this.state.sessions.values())if(void 0!==r.pendingPromptId||void 0!==r.currentRunId)return!0;return!1}rejectIfInFlight(r){return!!this.hasInFlightPrompts()&&(this.transport.sendError(r.id,Errors.InvalidParams.rpcCode,"Agent is busy",{code:Errors.InvalidParams.code,reason:"PROMPT_IN_FLIGHT",message:"Agent is processing a message; please retry after it completes."}),!0)}scheduleRestart(){this.restartTimer&&clearTimeout(this.restartTimer),this.restartTimer=setTimeout(()=>{this.logger.info?.("[mcp-service] mcp change applied — exiting for container restart"),this.exit(0)},750)}sendMappedError(r,e,s){const t=cliErrToAcpError(e,s);this.transport.sendError(r.id,t.code,t.message,t.data)}}export function cliErrToAcpError(r,e){if(r instanceof OpenClawCliError){const s=r.stderr??"";return null===r.exitCode||e?{code:Errors.InternalError.rpcCode,message:r.message,data:{code:Errors.InternalError.code}}:/No MCP server named/i.test(s)?{code:Errors.InvalidParams.rpcCode,message:r.message,data:{code:Errors.InvalidParams.code,reason:"NOT_FOUND"}}:{code:Errors.InvalidParams.rpcCode,message:r.message,data:{code:Errors.InvalidParams.code}}}return{code:Errors.InternalError.rpcCode,message:r instanceof Error?r.message:String(r),data:{code:Errors.InternalError.code}}}
@@ -0,0 +1 @@
1
+ import{describe,it,expect,beforeEach,afterEach,vi}from"vitest";vi.mock("../openclaw-cli.js",()=>{class e extends Error{exitCode;stderr;constructor(e,t,r){super(e),this.exitCode=t,this.stderr=r,this.name="OpenClawCliError"}}return{OpenClawCliError:e,listMcpServers:vi.fn(),setMcpServer:vi.fn(),unsetMcpServer:vi.fn()}});import{listMcpServers as mockedList,setMcpServer as mockedSet,unsetMcpServer as mockedUnset,OpenClawCliError}from"../openclaw-cli.js";import{McpService,cliErrToAcpError}from"./mcp-service.js";import{Errors}from"../errors/index.js";import{AcpGatewaySessionState}from"./session-state.js";function makeTransport(){const e=[];return{transport:{sendResult(t,r){e.push({kind:"result",id:t,result:r})},sendError(t,r,s,o){e.push({kind:"error",id:t,code:r,message:s,data:o})},sendAcpError(t,r,s){const o={code:r.code};s?.param&&(o.param=s.param),e.push({kind:"error",id:t,code:r.rpcCode,message:s?.message??r.message,data:o})},sendSessionUpdate:()=>{},sendNotification:()=>{}},sent:e}}function makeLogger(){return{info:vi.fn(),warn:vi.fn(),debug:vi.fn(),error:vi.fn()}}function makeSession(e,t={}){return{sessionId:e,gatewaySessionKey:`agent:main:${e}`,agentId:"main",createdAt:Date.now(),chatReplayDedupKeys:new Set,...t}}function makeService(){const{transport:e,sent:t}=makeTransport(),r=makeLogger(),s=new AcpGatewaySessionState(r),o=vi.fn();return{svc:new McpService({transport:e,logger:r,state:s,exit:o}),sent:t,state:s,exit:o}}const setReq=(e,t="foo",r={transport:"sse",url:"https://x"})=>({jsonrpc:"2.0",id:e,method:"mcp/set",params:{name:t,server:r}}),unsetReq=(e,t="foo")=>({jsonrpc:"2.0",id:e,method:"mcp/unset",params:{name:t}});beforeEach(()=>{vi.useFakeTimers(),vi.mocked(mockedList).mockReset(),vi.mocked(mockedSet).mockReset(),vi.mocked(mockedUnset).mockReset()}),afterEach(()=>{vi.useRealTimers()}),describe("McpService.handleSet",()=>{it("writes via CLI, replies {ok, restarting:true} and schedules one exit(0) after 750ms",async()=>{const{svc:e,sent:t,exit:r}=makeService();vi.mocked(mockedSet).mockResolvedValue(void 0),await e.handleSet(setReq(1)),expect(vi.mocked(mockedSet)).toHaveBeenCalledTimes(1),expect(t).toEqual([{kind:"result",id:1,result:{ok:!0,restarting:!0}}]),expect(r).not.toHaveBeenCalled(),vi.advanceTimersByTime(749),expect(r).not.toHaveBeenCalled(),vi.advanceTimersByTime(1),expect(r).toHaveBeenCalledTimes(1),expect(r).toHaveBeenCalledWith(0)}),it("rejects with InvalidParams when name or server missing",async()=>{const{svc:e,sent:t,exit:r}=makeService();await e.handleSet({jsonrpc:"2.0",id:1,method:"mcp/set",params:{name:"foo"}}),expect(vi.mocked(mockedSet)).not.toHaveBeenCalled(),expect(r).not.toHaveBeenCalled(),expect(t).toHaveLength(1),expect(t[0].kind).toBe("error"),expect(t[0].code).toBe(Errors.InvalidParams.rpcCode)})}),describe("McpService debounce",()=>{it("coalesces multiple rapid set/unset successes into a single exit(0)",async()=>{const{svc:e,exit:t}=makeService();vi.mocked(mockedSet).mockResolvedValue(void 0),vi.mocked(mockedUnset).mockResolvedValue(void 0),await e.handleSet(setReq(1,"a")),vi.advanceTimersByTime(300),await e.handleSet(setReq(2,"b")),vi.advanceTimersByTime(300),await e.handleUnset(unsetReq(3,"a")),vi.advanceTimersByTime(749),expect(t).not.toHaveBeenCalled(),vi.advanceTimersByTime(1),expect(t).toHaveBeenCalledTimes(1)})}),describe("McpService in-flight prompt gate",()=>{it("rejects handleSet with PROMPT_IN_FLIGHT when a session has a pending prompt",async()=>{const{svc:e,state:t,sent:r,exit:s}=makeService(),o=makeSession("s1",{pendingPromptId:42});t.sessions.set("s1",o),await e.handleSet(setReq(1)),expect(vi.mocked(mockedSet)).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),expect(r).toHaveLength(1);const a=r[0];expect(a.kind).toBe("error"),expect(a.code).toBe(Errors.InvalidParams.rpcCode),expect(a.data).toMatchObject({reason:"PROMPT_IN_FLIGHT"})}),it("rejects handleUnset with PROMPT_IN_FLIGHT when a session has an active run",async()=>{const{svc:e,state:t,sent:r}=makeService();t.sessions.set("s1",makeSession("s1",{currentRunId:"run-1"})),await e.handleUnset(unsetReq(7)),expect(vi.mocked(mockedUnset)).not.toHaveBeenCalled();const s=r.at(-1);expect(s.kind).toBe("error"),expect(s.data).toMatchObject({reason:"PROMPT_IN_FLIGHT"})}),it("allows writes when all sessions are idle",async()=>{const{svc:e,state:t,exit:r}=makeService();vi.mocked(mockedSet).mockResolvedValue(void 0),t.sessions.set("s1",makeSession("s1")),t.sessions.set("s2",makeSession("s2")),await e.handleSet(setReq(1)),expect(vi.mocked(mockedSet)).toHaveBeenCalledTimes(1),vi.advanceTimersByTime(750),expect(r).toHaveBeenCalledWith(0)})}),describe("cliErrToAcpError",()=>{it("maps null exit code (spawn/timeout) to InternalError regardless of readOnly",()=>{const e=new OpenClawCliError("spawn failed",null,"");expect(cliErrToAcpError(e,!1).code).toBe(Errors.InternalError.rpcCode),expect(cliErrToAcpError(e,!0).code).toBe(Errors.InternalError.rpcCode)}),it("treats any CLI error as InternalError when readOnly=true",()=>{const e=new OpenClawCliError("bad schema",1,"zod: invalid url"),t=cliErrToAcpError(e,!0);expect(t.code).toBe(Errors.InternalError.rpcCode)}),it("maps 'No MCP server named' stderr to InvalidParams + reason=NOT_FOUND",()=>{const e=new OpenClawCliError("openclaw CLI exited with code 1",1,'No MCP server named "foo" in /data/openclaw.json.'),t=cliErrToAcpError(e,!1);expect(t.code).toBe(Errors.InvalidParams.rpcCode),expect(t.data).toMatchObject({reason:"NOT_FOUND"})}),it("maps generic non-zero CLI exit to InvalidParams without reason",()=>{const e=new OpenClawCliError("openclaw CLI exited with code 1",1,"zod: invalid url"),t=cliErrToAcpError(e,!1);expect(t.code).toBe(Errors.InvalidParams.rpcCode),expect(t.data.reason).toBeUndefined()}),it("maps non-CLI errors to InternalError",()=>{const e=cliErrToAcpError(new Error("network blip"),!1);expect(e.code).toBe(Errors.InternalError.rpcCode),expect(e.message).toBe("network blip")})}),describe("McpService.handleList",()=>{it("returns {servers} on success",async()=>{const{svc:e,sent:t,exit:r}=makeService();vi.mocked(mockedList).mockResolvedValue({foo:{transport:"sse",url:"https://x"}}),await e.handleList({jsonrpc:"2.0",id:9,method:"mcp/list"}),expect(t).toEqual([{kind:"result",id:9,result:{servers:{foo:{transport:"sse",url:"https://x"}}}}]),expect(r).not.toHaveBeenCalled()}),it("maps CLI failure to InternalError (read-only path)",async()=>{const{svc:e,sent:t}=makeService();vi.mocked(mockedList).mockRejectedValue(new OpenClawCliError("openclaw CLI exited with code 1",1,"parse error")),await e.handleList({jsonrpc:"2.0",id:10,method:"mcp/list"});const r=t.at(-1);expect(r.kind).toBe("error"),expect(r.code).toBe(Errors.InternalError.rpcCode)})});
@@ -0,0 +1 @@
1
+ import{describe,it,expect}from"vitest";import{AcpGatewayBridgeCore}from"./bridge-core.js";import{MessageQueue}from"../utils/mq.js";function mkSession(s){return{...s}}function openMq(){return new MessageQueue}function closedMq(){const s=new MessageQueue;return s.close(),s}const assistantTail=[{role:"user"},{role:"assistant"}],userTail=[{role:"assistant"},{role:"user"}],empty=[];describe("shouldReplayAssistantMq — Bug-AV invariant",()=>{it("SKIPS replay when mq is in late-close window AND history has tail assistant",()=>{const s=mkSession({assistantMq:openMq(),assistantMqAwaitingRunId:"run-42",currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!1)}),it("REPLAYS for an active run (currentRunId set) — history does not yet cover the in-progress turn",()=>{const s=mkSession({assistantMq:openMq(),currentRunId:"run-active",assistantMqAwaitingRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!0)}),it("REPLAYS when mq is in late-close but history has NO tail assistant (defensive fallback)",()=>{const s=mkSession({assistantMq:openMq(),assistantMqAwaitingRunId:"run-42",currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,userTail)).toBe(!0),expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,empty)).toBe(!0)}),it("DOES NOT replay when mq is closed — regardless of history shape",()=>{const s=mkSession({assistantMq:closedMq(),assistantMqAwaitingRunId:"run-42",currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!1),expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,empty)).toBe(!1)}),it("DOES NOT replay when mq is undefined",()=>{const s=mkSession({assistantMq:void 0,assistantMqAwaitingRunId:"run-42",currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!1)}),it("REPLAYS when mq is open, no active run, no late-close (legacy / non-tracked armed path)",()=>{const s=mkSession({assistantMq:openMq(),assistantMqAwaitingRunId:void 0,currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!0),expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,userTail)).toBe(!0)}),it("active run PRECEDENCE: even if assistantMqAwaitingRunId is somehow set, currentRunId wins — never skip an in-flight turn",()=>{const s=mkSession({assistantMq:openMq(),assistantMqAwaitingRunId:"run-old",currentRunId:"run-new"});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,assistantTail)).toBe(!0)}),it("history tail-assistant check scans backwards past trailing system turns",()=>{const s=mkSession({assistantMq:openMq(),assistantMqAwaitingRunId:"run-42",currentRunId:void 0});expect(AcpGatewayBridgeCore.shouldReplayAssistantMq(s,[{role:"user"},{role:"assistant"},{role:"system"}])).toBe(!1)})});
@@ -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("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.core.refreshCronIsolatedSessionKeys()}),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,t.channelId,t.inboundMessageId).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,a){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const r=this.config.gateway.agentId||this.core.getDefaultAgentId();let o,c;if(s?(o=this.state.findSessionByKey(s),c=s,o?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${s} sid=${o.sessionId}`):(o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: created session for key=${s} sid=${o.sessionId}`))):(c=`agent:${r}:a2a:${randomUUID()}`,o=this.core.ensureSessionForKey(c),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created A2A session key=${c}`)),i&&o&&(o.channelId=i),a&&o&&(o.inboundMessageId=a),o&&!o.promptDone&&(void 0!==o.pendingPromptId||void 0!==o.currentRunId||void 0!==o.gatewayRequestId))return(o.pendingAgentSends??=[]).push({message:e,attachments:t,inputProvenance:n,channelId:i,inboundMessageId:a}),void this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",o.pendingAgentSends.length);const l=await this.gateway.agentSend(c,e,r,t,n);return l?.runId&&(o?(this.state.resetForNewRun(o,o.sessionId,this.transport),o.currentRunId=l.runId,o.assistantStreamStartedAt=Date.now(),this.state.promptsByRunId.set(l.runId,o)):this.state.promptsByRunId.set(l.runId,{gatewaySessionKey:c,channelId:i,inboundMessageId:a})),l}setMessageSdk(e,t){this.messageSdk=e,this.msgPrincipalId=t}async publishAgentReply(e,t,s,n){const i=this.messageSdk,a=this.msgPrincipalId;if(i&&a){if(e&&i.sendToChannel)try{return await i.sendToChannel(e,s,n,{inReplyTo:t}),void this.logger.info?.(`[acp-server] ${s} published channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendToChannel failed channel=${e}: ${t instanceof Error?t.message:String(t)}; attempting persistent fallback`)}if(e&&i.sendChannelMessage)try{return await i.sendChannelMessage(e,{type:s,payload:n,inReplyTo:t,senderId:a}),void this.logger.info?.(`[acp-server] ${s} persisted channel=${e} in_reply_to=${t??"-"}`)}catch(t){this.logger.warn?.(`[acp-server] sendChannelMessage failed channel=${e}: ${t instanceof Error?t.message:String(t)}; falling back to personal channel`)}await i.sendToPersonal(a,s,n)}}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,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunComplete runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"} principal=${this.msgPrincipalId} hasSDK=${!!this.messageSdk}`),!n)return void this.logger.warn?.(`[acp-server] onRunComplete: no session_key for runId=${e}, cannot publish reply`);const r={session_key:n,text:t};this.publishAgentReply(i,a,"agent_reply",r).catch(e=>this.logger.warn?.(`[acp-server] agent_reply publish failed: ${e instanceof Error?e.message:JSON.stringify(e)}`))},this.core.onRunFailed=(e,t)=>{const s=this.state.promptsByRunId.get(e),n=s?.gatewaySessionKey,i=s?.channelId,a=s?.inboundMessageId;if(this.logger.info?.(`[acp-server] onRunFailed runId=${e} session_key=${n} channel=${i} in_reply_to=${a??"-"}`),!n)return;const r={session_key:n,error:t||"execution failed"};this.publishAgentReply(i,a,"agent_reply",r).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.core.refreshCronIsolatedSessionKeys()}),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"})}}
@@ -0,0 +1 @@
1
+ export const HEARTBEAT_TOKEN="HEARTBEAT_OK";export const SILENT_REPLY_TOKEN="NO_REPLY";function escapeRegExp(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}const silentExactRegexByToken=new Map,silentTrailingRegexByToken=new Map,silentLeadingAttachedRegexByToken=new Map,silentLeadingRegexByToken=new Map;function getSilentExactRegex(e){const t=silentExactRegexByToken.get(e);if(t)return t;const i=new RegExp(`^\\s*${escapeRegExp(e)}\\s*$`,"i");return silentExactRegexByToken.set(e,i),i}function getSilentTrailingRegex(e){const t=silentTrailingRegexByToken.get(e);if(t)return t;const i=new RegExp(`(?:^|\\s+|\\*+)${escapeRegExp(e)}\\s*$`,"i");return silentTrailingRegexByToken.set(e,i),i}function getSilentLeadingAttachedRegex(e){const t=silentLeadingAttachedRegexByToken.get(e);if(t)return t;const i=escapeRegExp(e),n=new RegExp(`^\\s*(?:${i}\\s+)*${i}(?=[\\p{L}\\p{N}])`,"iu");return silentLeadingAttachedRegexByToken.set(e,n),n}function getSilentLeadingRegex(e){const t=silentLeadingRegexByToken.get(e);if(t)return t;const i=new RegExp(`^(?:\\s*${escapeRegExp(e)})+\\s*`,"i");return silentLeadingRegexByToken.set(e,i),i}export function isSilentReplyText(e,t="NO_REPLY"){return!!e&&getSilentExactRegex(t).test(e)}export function isSilentReplyEnvelopeText(e,t="NO_REPLY"){if(!e)return!1;const i=e.trim();if(!(i&&i.startsWith("{")&&i.endsWith("}")&&i.includes(t)))return!1;try{const e=JSON.parse(i);if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e,r=Object.keys(n);return 1===r.length&&"action"===r[0]&&"string"==typeof n.action&&n.action.trim()===t}catch{return!1}}export function isSilentReplyPayloadText(e,t="NO_REPLY"){return isSilentReplyText(e,t)||isSilentReplyEnvelopeText(e,t)}export function stripSilentToken(e,t="NO_REPLY"){return e.replace(getSilentTrailingRegex(t),"").trim()}export function stripLeadingSilentToken(e,t="NO_REPLY"){return e.replace(getSilentLeadingRegex(t),"").trim()}export function startsWithSilentToken(e,t="NO_REPLY"){return!!e&&getSilentLeadingAttachedRegex(t).test(e)}export function isSilentReplyPrefixText(e,t="NO_REPLY"){if(!e)return!1;const i=e.trimStart();if(!i)return!1;if(i!==i.toUpperCase())return!1;const n=i.toUpperCase();if(!n)return!1;if(n.length<2)return!1;if(/[^A-Z_]/.test(n))return!1;const r=t.toUpperCase();return!!r.startsWith(n)&&(!!n.includes("_")||"NO_REPLY"===r&&"NO"===n)}const HEARTBEAT_TOKEN_AT_END_RE=new RegExp(`${escapeRegExp("HEARTBEAT_OK")}[^\\w]{0,4}$`);function stripTokenAtEdges(e){let t=e.trim();if(!t)return{text:"",didStrip:!1};if(!t.includes("HEARTBEAT_OK"))return{text:t,didStrip:!1};let i=!1,n=!0;for(;n;){n=!1;const e=t.trim();if(e.startsWith("HEARTBEAT_OK"))t=e.slice(12).trimStart(),i=!0,n=!0;else if(HEARTBEAT_TOKEN_AT_END_RE.test(e)){const r=e.lastIndexOf("HEARTBEAT_OK"),s=e.slice(0,r).trimEnd();t=s?`${s}${e.slice(r+12).trimStart()}`.trimEnd():"",i=!0,n=!0}}return{text:t.replace(/\s+/g," ").trim(),didStrip:i}}function stripHeartbeatMarkup(e){return e.replace(/<[^>]*>/g," ").replace(/&nbsp;/gi," ").replace(/^[*`~_]+/,"").replace(/[*`~_]+$/,"")}export function stripHeartbeatToken(e,t={}){if(!e)return{shouldSkip:!0,text:"",didStrip:!1};const i=e.trim();if(!i)return{shouldSkip:!0,text:"",didStrip:!1};const n=t.mode??"message",r=t.maxAckChars,s="string"==typeof r?Number(r):r,p=Math.max(0,"number"==typeof s&&Number.isFinite(s)?s:300),o=stripHeartbeatMarkup(i);if(!i.includes("HEARTBEAT_OK")&&!o.includes("HEARTBEAT_OK"))return{shouldSkip:!1,text:i,didStrip:!1};const a=stripTokenAtEdges(i),l=stripTokenAtEdges(o),c=a.didStrip&&a.text?a:l;if(!c.didStrip)return{shouldSkip:!1,text:i,didStrip:!1};if(!c.text)return{shouldSkip:!0,text:"",didStrip:!0};const x=c.text.trim();return"heartbeat"===n&&x.length<=p?{shouldSkip:!0,text:"",didStrip:!0}:{shouldSkip:!1,text:x,didStrip:!0}}export function normalizeStreamingText(e,t={}){if(!e)return{skip:!0};let i=e;if(!t.isHeartbeat&&i.includes("HEARTBEAT_OK")){const e=stripHeartbeatToken(i,{mode:"message"});if(e.didStrip&&t.onHeartbeatStrip?.(),e.shouldSkip)return{skip:!0};i=e.text}return isSilentReplyText(i,"NO_REPLY")||isSilentReplyPrefixText(i,"NO_REPLY")||isSilentReplyPrefixText(i,"HEARTBEAT_OK")?{skip:!0}:(i&&startsWithSilentToken(i,"NO_REPLY")&&(i=stripLeadingSilentToken(i,"NO_REPLY")),i?{skip:!1,text:i}:{skip:!0})}
@@ -0,0 +1 @@
1
+ import{describe,it,expect}from"vitest";import{HEARTBEAT_TOKEN,SILENT_REPLY_TOKEN,isSilentReplyText,isSilentReplyEnvelopeText,isSilentReplyPayloadText,isSilentReplyPrefixText,startsWithSilentToken,stripLeadingSilentToken,stripSilentToken,stripHeartbeatToken,normalizeStreamingText}from"./auto-reply-tokens.js";describe("constants",()=>{it("exports upstream token strings verbatim",()=>{expect(HEARTBEAT_TOKEN).toBe("HEARTBEAT_OK"),expect(SILENT_REPLY_TOKEN).toBe("NO_REPLY")})}),describe("isSilentReplyText",()=>{it("matches exact token (with trimming, case-insensitive)",()=>{expect(isSilentReplyText("NO_REPLY")).toBe(!0),expect(isSilentReplyText(" NO_REPLY ")).toBe(!0),expect(isSilentReplyText("no_reply")).toBe(!0)}),it("rejects token with adjacent text",()=>{expect(isSilentReplyText("NO_REPLY please")).toBe(!1),expect(isSilentReplyText("")).toBe(!1),expect(isSilentReplyText(void 0)).toBe(!1)})}),describe("isSilentReplyEnvelopeText",()=>{it('matches canonical JSON envelope `{"action":"NO_REPLY"}`',()=>{expect(isSilentReplyEnvelopeText('{"action":"NO_REPLY"}')).toBe(!0),expect(isSilentReplyEnvelopeText(' { "action": "NO_REPLY" } ')).toBe(!0)}),it("rejects envelopes with extra keys or non-string action",()=>{expect(isSilentReplyEnvelopeText('{"action":"NO_REPLY","extra":1}')).toBe(!1),expect(isSilentReplyEnvelopeText('{"action":123}')).toBe(!1),expect(isSilentReplyEnvelopeText("not json")).toBe(!1),expect(isSilentReplyEnvelopeText("{broken json")).toBe(!1)})}),describe("isSilentReplyPayloadText",()=>{it("accepts both bare token and envelope",()=>{expect(isSilentReplyPayloadText("NO_REPLY")).toBe(!0),expect(isSilentReplyPayloadText('{"action":"NO_REPLY"}')).toBe(!0),expect(isSilentReplyPayloadText("something else")).toBe(!1)})}),describe("isSilentReplyPrefixText — the critical gate for streamed prefixes",()=>{it("flags NO_REPLY prefix containing `_`",()=>{expect(isSilentReplyPrefixText("NO_")).toBe(!0),expect(isSilentReplyPrefixText("NO_R")).toBe(!0),expect(isSilentReplyPrefixText("NO_REP")).toBe(!0),expect(isSilentReplyPrefixText("NO_REPLY")).toBe(!0)}),it("flags bare `NO` for NO_REPLY (upstream legacy)",()=>{expect(isSilentReplyPrefixText("NO")).toBe(!0)}),it("flags HEARTBEAT_OK prefixes with explicit token arg",()=>{expect(isSilentReplyPrefixText("HEARTBEAT_",HEARTBEAT_TOKEN)).toBe(!0),expect(isSilentReplyPrefixText("HEARTBEAT_O",HEARTBEAT_TOKEN)).toBe(!0)}),it("does NOT flag HEARTBEAT prefixes lacking `_` (upstream trade-off)",()=>{expect(isSilentReplyPrefixText("HEART",HEARTBEAT_TOKEN)).toBe(!1),expect(isSilentReplyPrefixText("HEARTBEAT",HEARTBEAT_TOKEN)).toBe(!1)}),it("requires strict uppercase A-Z_",()=>{expect(isSilentReplyPrefixText("heartbeat_",HEARTBEAT_TOKEN)).toBe(!1),expect(isSilentReplyPrefixText("HEART-BEAT",HEARTBEAT_TOKEN)).toBe(!1),expect(isSilentReplyPrefixText("HEART BEAT",HEARTBEAT_TOKEN)).toBe(!1),expect(isSilentReplyPrefixText("HEART123",HEARTBEAT_TOKEN)).toBe(!1)}),it("requires length >= 2",()=>{expect(isSilentReplyPrefixText("H",HEARTBEAT_TOKEN)).toBe(!1),expect(isSilentReplyPrefixText("N")).toBe(!1)}),it("tolerates leading whitespace, rejects empty",()=>{expect(isSilentReplyPrefixText(" NO_","NO_REPLY")).toBe(!0),expect(isSilentReplyPrefixText("","NO_REPLY")).toBe(!1),expect(isSilentReplyPrefixText(void 0)).toBe(!1)})}),describe("startsWithSilentToken",()=>{it("detects token glued directly to visible content",()=>{expect(startsWithSilentToken("NO_REPLYhello")).toBe(!0),expect(startsWithSilentToken("NO_REPLY NO_REPLYthing")).toBe(!0)}),it("does NOT match when token is space-separated (that's stripSilentToken's job)",()=>{expect(startsWithSilentToken("NO_REPLY hello")).toBe(!1)}),it("rejects unrelated text",()=>{expect(startsWithSilentToken("hello NO_REPLY")).toBe(!1),expect(startsWithSilentToken("")).toBe(!1)})}),describe("stripSilentToken",()=>{it("removes a trailing silent token",()=>{expect(stripSilentToken("see ya NO_REPLY")).toBe("see ya")}),it("leaves embedded tokens alone",()=>{expect(stripSilentToken("NO_REPLY is our sentinel")).toBe("NO_REPLY is our sentinel")})}),describe("stripLeadingSilentToken",()=>{it("strips one or more leading tokens with optional space",()=>{expect(stripLeadingSilentToken("NO_REPLY hello")).toBe("hello"),expect(stripLeadingSilentToken("NO_REPLY NO_REPLY hello")).toBe("hello")}),it("leaves content untouched when no leading token",()=>{expect(stripLeadingSilentToken("hello NO_REPLY")).toBe("hello NO_REPLY")})}),describe("stripHeartbeatToken — OpenClaw 1:1 behaviour",()=>{it("returns shouldSkip when input is empty or whitespace",()=>{expect(stripHeartbeatToken("")).toEqual({shouldSkip:!0,text:"",didStrip:!1}),expect(stripHeartbeatToken(" ")).toEqual({shouldSkip:!0,text:"",didStrip:!1})}),it("passes through text that does not contain the token",()=>{const e=stripHeartbeatToken("hello world");expect(e.didStrip).toBe(!1),expect(e.shouldSkip).toBe(!1),expect(e.text).toBe("hello world")}),it("skips when text is exactly the token",()=>{const e=stripHeartbeatToken("HEARTBEAT_OK");expect(e).toEqual({shouldSkip:!0,text:"",didStrip:!0})}),it("strips token with trailing punctuation; rest may be a bare punct residue",()=>{expect(stripHeartbeatToken("HEARTBEAT_OK.").didStrip).toBe(!0),expect(stripHeartbeatToken("HEARTBEAT_OK!!!").didStrip).toBe(!0),expect(stripHeartbeatToken("HEARTBEAT_OK ✅").didStrip).toBe(!0)}),it("strips token wrapped in markdown markup via the normalised-markup fallback",()=>{expect(stripHeartbeatToken("**HEARTBEAT_OK**").didStrip).toBe(!0),expect(stripHeartbeatToken("`HEARTBEAT_OK`").didStrip).toBe(!0)}),it("fully skips when residue is empty (bare token with no padding)",()=>{expect(stripHeartbeatToken("HEARTBEAT_OK").shouldSkip).toBe(!0),expect(stripHeartbeatToken(" HEARTBEAT_OK ").shouldSkip).toBe(!0)}),it("keeps leading token + content, reporting didStrip",()=>{const e=stripHeartbeatToken("HEARTBEAT_OK the rest of my reply");expect(e.didStrip).toBe(!0),expect(e.shouldSkip).toBe(!1),expect(e.text).toBe("the rest of my reply")}),it("keeps trailing content when token is at the end but with content before",()=>{const e=stripHeartbeatToken("important note HEARTBEAT_OK");expect(e.didStrip).toBe(!0),expect(e.shouldSkip).toBe(!1),expect(e.text).toBe("important note")}),it("collapses internal whitespace when stripping",()=>{const e=stripHeartbeatToken("HEARTBEAT_OK\n\n more\n\ntext");expect(e.didStrip).toBe(!0),expect(e.text).toBe("more text")}),it("heartbeat-mode skips short trailing chatter under maxAckChars",()=>{const e=stripHeartbeatToken("HEARTBEAT_OK short tail",{mode:"heartbeat"});expect(e.shouldSkip).toBe(!0),expect(e.text).toBe(""),expect(e.didStrip).toBe(!0)}),it("heartbeat-mode keeps long trailing content beyond maxAckChars",()=>{const e="x".repeat(400),t=stripHeartbeatToken(`HEARTBEAT_OK ${e}`,{mode:"heartbeat",maxAckChars:300});expect(t.shouldSkip).toBe(!1),expect(t.text).toBe(e)})}),describe("normalizeStreamingText — the full streaming pipeline",()=>{it("skips empty input",()=>{expect(normalizeStreamingText("")).toEqual({skip:!0}),expect(normalizeStreamingText(void 0)).toEqual({skip:!0})}),it("skips the full heartbeat token",()=>{expect(normalizeStreamingText("HEARTBEAT_OK")).toEqual({skip:!0})}),it("skips any HEARTBEAT_OK prefix (ghost-bubble protection)",()=>{expect(normalizeStreamingText("HEARTBEAT_")).toEqual({skip:!0}),expect(normalizeStreamingText("HEARTBEAT_O")).toEqual({skip:!0})}),it("skips NO_REPLY + its uppercase prefixes",()=>{expect(normalizeStreamingText("NO_REPLY")).toEqual({skip:!0}),expect(normalizeStreamingText("NO_")).toEqual({skip:!0}),expect(normalizeStreamingText("NO")).toEqual({skip:!0})}),it("strips leading glued silent tokens and keeps remainder",()=>{const e=normalizeStreamingText("NO_REPLYhello world");expect(e.skip).toBe(!1),expect(e.text).toBe("hello world")}),it("passes real content through untouched",()=>{expect(normalizeStreamingText("Hello user!")).toEqual({skip:!1,text:"Hello user!"})}),it("does NOT skip when isHeartbeat flag is set (heartbeat run consumes its own ack)",()=>{const e=normalizeStreamingText("HEARTBEAT_OK extra",{isHeartbeat:!0});expect(e.skip).toBe(!1),expect(e.text).toBe("HEARTBEAT_OK extra")}),it("invokes onHeartbeatStrip callback the first time",()=>{const e=[];normalizeStreamingText("HEARTBEAT_OK hi",{onHeartbeatStrip:()=>e.push(1)}),expect(e).toEqual([1])})});
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,_initializing=!1,_pluginRegisteredAt=0;function initializePlugin(e){let o,r;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{r=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const a=r.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=a.log.enabled?createObsEventWriter({dir:a.log.dir,redaction:{gatewayToken:a.gateway.token},logger:e.logger}):()=>{};s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:a,sources:r.sources,openclawConfigPath:r.openclawConfigPath}});for(const o of r.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(r.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),a.keyFile)try{agentKeys=loadAgentKeyPair(a.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&a.agentGatewayUrl&&(setUploadDeps({gatewayUrl:a.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:a,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,r){const{cfg:a,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,r,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(r)}`);const n=await executeUpload(r,a,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async r=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,r),await cleanupUploadToolResultCache()}catch(r){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(r)}`)}return r})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,r){await handleFileStage(e,r,o.cfg)}})}function registerServices(e,o,r){const{cfg:a,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(a,e.logger,t,n,i,s);r.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),r=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${r}ms, bridge.url=${a.bridge.url||"(empty)"}`),!a.bridge.url&&a.agentGatewayUrl&&n){const o=Date.now(),r=await discoverBridgeFromGW(a.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${r?.url??"null"}`),r)a.bridge.url=r.url,writeDiscoveryCache({bridge:{url:r.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${r.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(a.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(a.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:f,a2aEnabled:y}=a.messageBus;let u;if(a.agentGatewayUrl&&agentKeys){const e=a.agentGatewayUrl,o=agentKeys;u=async r=>{const a=await agentFetch(`${e}/api/v1/messaging/token`,o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:r})});if(!a.ok){const e=await a.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${a.status}: ${e}`)}return a.json()}}if(d&&(u||a.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:u?void 0:a.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const o=f,r=async o=>{c.onceConnected(()=>{l.setMessageSdk(c,o),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,u?"gateway":"direct")}),await c.connect(o,{centrifugoUrl:p,tokenProvider:u})};e.registerService({id:"beeos-message-bus",async start(){let a=o;if(a||(a=process.env.BEEOS_PRINCIPAL_ID||""),!a&&l)try{await l.waitForAgentMapping(),a=l.getDefaultPlatformAgentId()||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}a?await r(a):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgentId();o?r(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}}),c.onMessage("agent_request",async o=>{const r=o.payload,a=o.metadata,t="string"==typeof r?.message?r.message:"",s=Array.isArray(r?.files)?r.files:void 0,n=o.channel_id??a?.channel_id??("string"==typeof r?.channel_id?r.channel_id:void 0),i=n&&n.length>0?n:void 0;if(!t)return;const g=("string"==typeof r?.context_id&&r.context_id.length>0?r.context_id:void 0)??(a?.context_id&&a.context_id.length>0?a.context_id:void 0);let c="string"==typeof r?.session_key?r.session_key:void 0;if(!c){const e="string"==typeof r?.channel?r.channel:"a2a";c=`agent:${l.getLocalAgentId()}:${e}:${g?`ctx-${g}`:randomUUID()}`}e.logger.debug?.("[beeos-claw] agent_request received (sessionKey=%s, contextId=%s)",c,g??"-"),await l.agentSend(t,s,c,void 0,i)})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==y&&a.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),r=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:a.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,r,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(a.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(a.agentGatewayUrl&&agentKeys){let r;o.setLogger(e.logger);{const e=a.agentGatewayUrl,o=agentKeys;r=async()=>{const r=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Canvas token returned ${r.status}: ${e}`)}return r.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const s=new CanvasRelayClient({canvasRelayUrl:a.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:r,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const r={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const g=e=>async(o,r,a)=>(l.preMapToolCallSession(o),e(o,r));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(r))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(r))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:i});const c=l.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const d=l.getResourceRenderer();if(d){generateActionTools(i,e,async(e,o)=>l.gatewayRequest(e,o),d,e.logger)}const p='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',f=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),y=256,u=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(u.size>=y&&!u.has(o.sessionKey)){const e=u.keys().next().value;void 0!==e&&u.delete(e)}u.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return;if(!o?.sessionKey)return{appendSystemContext:p};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+p}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+p}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!f.has(e.toolName))return;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:a.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:a.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(a.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${a.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const r=mirrorOpenClawConfig(o.openclawConfig);r.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${r.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${r.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(e.registrationMode&&"full"!==e.registrationMode)e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);else{_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const r={current:null};registerUploadTools(e,o,r),registerServices(e,o,r),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}}};export default plugin;
1
+ import{resolveConfigWithMeta,mirrorOpenClawConfig}from"./config.js";import{MessageSDK}from"./messaging/message-sdk.js";import{extractChatPrompt}from"./messaging/chat-envelope.js";import{registerA2ATools,TaskHandleRegistry}from"./a2a/tools.js";import{BEEOS_UPLOAD_TOOL_NAME,BEEOS_UPLOAD_TOOL_SCHEMA,BEEOS_UPLOAD_TOOL_DESCRIPTION,executeUpload}from"./upload/tool.js";import{writeUploadToolResult,cleanupUploadToolResultCache}from"./upload/result-cache.js";import{handleFileStage}from"./upload/file-stage.js";import{handleStatusRequest}from"./status.js";import{ACPServer}from"./acp-server.js";import{createObsEventWriter}from"./observability.js";import{resolvePluginVersion}from"./plugin-version.js";import{createTerminalWebSocketService}from"./terminal/websocket.js";import{TerminalManager}from"./terminal/session-manager.js";import{loadAgentKeyPair,agentFetch}from"./agent-auth.js";import{readDiscoveryCache,writeDiscoveryCache}from"./discovery-cache.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{setUploadDeps}from"./upload/upload-file.js";import{registerLocalFileExtractor}from"./upload/local-file-extractors.js";import{browserScreenshotExtractor}from"./browser/screenshot-extractor.js";import{CanvasStateManager}from"./canvas/state.js";import{CanvasRelayClient}from"./canvas/relay-client.js";import{RENDER_UI_TOOL_NAME,RENDER_UI_TOOL_DESCRIPTION,RENDER_UI_TOOL_SCHEMA,CLOSE_UI_TOOL_NAME,CLOSE_UI_TOOL_DESCRIPTION,CLOSE_UI_TOOL_SCHEMA,createRenderUiHandler,createCloseUiHandler,CANVAS_SYSTEM_PROMPT}from"./canvas/tools.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{CRON_CREATE_SCHEMA,CRON_UPDATE_SCHEMA}from"./surface/cron-schemas.js";import{RENDER_FORM_TOOL_NAME,RENDER_FORM_TOOL_SCHEMA,buildRenderFormDescription,createRenderFormHandler}from"./surface/render-form-tool.js";import{ResourceDescriptorRegistry}from"./surface/resource-descriptor-registry.js";import{CRON_RESOURCE}from"./surface/cron-resource.js";import{generateActionTools}from"./surface/action-tool-generator.js";import{buildThirdPartyChannelSet,isThirdPartyChannel}from"./channel-detection.js";const PLUGIN_VERSION=resolvePluginVersion();let agentKeys=null,sharedCanvasState=null,_initializing=!1,_pluginRegisteredAt=0;function initializePlugin(e){let o,a;try{o=e.runtime?.config?.loadConfig?.()}catch{}try{a=resolveConfigWithMeta({pluginConfig:e.pluginConfig??{},openclawConfig:o})}catch(o){return e.logger.error?.("[beeos-claw] Failed to resolve config:",o),null}const r=a.config,t=buildThirdPartyChannelSet(o);t.size>0&&e.logger.info?.(`[beeos-claw] Third-party channels detected: ${[...t].join(", ")}`);const s=r.log.enabled?createObsEventWriter({dir:r.log.dir,redaction:{gatewayToken:r.gateway.token},logger:e.logger}):()=>{};s({component:"plugin",domain:"lifecycle",name:"lifecycle.startup",severity:"info",payload:{pluginVersion:PLUGIN_VERSION,pid:process.pid,nodeVersion:process.version,cwd:process.cwd()}}),s({component:"plugin",domain:"config",name:"config.resolved",severity:"info",payload:{config:r,sources:a.sources,openclawConfigPath:a.openclawConfigPath}});for(const o of a.validation)s({component:"plugin",domain:"config",name:"config.validation",severity:o.severity,summary:o.message,error:{code:o.code,message:o.message,nextSteps:o.nextSteps}}),"error"===o.severity?e.logger.error?.(`[beeos-claw] ${o.message}`):e.logger.warn?.(`[beeos-claw] ${o.message}`);if(a.validation.some(e=>"error"===e.severity)&&e.logger.error?.("[beeos-claw] Plugin has configuration errors — some features may not work."),r.keyFile)try{agentKeys=loadAgentKeyPair(r.keyFile);const o=Buffer.from(agentKeys.publicKey).toString("base64");e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (publicKey: ${o})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to load agent key pair: ${o}`),e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no key pair — signed requests disabled)`)}else e.logger.info?.(`[beeos-claw] v${PLUGIN_VERSION} registering (no keyFile configured)`);return agentKeys&&r.agentGatewayUrl&&(setUploadDeps({gatewayUrl:r.agentGatewayUrl,keys:agentKeys,logger:e.logger}),registerLocalFileExtractor(browserScreenshotExtractor),e.logger.info?.("[beeos-claw] Upload deps + browser screenshot extractor registered")),{cfg:r,writeObs:s,thirdPartyChannels:t,openclawConfig:o}}function registerUploadTools(e,o,a){const{cfg:r,writeObs:t}=o;cleanupUploadToolResultCache().catch(o=>{e.logger.warn?.(`[beeos_upload_file] cache cleanup failed on startup: ${String(o)}`)}),e.registerTool({name:BEEOS_UPLOAD_TOOL_NAME,description:BEEOS_UPLOAD_TOOL_DESCRIPTION,parameters:BEEOS_UPLOAD_TOOL_SCHEMA,async execute(o,a,s){e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} params=${JSON.stringify(a)}`);const n=await executeUpload(a,r,agentKeys,s);return e.logger.debug?.(`[beeos_upload_file] toolCallId=${o} ok=${n.ok}`),t({component:"upload-tool",domain:"tool",name:n.ok?"upload.success":"upload.failure",severity:n.ok?"info":"warn",payload:{toolCallId:o,ok:n.ok,fileCount:n.files?.length??0,error:n.error}}),(async a=>{try{await writeUploadToolResult(o,BEEOS_UPLOAD_TOOL_NAME,a),await cleanupUploadToolResultCache()}catch(a){e.logger.warn?.(`[beeos_upload_file] persist cache failed toolCallId=${o}: ${String(a)}`)}return a})(n)}}),e.registerHttpRoute({path:"/beeos/files/stage",auth:"gateway",async handler(e,a){await handleFileStage(e,a,o.cfg)}}),e.registerHttpRoute({path:"/beeos/status",auth:"plugin",handler(e,o){handleStatusRequest(e,o,{publicKey:agentKeys?.publicKey??null,pluginVersion:PLUGIN_VERSION})}})}function registerServices(e,o,a){const{cfg:r,writeObs:t,thirdPartyChannels:s}=o,n=agentKeys?(e,o)=>agentFetch(e,agentKeys,o):void 0,i=agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):void 0,l=new ACPServer(r,e.logger,t,n,i,s);a.current=l,e.registerService({id:"beeos-acp",async start(){const o=Date.now(),a=_pluginRegisteredAt?o-_pluginRegisteredAt:-1;if(e.logger.info?.(`[beeos-claw] beeos-acp service start() begin, sincePluginRegister=${a}ms, bridge.url=${r.bridge.url||"(empty)"}`),!r.bridge.url&&r.agentGatewayUrl&&n){const o=Date.now(),a=await discoverBridgeFromGW(r.agentGatewayUrl,n);if(e.logger.info?.(`[beeos-claw] bridge discovery took ${Date.now()-o}ms result=${a?.url??"null"}`),a)r.bridge.url=a.url,writeDiscoveryCache({bridge:{url:a.url,discoveredAt:Date.now()}}),e.logger.info?.(`[beeos-claw] Bridge URL discovered from Agent Gateway: ${a.url}`);else{const o=readDiscoveryCache();o?.bridge?.url?(r.bridge.url=o.bridge.url,e.logger.info?.(`[beeos-claw] Bridge URL from cache (GW unreachable): ${o.bridge.url}`)):e.logger.debug?.("[beeos-claw] Bridge URL not available (no GW, no cache) — bridge disabled")}}await l.start(r.bridge.url||void 0),e.logger.info?.(`[beeos-claw] ACP Server started (total service start: ${Date.now()-o}ms)`)},async stop(){await l.stop(),e.logger.info?.("[beeos-claw] ACP Server stopped")}});const g=new TaskHandleRegistry;let c=null;const{enabled:d,url:p,principalId:u,a2aEnabled:f}=r.messageBus;let y;if(r.agentGatewayUrl&&agentKeys){const e=r.agentGatewayUrl,o=agentKeys;y=async a=>{const r=await agentFetch(`${e}/api/v1/messaging/token`,o,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({principal_id:a})});if(!r.ok){const e=await r.text().catch(()=>"");throw new Error(`Gateway messaging token returned ${r.status}: ${e}`)}return r.json()}}if(d&&(y||r.messageBus.serviceUrl)){c=new MessageSDK({serviceUrl:y?void 0:r.messageBus.serviceUrl,centrifugoUrl:p},e.logger);const o=u,a=async o=>{c.onceConnected(()=>{l.setMessageSdk(c,o),e.logger.info?.("[beeos-claw] Message SDK connected (principalId=%s, via=%s)",o,y?"gateway":"direct")}),await c.connect(o,{centrifugoUrl:p,tokenProvider:y})};e.registerService({id:"beeos-message-bus",async start(){let r=o;if(r||(r=process.env.BEEOS_PRINCIPAL_ID||""),!r&&l)try{await l.waitForAgentMapping(),r=l.getDefaultPlatformAgent()?.instanceId||""}catch(o){e.logger.warn?.("[beeos-claw] Failed to resolve principalId from agent sync:",o)}r?await a(r):l?(e.logger.info?.("[beeos-claw] Message bus deferred: waiting for late agent sync..."),l.onceAgentMappingReady(()=>{const o=l.getDefaultPlatformAgent()?.instanceId;o?a(o).catch(o=>{e.logger.error?.("[beeos-claw] Deferred message bus connect failed:",o)}):e.logger.warn?.("[beeos-claw] Agent mapping ready but no principalId, message bus stays disabled")})):e.logger.warn?.("[beeos-claw] No principalId resolved, message bus disabled")},async stop(){await(c?.disconnect()),e.logger.info?.("[beeos-claw] Message SDK disconnected")}});const t=async(o,a)=>{const r=extractChatPrompt(a,{source:o,localAgentId:l.getLocalAgentId()});r&&(e.logger.debug?.(`[beeos-claw] ${o} received (sessionKey=%s, contextId=%s)`,r.sessionKey,r.contextId??"-"),await l.agentSend(r.message,r.files,r.sessionKey,void 0,r.channelId,r.messageId))};c.onMessage("chat_message",async e=>{await t("chat_message",e)});let s=!1;c.onMessage("agent_request",async o=>{s||(e.logger.warn?.("[beeos-claw] Received legacy 'agent_request' envelope — this path is deprecated and will be removed once all upstreams migrate to 'chat_message' (L0 AgentInvocationService)."),s=!0),await t("agent_request",o)})}else d?e.logger.warn?.("[beeos-claw] Message bus enabled but MESSAGE_SERVICE_URL not set — disabled"):e.logger.info?.("[beeos-claw] Message bus disabled (enabled=%s)",d);if(!1!==f&&r.agentGatewayUrl){const o=e=>l.getGatewaySessionKeyForToolCall(e),a=e=>l.getMentionsForToolCall(e),t=(e,o)=>{l.broadcastNotification(e,o)};registerA2ATools(e,{agentGatewayUrl:r.agentGatewayUrl,agentKeys:agentKeys,logger:e.logger},g,o,a,t),e.logger.info?.("[beeos-claw] A2A tools registered (beeos_discover_agents, beeos_call_agent) via REST")}if(r.canvasEnabled){sharedCanvasState||(sharedCanvasState=new CanvasStateManager);const o=sharedCanvasState;if(r.agentGatewayUrl&&agentKeys){let a;o.setLogger(e.logger);{const e=r.agentGatewayUrl,o=agentKeys;a=async()=>{const a=await agentFetch(`${e}/api/v1/canvas/token`,o,{method:"POST"});if(!a.ok){const e=await a.text().catch(()=>"");throw new Error(`Canvas token returned ${a.status}: ${e}`)}return a.json()}}e.registerService({id:"beeos-canvas-relay",async start(){try{await l.waitForAgentMapping();const t=l.getDefaultPlatformAgent();if(!t?.id)return void e.logger.info?.("[beeos-claw] Canvas Relay skipped: no agentId after sync");const s=new CanvasRelayClient({canvasRelayUrl:r.canvasRelayUrl,agentKeys:agentKeys,agentId:t.id,displayName:t.displayName,avatarUrl:t.avatarUrl,tokenProvider:a,onReconnect:()=>l.onRelayReconnected(),logger:e.logger});s.connect(),o.setRelayClient(s),o.setAgentId(t.id),l.wireRelayActionHandler(),l.flushPendingCanvasLinks(),e.logger.info?.(`[beeos-claw] Canvas Relay multiplexed client connected (agentId=${t.id}, name=${t.name??"none"})`)}catch(o){e.logger.warn?.(`[beeos-claw] Failed to start Canvas Relay client: ${o}`)}},stop(){o.getRelayClient()?.disconnect()}}),e.logger.info?.("[beeos-claw] Canvas Relay configured")}else e.logger.warn?.("[beeos-claw] Canvas Relay unavailable: agentGatewayUrl or agentKeys missing");const a={stateManager:o,logger:e.logger,writeObs:e=>t(e),isCanvasActive:e=>!e||l.isCanvasActiveForGatewayKey(e),getActiveSessionId:e=>l.getSessionIdForToolCall(e),getPlatformAgentId:e=>l.getPlatformAgentIdForToolCall(e),getGatewaySessionKey:e=>l.getGatewaySessionKeyForToolCall(e),waitForAgentSync:()=>l.waitForAgentMapping()},n=new FormSchemaRegistry;n.register(CRON_CREATE_SCHEMA),n.register(CRON_UPDATE_SCHEMA);const i=new ResourceDescriptorRegistry;i.register(CRON_RESOURCE);const g=e=>async(o,a,r)=>(l.preMapToolCallSession(o),e(o,a));e.registerTool({name:RENDER_UI_TOOL_NAME,description:RENDER_UI_TOOL_DESCRIPTION,parameters:RENDER_UI_TOOL_SCHEMA,execute:g(createRenderUiHandler(a))}),e.registerTool({name:CLOSE_UI_TOOL_NAME,description:CLOSE_UI_TOOL_DESCRIPTION,parameters:CLOSE_UI_TOOL_SCHEMA,execute:g(createCloseUiHandler(a))}),l.setCanvasState(o,{schemaRegistry:n,descriptorRegistry:i});const c=l.getFormBuilder();c&&e.registerTool({name:RENDER_FORM_TOOL_NAME,description:buildRenderFormDescription(n),parameters:RENDER_FORM_TOOL_SCHEMA,execute:g(createRenderFormHandler({registry:n,builder:c,logger:e.logger}))});const d=l.getResourceRenderer();if(d){generateActionTools(i,e,async(e,o)=>l.gatewayRequest(e,o),d,e.logger)}const p='\n\nTool selection guidance:\n- browser tool: Use when the user wants to OPEN, VISIT, NAVIGATE to a webpage, take SCREENSHOTS, interact with elements (click, type), or visually inspect pages.\n- beeos_search: Use ONLY for text-based information retrieval (fact-finding, research queries). Never for page interaction or screenshots.\n- beeos_fetch: Use ONLY to extract article text from a known URL. Never for visual content or screenshots.\nWhen the user says "open", "visit", "go to", "screenshot", or mentions seeing/viewing a webpage, ALWAYS prefer the browser tool.',u=new Set([RENDER_UI_TOOL_NAME,CLOSE_UI_TOOL_NAME,RENDER_FORM_TOOL_NAME]),f=256,y=new Map;if("function"==typeof e.on){try{e.on("before_prompt_build",(e,o)=>{if(o?.sessionKey){if(y.size>=f&&!y.has(o.sessionKey)){const e=y.keys().next().value;void 0!==e&&y.delete(e)}y.set(o.sessionKey,o.channelId)}if(isThirdPartyChannel(s,{sessionKey:o?.sessionKey,channelId:o?.channelId}))return;if(!o?.sessionKey)return{appendSystemContext:p};return l.isCanvasActiveForGatewayKey(o.sessionKey)?{appendSystemContext:CANVAS_SYSTEM_PROMPT+p}:{appendSystemContext:"Canvas is closed by user. Do NOT use render_ui, close_ui, or render_form. Respond with plain text only."+p}}),e.logger.debug?.("[beeos-claw] Canvas system prompt hook registered: before_prompt_build")}catch{e.logger.debug?.("[beeos-claw] before_prompt_build hook not supported")}try{e.on("before_tool_call",(e,o)=>{if(o?.sessionKey&&l.setPendingToolCallSessionKey(o.sessionKey),!u.has(e.toolName))return;return isThirdPartyChannel(s,{sessionKey:o?.sessionKey})?{block:!0,blockReason:"Canvas tools are not available in this channel. Respond with plain text only."}:o?.sessionKey?l.isCanvasActiveForGatewayKey(o.sessionKey)?void 0:{block:!0,blockReason:"Canvas is closed by user. Respond with plain text instead."}:{block:!0,blockReason:"Canvas session unknown. Respond with plain text instead."}}),e.logger.debug?.("[beeos-claw] Canvas tool guard registered: before_tool_call")}catch{e.logger.debug?.("[beeos-claw] before_tool_call hook not supported")}}e.logger.info?.("[beeos-claw] Canvas tools registered (render_ui, close_ui, render_form + auto-action tools)")}else e.logger.info?.("[beeos-claw] Canvas disabled by config (canvasEnabled=false)");const m=new TerminalManager,b=createTerminalWebSocketService({config:{port:r.terminalWsPort,token:agentKeys?Buffer.from(agentKeys.publicKey).toString("base64"):"",allowedOrigins:r.terminalWsAllowedOrigins},terminalMgr:m,logger:e.logger,writeObs:t});if(r.bridge.shell.enabled?e.registerService({id:"beeos-terminal-ws",start(){b.start(),e.logger.info?.(`[beeos-claw] Terminal WS started on port ${r.terminalWsPort}`)},stop(){b.stop(),m.destroy(),e.logger.info?.("[beeos-claw] Terminal WS stopped")}}):e.logger.info?.("[beeos-claw] Terminal/shell disabled by config (bridge.shell.enabled=false)"),o.openclawConfig){const a=mirrorOpenClawConfig(o.openclawConfig);a.copied?e.logger.info?.(`[beeos-claw] mirrored openclaw config to ${a.destinationPath}`):e.logger.debug?.(`[beeos-claw] openclaw config mirror skipped: ${a.reason}`)}}const plugin={id:"beeos-claw",name:"beeos-claw",description:"BeeOS platform bridge plugin for OpenClaw. Provides file upload/download, ACP bridge, and observability.",version:PLUGIN_VERSION,register(e){if(!_initializing)if(e.registrationMode&&"full"!==e.registrationMode)e.logger.debug?.(`[beeos-claw] Skipping registration (mode=${e.registrationMode})`);else{_initializing=!0;try{const o=initializePlugin(e);if(!o)return;const a={current:null};registerUploadTools(e,o,a),registerServices(e,o,a),_pluginRegisteredAt=Date.now(),e.logger.info?.("[beeos-claw] Plugin registered successfully")}finally{_initializing=!1}}}};export default plugin;
@@ -1 +1 @@
1
- const TIMESTAMP_PREFIX_RE=/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?\s+GMT[+-]\d{1,2}(?::?\d{2})?]\s*/i,WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,USER_MSG_PREFIX_RE=/^\s*User Message From \w+:\s*/i,HEARTBEAT_OK_LINE="HEARTBEAT_OK",HEARTBEAT_OK_LINE_RE=/^[\s\p{P}\p{S}]*HEARTBEAT_OK[\s\p{P}\p{S}]*$/u,METADATA_PREFIX_PATTERNS=[TIMESTAMP_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,USER_MSG_PREFIX_RE],UNTRUSTED_METADATA_BLOCK_RE=/^Conversation info \(untrusted metadata\):[\s\S]*?Current time:\s*.*?$/i,UNTRUSTED_BLOCK_MARKER="Conversation info (untrusted metadata):",SENDER_UNTRUSTED_MARKER="Sender (untrusted metadata):",SENDER_UNTRUSTED_BLOCK_RE=/Sender\s*\(untrusted[^)]*\)\s*:\s*```[a-z]*\s*[\s\S]*?```/i;function stripMetadataPrefixesFromLine(t){let e=t,r=!1,n=!0;for(;n;){n=!1;for(const t of METADATA_PREFIX_PATTERNS)t.test(e)&&(e=e.replace(t,""),r=!0,n=!0)}return{line:e,changed:r}}export function stripTransportMetadata(t){const e=t.includes(UNTRUSTED_BLOCK_MARKER),r=t.includes(SENDER_UNTRUSTED_MARKER);if(!(t.includes("[")||t.includes("HEARTBEAT_OK")||t.includes("User Message From")||e||r))return t;if(r){const e=t.replace(SENDER_UNTRUSTED_BLOCK_RE,"").replace(/^\n+/,"").trim();if(e!==t)return e?stripTransportMetadata(e):"";const r=t.indexOf(SENDER_UNTRUSTED_MARKER),n=t.indexOf("\n\n",r),s=-1===n?t.length:n,i=t.slice(s).replace(/^\n+/,"").trim();return i?stripTransportMetadata(i):""}if(e){const e=t.indexOf(UNTRUSTED_BLOCK_MARKER),r=/\nCurrent time:[^\n]*/i.exec(t.slice(e));if(r){const n=e+r.index+r[0].length,s=t.slice(n).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}const n=t.indexOf("\n\n",e),s=-1===n?t.length:n,i=t.slice(s).replace(/^\n+/,"").trim();return i?stripTransportMetadata(i):""}const n=t.split(/\r?\n/),s=[];let i=!1;for(const t of n){const e=stripMetadataPrefixesFromLine(t);HEARTBEAT_OK_LINE_RE.test(e.line.trim())?i=!0:e.changed&&(i=!0,!e.line.trim())||s.push(e.line)}return i?s.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):t}export function normalizeTransportText(t){if(!t)return;const e=stripTransportMetadata(t);return e.length>0?e:void 0}export function isHeartbeatOnlyText(t){const e=t.trim();if(!e)return!1;if(HEARTBEAT_OK_LINE_RE.test(e))return!0;if(!e.includes("HEARTBEAT_OK"))return!1;return 0===stripTransportMetadata(e).trim().length}const HEARTBEAT_PROMPT_PREFIX="read heartbeat.md if it exists";export function isHeartbeatPromptText(t){return t.slice(0,80).toLowerCase().startsWith(HEARTBEAT_PROMPT_PREFIX)}const CRON_PROMPT_PREFIX_RE=/^\[cron:[0-9a-f-]{8,}\s/i;export function isCronPromptText(t){return CRON_PROMPT_PREFIX_RE.test(t.trimStart())}function isRecord(t){return!!t&&"object"==typeof t&&!Array.isArray(t)}function sanitizeNode(t){if("string"==typeof t){const e=stripTransportMetadata(t);return{value:e,changed:e!==t}}if(Array.isArray(t)){let e=!1;const r=t.map(t=>{const r=sanitizeNode(t);return r.changed&&(e=!0),r.value});return{value:e?r:t,changed:e}}if(!isRecord(t))return{value:t,changed:!1};let e=!1,r=t;if("string"==typeof t.type&&"text"===t.type&&"string"==typeof t.text){const n=stripTransportMetadata(t.text);n!==t.text&&(r={...r,text:n},e=!0)}if(Array.isArray(t.content)){const n=sanitizeNode(t.content);n.changed&&(e||(r={...r}),r.content=n.value,e=!0)}return{value:e?r:t,changed:e}}export function sanitizePromptPayload(t){return sanitizeNode(t).value}
1
+ import{HEARTBEAT_TOKEN,isSilentReplyPrefixText,stripHeartbeatToken}from"./auto-reply-tokens.js";const TIMESTAMP_PREFIX_RE=/^\s*\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{1,2}:\d{2}(?::\d{2})?\s+GMT[+-]\d{1,2}(?::?\d{2})?]\s*/i,WORKDIR_PREFIX_RE=/^\s*\[working directory:[^\]]*]\s*/i,MESSAGE_ID_PREFIX_RE=/^\s*\[message_id:[^\]]*]\s*/i,USER_MSG_PREFIX_RE=/^\s*User Message From \w+:\s*/i,HEARTBEAT_OK_LINE=HEARTBEAT_TOKEN,HEARTBEAT_OK_LINE_RE=/^[\s\p{P}\p{S}]*HEARTBEAT_OK[\s\p{P}\p{S}]*$/u,METADATA_PREFIX_PATTERNS=[TIMESTAMP_PREFIX_RE,WORKDIR_PREFIX_RE,MESSAGE_ID_PREFIX_RE,USER_MSG_PREFIX_RE],UNTRUSTED_METADATA_BLOCK_RE=/^Conversation info \(untrusted metadata\):[\s\S]*?Current time:\s*.*?$/i,UNTRUSTED_BLOCK_MARKER="Conversation info (untrusted metadata):",SENDER_UNTRUSTED_MARKER="Sender (untrusted metadata):",SENDER_UNTRUSTED_BLOCK_RE=/Sender\s*\(untrusted[^)]*\)\s*:\s*```[a-z]*\s*[\s\S]*?```/i;function stripMetadataPrefixesFromLine(e){let t=e,r=!1,n=!0;for(;n;){n=!1;for(const e of METADATA_PREFIX_PATTERNS)e.test(t)&&(t=t.replace(e,""),r=!0,n=!0)}return{line:t,changed:r}}export function stripTransportMetadata(e){const t=e.includes(UNTRUSTED_BLOCK_MARKER),r=e.includes(SENDER_UNTRUSTED_MARKER);if(!(e.includes("[")||e.includes(HEARTBEAT_OK_LINE)||e.includes("User Message From")||t||r))return e;if(r){const t=e.replace(SENDER_UNTRUSTED_BLOCK_RE,"").replace(/^\n+/,"").trim();if(t!==e)return t?stripTransportMetadata(t):"";const r=e.indexOf(SENDER_UNTRUSTED_MARKER),n=e.indexOf("\n\n",r),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}if(t){const t=e.indexOf(UNTRUSTED_BLOCK_MARKER),r=/\nCurrent time:[^\n]*/i.exec(e.slice(t));if(r){const n=t+r.index+r[0].length,i=e.slice(n).replace(/^\n+/,"").trim();return i?stripTransportMetadata(i):""}const n=e.indexOf("\n\n",t),i=-1===n?e.length:n,s=e.slice(i).replace(/^\n+/,"").trim();return s?stripTransportMetadata(s):""}const n=e.split(/\r?\n/),i=[];let s=!1;for(const e of n){const t=stripMetadataPrefixesFromLine(e);HEARTBEAT_OK_LINE_RE.test(t.line.trim())?s=!0:t.changed&&(s=!0,!t.line.trim())||i.push(t.line)}return s?i.join("\n").replace(/^\n+/,"").replace(/\n+$/,""):e}export function normalizeTransportText(e){if(!e)return;const t=stripTransportMetadata(e);return t.length>0?t:void 0}export function isHeartbeatOnlyText(e){const t=e.trim();if(!t)return!1;if(HEARTBEAT_OK_LINE_RE.test(t))return!0;if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return!0;if(0===stripTransportMetadata(t).trim().length)return!0}return!1}export function isHeartbeatPrefixText(e){if(!e)return!1;const t=e.trim();return!!t&&(!(t.length>=HEARTBEAT_OK_LINE.length)&&isSilentReplyPrefixText(t,HEARTBEAT_OK_LINE))}export function isHeartbeatFullOrPrefix(e){return"none"!==classifyHeartbeatFilter(e).reason}export function previewFilteredText(e,t=120){if(!e)return"";const r=e.replace(/\\/g,"\\\\").replace(/\r/g,"\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/\s+/g," ").trim();if(r.length<=t)return r;const n=Math.floor((t-1)/2),i=t-1-n;return`${r.slice(0,n)}…${r.slice(-i)}`}export function classifyHeartbeatFilter(e){if(!e)return{reason:"none",filtered:!1};const t=e.trim();if(!t)return{reason:"none",filtered:!1};if(HEARTBEAT_OK_LINE_RE.test(t))return{reason:"full",filtered:!0};if(t.includes(HEARTBEAT_OK_LINE)){if(stripHeartbeatToken(t,{mode:"message"}).shouldSkip)return{reason:"full-stripped",filtered:!0};if(0===stripTransportMetadata(t).trim().length)return{reason:"full-metadata-residue",filtered:!0}}return isHeartbeatPrefixText(t)?{reason:"prefix",filtered:!0}:{reason:"none",filtered:!1}}const HEARTBEAT_PROMPT_PREFIX="read heartbeat.md if it exists";export function isHeartbeatPromptText(e){return e.slice(0,80).toLowerCase().startsWith(HEARTBEAT_PROMPT_PREFIX)}const CRON_PROMPT_PREFIX_RE=/^\[cron:[0-9a-f-]{8,}\s/i;export function isCronPromptText(e){return CRON_PROMPT_PREFIX_RE.test(e.trimStart())}function isRecord(e){return!!e&&"object"==typeof e&&!Array.isArray(e)}function sanitizeNode(e){if("string"==typeof e){const t=stripTransportMetadata(e);return{value:t,changed:t!==e}}if(Array.isArray(e)){let t=!1;const r=e.map(e=>{const r=sanitizeNode(e);return r.changed&&(t=!0),r.value});return{value:t?r:e,changed:t}}if(!isRecord(e))return{value:e,changed:!1};let t=!1,r=e;if("string"==typeof e.type&&"text"===e.type&&"string"==typeof e.text){const n=stripTransportMetadata(e.text);n!==e.text&&(r={...r,text:n},t=!0)}if(Array.isArray(e.content)){const n=sanitizeNode(e.content);n.changed&&(t||(r={...r}),r.content=n.value,t=!0)}return{value:t?r:e,changed:t}}export function sanitizePromptPayload(e){return sanitizeNode(e).value}
@@ -1 +1 @@
1
- import{describe,it,expect}from"vitest";import{isCronPromptText,isHeartbeatPromptText}from"./message-filter.js";describe("isCronPromptText",()=>{it("matches canonical upstream cron prefix",()=>{expect(isCronPromptText("[cron:3f50c57c-e9ee-420c-acb4-cc80c1dbb15d daily-summary] please summarize")).toBe(!0)}),it("matches case-insensitively and tolerates leading whitespace",()=>{expect(isCronPromptText("\n [CRON:abcdef12-3456-7890-abcd-ef0123456789 job] run")).toBe(!0)}),it("rejects text without the cron prefix",()=>{expect(isCronPromptText("hello, please run my task")).toBe(!1),expect(isCronPromptText("[cron] missing id")).toBe(!1),expect(isCronPromptText("")).toBe(!1)}),it("does not collide with heartbeat prompts",()=>{const e="Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.";expect(isHeartbeatPromptText(e)).toBe(!0),expect(isCronPromptText(e)).toBe(!1)})});
1
+ import{describe,it,expect}from"vitest";import{isCronPromptText,isHeartbeatPromptText,isHeartbeatOnlyText,isHeartbeatPrefixText,isHeartbeatFullOrPrefix,classifyHeartbeatFilter,previewFilteredText}from"./message-filter.js";describe("isCronPromptText",()=>{it("matches canonical upstream cron prefix",()=>{expect(isCronPromptText("[cron:3f50c57c-e9ee-420c-acb4-cc80c1dbb15d daily-summary] please summarize")).toBe(!0)}),it("matches case-insensitively and tolerates leading whitespace",()=>{expect(isCronPromptText("\n [CRON:abcdef12-3456-7890-abcd-ef0123456789 job] run")).toBe(!0)}),it("rejects text without the cron prefix",()=>{expect(isCronPromptText("hello, please run my task")).toBe(!1),expect(isCronPromptText("[cron] missing id")).toBe(!1),expect(isCronPromptText("")).toBe(!1)}),it("does not collide with heartbeat prompts",()=>{const e="Read HEARTBEAT.md if it exists (workspace context). Follow it strictly.";expect(isHeartbeatPromptText(e)).toBe(!0),expect(isCronPromptText(e)).toBe(!1)})}),describe("isHeartbeatOnlyText",()=>{it("detects bare token",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK")).toBe(!0)}),it("detects token with surrounding whitespace",()=>{expect(isHeartbeatOnlyText(" HEARTBEAT_OK ")).toBe(!0),expect(isHeartbeatOnlyText("\n\nHEARTBEAT_OK\n\n")).toBe(!0)}),it("detects token with trailing punctuation (OpenClaw rule: up to 4 non-word chars)",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK!!!")).toBe(!0),expect(isHeartbeatOnlyText("HEARTBEAT_OK ✅")).toBe(!0)}),it("detects token wrapped in markdown markup",()=>{expect(isHeartbeatOnlyText("**HEARTBEAT_OK**")).toBe(!0),expect(isHeartbeatOnlyText("`HEARTBEAT_OK`")).toBe(!0),expect(isHeartbeatOnlyText("_HEARTBEAT_OK_")).toBe(!0)}),it("detects token sitting next to stripped transport metadata",()=>{expect(isHeartbeatOnlyText("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]")).toBe(!0)}),it("rejects text that merely mentions the token inline",()=>{expect(isHeartbeatOnlyText("Use the sentinel HEARTBEAT_OK in your ack handler.")).toBe(!1),expect(isHeartbeatOnlyText("The response should be HEARTBEAT_OK always.")).toBe(!1)}),it("rejects empty and pure-whitespace input",()=>{expect(isHeartbeatOnlyText("")).toBe(!1),expect(isHeartbeatOnlyText(" \n\t ")).toBe(!1)})}),describe("isHeartbeatPrefixText",()=>{it("flags the real-world prefix that leaked to staging",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_")).toBe(!0)}),it("flags every meaningful uppercase prefix of HEARTBEAT_OK that includes '_'",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_O")).toBe(!0)}),it("does NOT flag prefixes without '_' (upstream trade-off, avoids eating real words)",()=>{expect(isHeartbeatPrefixText("HEART")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT")).toBe(!1)}),it("rejects length < 2",()=>{expect(isHeartbeatPrefixText("H")).toBe(!1),expect(isHeartbeatPrefixText("")).toBe(!1)}),it("rejects mixed case (avoids eating conversational text)",()=>{expect(isHeartbeatPrefixText("heartbeat_")).toBe(!1),expect(isHeartbeatPrefixText("Heartbeat_")).toBe(!1)}),it("rejects text with non-[A-Z_] characters",()=>{expect(isHeartbeatPrefixText("HEARTBEAT!")).toBe(!1),expect(isHeartbeatPrefixText("HEART-BEAT")).toBe(!1),expect(isHeartbeatPrefixText("HEARTBEAT_OK TRAILING")).toBe(!1)}),it("does NOT flag the full token (that's isHeartbeatOnlyText's job)",()=>{expect(isHeartbeatPrefixText("HEARTBEAT_OK")).toBe(!1)}),it("tolerates leading whitespace",()=>{expect(isHeartbeatPrefixText(" HEARTBEAT_")).toBe(!0)})}),describe("isHeartbeatFullOrPrefix — exactly matches OpenClaw semantics",()=>{it("catches the full token",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_OK.")).toBe(!0),expect(isHeartbeatFullOrPrefix("**HEARTBEAT_OK**")).toBe(!0)}),it("catches prefixes that contain '_'",()=>{expect(isHeartbeatFullOrPrefix("HEARTBEAT_")).toBe(!0),expect(isHeartbeatFullOrPrefix("HEARTBEAT_O")).toBe(!0)}),it("intentionally does NOT catch prefixes without '_' (upstream trade-off)",()=>{expect(isHeartbeatFullOrPrefix("HEART")).toBe(!1),expect(isHeartbeatFullOrPrefix("HEARTBEAT")).toBe(!1),expect(isHeartbeatFullOrPrefix("HE")).toBe(!1)}),it("lets real user content through",()=>{expect(isHeartbeatFullOrPrefix("Let's design a HEARTBEAT_OK-like ack for our API.")).toBe(!1),expect(isHeartbeatFullOrPrefix("Hello, how are you?")).toBe(!1),expect(isHeartbeatFullOrPrefix("Heart of darkness is a novel")).toBe(!1)})}),describe("classifyHeartbeatFilter — structured drop reasons for logs",()=>{it("reports `full` on canonical tokens",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK")).toEqual({reason:"full",filtered:!0}),expect(classifyHeartbeatFilter("HEARTBEAT_OK.").reason).toBe("full"),expect(classifyHeartbeatFilter("**HEARTBEAT_OK**").reason).toBe("full"),expect(classifyHeartbeatFilter("`HEARTBEAT_OK`").reason).toBe("full")}),it("reports `full-stripped` as a defensive fallback when regex misses but stripHeartbeatToken catches",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK ✅").reason).toBe("full")}),it("reports `full-metadata-residue` when the token is surrounded by transport metadata",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_OK\n[working directory: /home/node]\n[message_id: abc-123]").reason).toBe("full-metadata-residue")}),it("reports `prefix` on uppercase prefixes that contain '_' (upstream rule)",()=>{expect(classifyHeartbeatFilter("HEARTBEAT_").reason).toBe("prefix"),expect(classifyHeartbeatFilter("HEARTBEAT_O").reason).toBe("prefix")}),it("reports `none` for uppercase prefixes WITHOUT '_' (upstream trade-off — avoids eating real words)",()=>{expect(classifyHeartbeatFilter("HEART").reason).toBe("none"),expect(classifyHeartbeatFilter("HEARTBEAT").reason).toBe("none"),expect(classifyHeartbeatFilter("HE").reason).toBe("none")}),it("reports `none` for real content",()=>{expect(classifyHeartbeatFilter("Hello").reason).toBe("none"),expect(classifyHeartbeatFilter("Use HEARTBEAT_OK to reply.").reason).toBe("none"),expect(classifyHeartbeatFilter("").reason).toBe("none"),expect(classifyHeartbeatFilter(" ").reason).toBe("none")}),it("keeps `filtered` in sync with `reason` (never drifts)",()=>{for(const e of["HEARTBEAT_OK","**HEARTBEAT_OK**","HEARTBEAT_","HE","HEART","hello",""]){const t=classifyHeartbeatFilter(e);expect(t.filtered).toBe("none"!==t.reason)}})}),describe("previewFilteredText",()=>{it("returns empty string on empty input",()=>{expect(previewFilteredText("")).toBe("")}),it("escapes newlines / tabs and collapses runs of whitespace",()=>{expect(previewFilteredText("HEARTBEAT_OK\n[meta]")).toBe("HEARTBEAT_OK\\n[meta]"),expect(previewFilteredText("a\t\tb")).toBe("a\\t\\tb"),expect(previewFilteredText("a b c")).toBe("a b c")}),it("returns the full text when within maxLen",()=>{expect(previewFilteredText("HEARTBEAT_OK",20)).toBe("HEARTBEAT_OK")}),it("keeps head + tail with … when longer than maxLen",()=>{const e="A".repeat(50)+"HEARTBEAT_OK"+"B".repeat(50),t=previewFilteredText(e,40);expect(t.length).toBeLessThanOrEqual(40),expect(t).toContain("…"),expect(t.startsWith("A")).toBe(!0),expect(t.endsWith("B")).toBe(!0)})});
@@ -0,0 +1 @@
1
+ import{randomUUID}from"node:crypto";export const A2A_PROTOCOL_FIELDS=["task_id","caller_protocol","correlation_id","a2a_task_id","a2a_correlation_id"];export function extractChatPrompt(e,t){const n=e.payload,i=e.metadata,o="string"==typeof n?.message?n.message:"";if(!o)return null;let a;if(Array.isArray(n?.files))a=n.files;else if(Array.isArray(n?.parts)){const e=n.parts.filter(e=>e&&"file"===e.type&&"string"==typeof e.file_url).map(e=>e.file_url);a=e.length>0?e:void 0}const s=e.channel_id??i?.channel_id??("string"==typeof n?.channel_id?n.channel_id:void 0),r=s&&s.length>0?s:void 0,l=("string"==typeof n?.context_id&&n.context_id.length>0?n.context_id:void 0)??(i?.context_id&&i.context_id.length>0?i.context_id:void 0);let d="string"==typeof n?.session_key?n.session_key:void 0;if(!d){const e="chat_message"===t.source?"invoke":"a2a",i="string"==typeof n?.channel?n.channel:e,o=l?`ctx-${l}`:r?`ch-${r}`:t.generateSuffix?.()??fallbackRandom();d=`agent:${t.localAgentId}:${i}:${o}`}const c=e.message_id??i?.message_id??("string"==typeof n?.message_id?n.message_id:void 0);return{message:o,files:a,channelId:r,contextId:l,sessionKey:d,messageId:c&&c.length>0?c:void 0}}function fallbackRandom(){return randomUUID()}
@@ -0,0 +1 @@
1
+ import{describe,it,expect}from"vitest";import{extractChatPrompt,A2A_PROTOCOL_FIELDS}from"./chat-envelope.js";const LOCAL_AGENT_ID="agent-local-test",FIXED_SUFFIX=()=>"fixed-suffix";describe("extractChatPrompt — chat_message envelope",()=>{it("returns null when no message is present",()=>{const e=extractChatPrompt({type:"chat_message",payload:{parts:[]}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e).toBeNull()}),it("extracts message, channelId and contextId from a native envelope",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-123",payload:{message:"hello",context_id:"ctx-42",parts:[{type:"text",text:"hello"},{type:"file",file_url:"https://cdn/a.png"}]}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e).not.toBeNull(),expect(e.message).toBe("hello"),expect(e.files).toEqual(["https://cdn/a.png"]),expect(e.channelId).toBe("ch-123"),expect(e.contextId).toBe("ctx-42"),expect(e.sessionKey).toBe(`agent:${LOCAL_AGENT_ID}:invoke:ctx-ctx-42`)}),it("generates a fresh session key when neither context_id nor channel_id is supplied",()=>{const e=extractChatPrompt({type:"chat_message",payload:{message:"hi"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.sessionKey).toBe(`agent:${LOCAL_AGENT_ID}:invoke:fixed-suffix`),expect(e.channelId).toBeUndefined()}),it("uses ch-{channelId} suffix when only channel_id is present",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-stable",payload:{message:"hi"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.channelId).toBe("ch-stable"),expect(e.sessionKey).toBe(`agent:${LOCAL_AGENT_ID}:invoke:ch-ch-stable`),expect(e.sessionKey.includes("fixed-suffix")).toBe(!1)}),it("prefers ctx-{contextId} over ch-{channelId} when both are present",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-stable",payload:{message:"hi",context_id:"ctx-pinned"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.sessionKey).toBe(`agent:${LOCAL_AGENT_ID}:invoke:ctx-ctx-pinned`),expect(e.channelId).toBe("ch-stable")}),it("does NOT surface any A2A protocol header even if upstream leaks one",()=>{const e={message:"hello"};for(const t of A2A_PROTOCOL_FIELDS)e[t]=`leaked:${t}`;const t=extractChatPrompt({type:"chat_message",payload:e,metadata:{task_id:"leaked-task",caller_protocol:"a2a",correlation_id:"leaked-corr"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(t).not.toBeNull();const a=new Set(["message","files","channelId","contextId","sessionKey","messageId"]);for(const e of Object.keys(t))expect(a.has(e)).toBe(!0);for(const e of A2A_PROTOCOL_FIELDS)expect(t.sessionKey.includes(`leaked:${e}`)).toBe(!1),expect(t.message.includes(`leaked:${e}`)).toBe(!1)}),it("legacy agent_request envelopes still parse, but default channel stays `a2a`",()=>{const e=extractChatPrompt({type:"agent_request",payload:{message:"legacy",files:["/tmp/a.txt"]}},{source:"agent_request",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.message).toBe("legacy"),expect(e.files).toEqual(["/tmp/a.txt"]),expect(e.sessionKey).toBe(`agent:${LOCAL_AGENT_ID}:a2a:fixed-suffix`)}),it("respects an explicit session_key from legacy callers",()=>{const e=extractChatPrompt({type:"agent_request",payload:{message:"legacy",session_key:"agent:legacy:pre-existing"}},{source:"agent_request",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.sessionKey).toBe("agent:legacy:pre-existing")}),it("surfaces envelope.message_id as the generic IM request/response correlator",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-1",message_id:"msg-envelope",payload:{message:"hi",message_id:"msg-payload"},metadata:{message_id:"msg-meta"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.messageId).toBe("msg-envelope")}),it("falls back to metadata.message_id when the envelope field is absent",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-1",payload:{message:"hi"},metadata:{message_id:"msg-meta"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.messageId).toBe("msg-meta")}),it("normalizes empty string message_id to undefined",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"ch-1",message_id:"",payload:{message:"hi"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.messageId).toBeUndefined()}),it("prefers envelope.channel_id over metadata over payload",()=>{const e=extractChatPrompt({type:"chat_message",channel_id:"envelope-ch",payload:{message:"m",channel_id:"payload-ch"},metadata:{channel_id:"meta-ch"}},{source:"chat_message",localAgentId:LOCAL_AGENT_ID,generateSuffix:FIXED_SUFFIX});expect(e.channelId).toBe("envelope-ch")})});
@@ -1 +1 @@
1
- import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;_resubscribeTimer=null;_resubscribeAttempts=0;static MAX_RESUBSCRIBE_ATTEMPTS=5;_reconnectCount=0;_lastDisconnectTs=0;_tokenRefreshCount=0;_onConnectedCallback=null;logger;constructor(e,t){this.config=e,this.logger=t}async getToken(e,t,n){const s={principal_id:e};return t&&(s.claims=t),n&&(s.ttl_seconds=n),this.restRequest("POST","/api/v1/tokens",s)}async send(e,t,n){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:t,payload:n})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,t,n){const s={url:e,events:t};return n&&(s.secret=n),this.restRequest("POST","/api/v1/webhooks",s)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,t){this.client&&(this.logger.info?.("[message-sdk] Disconnecting previous client before reconnect"),await this.disconnect()),this.principalId=e,this._reconnectCount=0,this._lastDisconnectTs=0,this._tokenRefreshCount=0;const n=t?.tokenProvider??(e=>this.getToken(e));let s=t?.centrifugoUrl||this.config.centrifugoUrl;if(!s)try{s=(await n(e)).centrifugo_url}catch(e){this.logger.warn?.(`[message-sdk] Token probe failed, cannot discover Centrifugo URL: ${e instanceof Error?e.message:String(e)}`)}if(!s)throw new Error("No Centrifugo URL available. Set centrifugoUrl in config, ConnectOptions, or ensure the token provider is reachable at startup.");const i=`personal:${e}`;this.logger.info?.(`[message-sdk] Connecting as principal=${e} url=${s}`);this.client=new Centrifuge(s,{getToken:async()=>{this._tokenRefreshCount++,this.logger.info?.(`[message-sdk] Token fetch #${this._tokenRefreshCount} for principal=${e}`);return(await n(e)).token},websocket:WebSocket}),this.client.on("connecting",e=>{this.logger.info?.(`[message-sdk] Connecting: code=${e.code} reason=${e.reason}`)}),this.client.on("connected",e=>{if(this._reconnectCount>0){const t=this._lastDisconnectTs>0?Date.now()-this._lastDisconnectTs:0;this.logger.info?.(`[message-sdk] Reconnected (client=${e.client}) reconnects=${this._reconnectCount} downtime=${t}ms`)}else this.logger.info?.(`[message-sdk] Connected (client=${e.client})`);if(this._onConnectedCallback){try{this._onConnectedCallback()}catch{}this._onConnectedCallback=null}}),this.client.on("disconnected",e=>{this._reconnectCount++,this._lastDisconnectTs=Date.now(),this.logger.warn?.(`[message-sdk] Disconnected: code=${e.code} reason=${e.reason} totalDisconnects=${this._reconnectCount}`)}),this.client.on("error",e=>{this.logger.error?.("[message-sdk] Error:",e.error)}),this.client.on("subscribed",e=>{e.channel===i&&this.logger.info?.(`[message-sdk] Subscribed channel=${i} recoverable=${e.recoverable} recovered=${e.recovered}`)}),this.client.on("subscribing",e=>{e.channel===i&&this.logger.info?.(`[message-sdk] Subscribing channel=${i}`)}),this.client.on("unsubscribed",e=>{e.channel===i&&this.logger.warn?.(`[message-sdk] Unsubscribed channel=${i}`)}),this.client.on("publication",e=>{e.channel===i&&this.dispatch(e.data).catch(e=>{this.logger.error?.("[message-sdk] dispatch error:",e)})}),this.client.connect()}async disconnect(){this._resubscribeTimer&&(clearTimeout(this._resubscribeTimer),this._resubscribeTimer=null),this._resubscribeAttempts=0,this.personalSub&&(this.personalSub.unsubscribe(),this.personalSub=null),this.client?.disconnect(),this.client=null}onMessage(e,t){this.handlers.set(e,t)}onceConnected(e){this._onConnectedCallback=e}async joinGroup(e,t){if(!this.client)throw new Error("Not connected");const n=this.client.newSubscription(`group:${e}`);return n.on("publication",n=>{t(n.data).catch(t=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,t)})}),n.subscribe(),n}async publishToGroup(e,t){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:t,timestamp:Date.now()})}async sendToChannel(e,t,n){const s=`ch:${e}`,i={type:t,payload:n,channel_id:e,metadata:{from:this.principalId,channel_id:e,timestamp:String(Date.now())}};if("connected"===this.client?.state)return await this.client.publish(s,i),void this.logger.debug?.(`[message-sdk] Published type=${t} to ${s}`);if(this.config.serviceUrl)return this.logger.warn?.(`[message-sdk] Centrifugo not connected, falling back to REST for ch:${e}`),void await this.restRequest("POST",`/api/v1/channels/${encodeURIComponent(e)}/messages`,{sender_id:this.principalId,type:t,payload:n});throw new Error(`Cannot send to channel ${e}: no Centrifugo connection and no serviceUrl`)}async sendToPersonal(e,t,n){if(!this.client)throw new Error("Not connected");const s=`personal:${e}`,i={type:t,payload:n,metadata:{from:this.principalId,timestamp:String(Date.now())}};try{await this.client.publish(s,i),this.logger.debug?.(`[message-sdk] Published type=${t} to ${s}`)}catch(e){throw this.logger.warn?.(`[message-sdk] Publish failed type=${t} channel=${s}: ${e instanceof Error?e.message:String(e)}`),e}}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}async dispatch(e){const t=this.handlers.get(e.type);t?await t(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,t,n){if(!this.config.serviceUrl)throw new Error("Message Service REST API requires serviceUrl in config");const s=`${this.config.serviceUrl}${t}`,i={},o={method:e,headers:i};void 0!==n&&(i["Content-Type"]="application/json",o.body=JSON.stringify(n));const r=await fetch(s,o);if(!r.ok){const n=await r.text().catch(()=>"");throw new Error(`Message Service ${e} ${t} returned ${r.status}: ${n}`)}if(204!==r.status)return await r.json()}}
1
+ import{Centrifuge}from"centrifuge";import WebSocket from"ws";export class MessageSDK{config;client=null;personalSub=null;principalId="";handlers=new Map;_resubscribeTimer=null;_resubscribeAttempts=0;static MAX_RESUBSCRIBE_ATTEMPTS=5;_reconnectCount=0;_lastDisconnectTs=0;_tokenRefreshCount=0;_onConnectedCallback=null;logger;constructor(e,n){this.config=e,this.logger=n}async getToken(e,n,t){const s={principal_id:e};return n&&(s.claims=n),t&&(s.ttl_seconds=t),this.restRequest("POST","/api/v1/tokens",s)}async send(e,n,t){await this.restRequest("POST","/api/v1/messages/send",{target_id:e,message_type:n,payload:t})}async isOnline(e){return(await this.restRequest("GET",`/api/v1/connections/${encodeURIComponent(e)}/online`)).online}async registerWebhook(e,n,t){const s={url:e,events:n};return t&&(s.secret=t),this.restRequest("POST","/api/v1/webhooks",s)}async removeWebhook(e){await this.restRequest("DELETE",`/api/v1/webhooks/${encodeURIComponent(e)}`)}async connect(e,n){this.client&&(this.logger.info?.("[message-sdk] Disconnecting previous client before reconnect"),await this.disconnect()),this.principalId=e,this._reconnectCount=0,this._lastDisconnectTs=0,this._tokenRefreshCount=0;const t=n?.tokenProvider??(e=>this.getToken(e));let s=n?.centrifugoUrl||this.config.centrifugoUrl;if(!s)try{s=(await t(e)).centrifugo_url}catch(e){this.logger.warn?.(`[message-sdk] Token probe failed, cannot discover Centrifugo URL: ${e instanceof Error?e.message:String(e)}`)}if(!s)throw new Error("No Centrifugo URL available. Set centrifugoUrl in config, ConnectOptions, or ensure the token provider is reachable at startup.");const i=`personal:${e}`;this.logger.info?.(`[message-sdk] Connecting as principal=${e} url=${s}`);this.client=new Centrifuge(s,{getToken:async()=>{this._tokenRefreshCount++,this.logger.info?.(`[message-sdk] Token fetch #${this._tokenRefreshCount} for principal=${e}`);return(await t(e)).token},websocket:WebSocket}),this.client.on("connecting",e=>{this.logger.info?.(`[message-sdk] Connecting: code=${e.code} reason=${e.reason}`)}),this.client.on("connected",e=>{if(this._reconnectCount>0){const n=this._lastDisconnectTs>0?Date.now()-this._lastDisconnectTs:0;this.logger.info?.(`[message-sdk] Reconnected (client=${e.client}) reconnects=${this._reconnectCount} downtime=${n}ms`)}else this.logger.info?.(`[message-sdk] Connected (client=${e.client})`);if(this._onConnectedCallback){try{this._onConnectedCallback()}catch{}this._onConnectedCallback=null}}),this.client.on("disconnected",e=>{this._reconnectCount++,this._lastDisconnectTs=Date.now(),this.logger.warn?.(`[message-sdk] Disconnected: code=${e.code} reason=${e.reason} totalDisconnects=${this._reconnectCount}`)}),this.client.on("error",e=>{this.logger.error?.("[message-sdk] Error:",e.error)}),this.client.on("subscribed",e=>{e.channel===i&&this.logger.info?.(`[message-sdk] Subscribed channel=${i} recoverable=${e.recoverable} recovered=${e.recovered}`)}),this.client.on("subscribing",e=>{e.channel===i&&this.logger.info?.(`[message-sdk] Subscribing channel=${i}`)}),this.client.on("unsubscribed",e=>{e.channel===i&&this.logger.warn?.(`[message-sdk] Unsubscribed channel=${i}`)}),this.client.on("publication",e=>{e.channel===i&&this.dispatch(e.data).catch(n=>{const t=e?.data?.type??"(unknown)",s=n instanceof Error?n.message:String(n);this.logger.error?.(`[message-sdk] dispatch failed envType=${t}: ${s}`)})}),this.client.connect()}async disconnect(){this._resubscribeTimer&&(clearTimeout(this._resubscribeTimer),this._resubscribeTimer=null),this._resubscribeAttempts=0,this.personalSub&&(this.personalSub.unsubscribe(),this.personalSub=null),this.client?.disconnect(),this.client=null}onMessage(e,n){this.handlers.set(e,n)}onceConnected(e){this._onConnectedCallback=e}async joinGroup(e,n){if(!this.client)throw new Error("Not connected");const t=this.client.newSubscription(`group:${e}`);return t.on("publication",t=>{n(t.data).catch(n=>{this.logger.error?.(`[message-sdk] group handler error (${e}):`,n)})}),t.subscribe(),t}async publishToGroup(e,n){if(!this.client)throw new Error("Not connected");await this.client.publish(`group:${e}`,{from:this.principalId,content:n,timestamp:Date.now()})}async sendToChannel(e,n,t,s){const i=`ch:${e}`,o={type:n,payload:t,channel_id:e,metadata:{from:this.principalId,channel_id:e,timestamp:String(Date.now())}};if(s?.inReplyTo&&(o.in_reply_to=s.inReplyTo),s?.messageId&&(o.message_id=s.messageId),s?.idempotencyKey&&(o.idempotency_key=s.idempotencyKey),"connected"===this.client?.state)return await this.client.publish(i,o),void this.logger.debug?.(`[message-sdk] Published type=${n} to ${i}`);if(this.config.serviceUrl){this.logger.warn?.(`[message-sdk] Centrifugo not connected, falling back to REST for ch:${e}`);const i={sender_id:this.principalId,type:n,payload:t};return s?.inReplyTo&&(i.in_reply_to=s.inReplyTo),s?.messageId&&(i.message_id=s.messageId),s?.idempotencyKey&&(i.idempotency_key=s.idempotencyKey),void await this.restRequest("POST",`/api/v1/channels/${encodeURIComponent(e)}/messages`,i)}throw new Error(`Cannot send to channel ${e}: no Centrifugo connection and no serviceUrl`)}async sendChannelMessage(e,n){if(!e)throw new Error("channelId is required");if(!n.type)throw new Error("type is required");const t={sender_id:n.senderId??this.principalId,type:n.type,payload:n.payload};if(!t.sender_id)throw new Error("senderId required (and no principalId is known)");return n.messageId&&(t.message_id=n.messageId),n.inReplyTo&&(t.in_reply_to=n.inReplyTo),n.idempotencyKey&&(t.idempotency_key=n.idempotencyKey),this.restRequest("POST",`/api/v1/channels/${encodeURIComponent(e)}/messages`,t)}async replyToMessage(e,n,t,s){const i=e.channel_id??e.metadata?.channel_id;if(!i)throw new Error("replyToMessage: envelope has no channel_id");const o=e.message_id??e.metadata?.message_id??e.metadata?.invocation_id??e.correlationId;if(!o)throw new Error("replyToMessage: envelope has no message_id / invocation_id / correlationId");return this.sendChannelMessage(i,{type:n,payload:t,inReplyTo:o,idempotencyKey:s?.idempotencyKey,messageId:s?.messageId})}async listChannelMessages(e,n){if(!e)throw new Error("channelId is required");const t=new URLSearchParams;void 0!==n?.since&&t.set("since",String(n.since)),void 0!==n?.limit&&t.set("limit",String(n.limit)),n?.type&&t.set("type",n.type);const s=t.toString(),i=`/api/v1/channels/${encodeURIComponent(e)}/messages${s?`?${s}`:""}`;return this.restRequest("GET",i)}async waitForReply(e,n){if(!e)throw new Error("channelId is required");if(!n.inReplyTo)throw new Error("inReplyTo is required");const t={in_reply_to:n.inReplyTo};return void 0!==n.timeoutMs&&(t.timeout_ms=n.timeoutMs),void 0!==n.since&&(t.since=n.since),this.restRequest("POST",`/api/v1/channels/${encodeURIComponent(e)}/wait`,t)}async sendToPersonal(e,n,t){if(!this.client)throw new Error("Not connected");const s=`personal:${e}`,i={type:n,payload:t,metadata:{from:this.principalId,timestamp:String(Date.now())}};try{await this.client.publish(s,i),this.logger.debug?.(`[message-sdk] Published type=${n} to ${s}`)}catch(e){throw this.logger.warn?.(`[message-sdk] Publish failed type=${n} channel=${s}: ${e instanceof Error?e.message:String(e)}`),e}}get isConnected(){return"connected"===this.client?.state}get currentPrincipalId(){return this.principalId}async dispatch(e){const n=this.handlers.get(e.type);n?await n(e):this.logger.debug?.(`[message-sdk] No handler for type=${e.type}`)}async restRequest(e,n,t){if(!this.config.serviceUrl)throw new Error("Message Service REST API requires serviceUrl in config");const s=`${this.config.serviceUrl}${n}`,i={},o={method:e,headers:i};void 0!==t&&(i["Content-Type"]="application/json",o.body=JSON.stringify(t));const r=await fetch(s,o);if(!r.ok){const t=await r.text().catch(()=>"");throw new Error(`Message Service ${e} ${n} returned ${r.status}: ${t}`)}if(204!==r.status)return await r.json()}}
@@ -0,0 +1 @@
1
+ import{spawn}from"node:child_process";const TIMEOUT_MS=15e3;export class OpenClawCliError extends Error{exitCode;stderr;constructor(e,r,t){super(e),this.exitCode=r,this.stderr=t,this.name="OpenClawCliError"}}function resolveBinary(){const e=process.env.OPENCLAW_BIN?.trim();if(e){const r=e.split(/\s+/);return{cmd:r[0],baseArgs:r.slice(1)}}return{cmd:"openclaw",baseArgs:[]}}const FALLBACK_CMD="node",FALLBACK_ARGS=["/app/openclaw.mjs"];function spawnOnce(e,r){return new Promise((t,n)=>{let o="",s="",a=!1;const i=spawn(e,r,{stdio:["ignore","pipe","pipe"]}),c=setTimeout(()=>{if(!a){a=!0;try{i.kill("SIGKILL")}catch{}n(new OpenClawCliError("openclaw CLI timed out after 15000ms",null,s))}},15e3);i.stdout.on("data",e=>o+=String(e)),i.stderr.on("data",e=>s+=String(e)),i.on("error",e=>{if(a)return;a=!0,clearTimeout(c);const r=new OpenClawCliError(`openclaw CLI spawn failed: ${e.message}`,null,s);r.code=e.code,n(r)}),i.on("exit",e=>{a||(a=!0,clearTimeout(c),0===e?t({stdout:o,stderr:s}):n(new OpenClawCliError(`openclaw CLI exited with code ${e}${s?`: ${s.trim()}`:""}`,e,s)))})})}async function runCli(e){const r=resolveBinary();try{return await spawnOnce(r.cmd,[...r.baseArgs,...e])}catch(r){const t=r?.code;if("ENOENT"===t&&!process.env.OPENCLAW_BIN)return spawnOnce("node",[...FALLBACK_ARGS,...e]);throw r}}let queueTail=Promise.resolve();function enqueue(e){const r=queueTail.catch(()=>{}).then(e);return queueTail=r.catch(()=>{}),r}const SERVER_NAME_RE=/^[a-zA-Z0-9_-]{1,64}$/;export function validateServerName(e){if(!SERVER_NAME_RE.test(e))throw new Error("Invalid MCP server name (allowed: letters, digits, underscore, hyphen, 1-64 chars)")}export async function listMcpServers(){return enqueue(async()=>{const{stdout:e}=await runCli(["mcp","list","--json"]),r=e.trim();if(!r)return{};try{const e=JSON.parse(r);return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}catch(r){throw new OpenClawCliError(`Failed to parse 'openclaw mcp list --json' output: ${r.message}`,0,e)}})}export async function setMcpServer(e,r){if(validateServerName(e),!r||"object"!=typeof r)throw new Error("MCP server config must be an object");return enqueue(async()=>{await runCli(["mcp","set",e,JSON.stringify(r)])})}export async function unsetMcpServer(e){return validateServerName(e),enqueue(async()=>{await runCli(["mcp","unset",e])})}
package/dist/status.js ADDED
@@ -0,0 +1 @@
1
+ import{createHash}from"node:crypto";export function handleStatusRequest(e,t,n){const o=t,s=(e.method??"GET").toUpperCase();if("GET"!==s&&"HEAD"!==s)return o.statusCode=405,o.setHeader("Allow","GET, HEAD"),void o.end();const r=n.publicKey?createHash("sha256").update(n.publicKey).digest("hex"):null,a={plugin:"beeos-claw",plugin_version:n.pluginVersion,fingerprint:r};o.statusCode=200,o.setHeader("Content-Type","application/json"),o.setHeader("Cache-Control","no-store"),"HEAD"!==s?o.end(JSON.stringify(a)):o.end()}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
5
5
  "license": "MIT",
6
6
  "author": "BeeOS <dev@beeos.ai>",