beeos-claw 0.6.14 → 0.6.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";export function lifecycleEndFailureMessage(e){if(!e)return;const t=!0===e.isError||!0===e.is_error,s=e.error,o=s&&"object"==typeof s?s:void 0,n="string"==typeof s||void 0!==o||"string"==typeof e.message||"string"==typeof e.reason;if(t||n){if(!t){const t="string"==typeof e.stopReason?e.stopReason:"";if("error"!==t&&"timeout"!==t&&"aborted"!==t)return}return("string"==typeof e.message?e.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof e.reason?e.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error"}}function asString(e){if("string"==typeof e){return e.trim()||void 0}}function isRecord(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function normalizeText(e){if("string"==typeof e)return normalizeTransportText(e.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(e,...t){const s=`${e}:${t.join(":")}`;if(s.length<=256)return s;return`${e}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(e){if(!(null==e||e<=0))try{return new Date(e).toISOString()}catch{return}}function resolveToolArgs(e){const t=e.arguments??e.args??e.input;if(isRecord(t))return t;if("string"==typeof t){const e=t.trim();if(e.startsWith("{")||e.startsWith("["))try{return JSON.parse(e)}catch{}}return{}}export function resolveTurnId(e){if(e.currentRunId)return e.currentRunId;const t=e.pendingPromptId;return void 0!==t?"string"==typeof t?`req:${t}`:`req:${String(t)}`:void 0}export function roleFromSessionUpdate(e){const t=e.sessionUpdate;if(t)return"user_message_chunk"===t?"user":"system_message_chunk"===t?"system":t.startsWith("agent_")||t.startsWith("tool_")?"assistant":"history_turn"===t?"user"===e.role?"user":"system"===e.role?"system":"assistant":void 0}export function recordTurnIdForRole(e,t,s){if(!s)return;const o=roleFromSessionUpdate(t);o&&(e.lastTurnIdByRole||(e.lastTurnIdByRole={}),e.lastTurnIdByRole[o]=s)}export function recordToolCallTurnForUpdate(e,t,s){s&&"tool_call"===t.sessionUpdate&&t.toolCallId&&(e.toolCallTurns??=new Map,e.toolCallTurns.set(t.toolCallId,{turnId:s,..."string"==typeof t.name&&t.name?{toolName:t.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;interruptPrompt;schedulePromptTimeout;adoptBackgroundMutation;abortGatewayRun;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.completePrompt=e.completePrompt,this.failPrompt=e.failPrompt,this.interruptPrompt=e.interruptPrompt,this.schedulePromptTimeout=e.schedulePromptTimeout,this.adoptBackgroundMutation=e.adoptBackgroundMutation,this.abortGatewayRun=e.abortGatewayRun,this.maybeCompletePromptFromChatState=e.maybeCompletePromptFromChatState,this.agentFetchFn=e.agentFetch,this.getMessageClient=e.getMessageClient,this.getMsgPrincipalId=e.getMsgPrincipalId,this.gatewayRequest=e.gatewayRequest,this.getCanvasState=e.getCanvasState,this.cronChannelCache=e.cronChannelCache}observeLifecycle(e,t,s){observeLifecyclePromise(e,this.logger,{component:"gateway-events",action:t,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(e){e.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(e)}resetStreamIdleTimer(e){this.state.clearStreamIdleTimer(e);const t=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;e.streamIdleTimer=setTimeout(()=>{if(!e.lastStreamActivityTs)return;const t=Date.now()-e.lastStreamActivityTs;t>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${e.sessionId} stale=${t}ms`),this.observeLifecycle(this.interruptPrompt(e,classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${t}ms`,source:"live_gateway",runtimeKind:"openclaw"})),"stream_stale_timeout",e))},t),"object"==typeof e.streamIdleTimer&&"unref"in e.streamIdleTimer&&e.streamIdleTimer.unref()}sendTurnUpdate(e,t){const s=resolveTurnId(e);return s&&void 0===t._turnId&&(t._turnId=s),recordTurnIdForRole(e,t,s),recordToolCallTurnForUpdate(e,t,s),!!e.sessionLoadInProgress||this.transport.sendSessionUpdate(e.sessionId,t,{turnId:s})}sendToolCallSpacer(e,t){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...t?{timestamp:t}:{}};this.sendTurnUpdate(e,s),e.assistantMq?.push(s)}resolveToolCallId(e,t,s){const o=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.callId)??asString(t.id);if(o){const t=this.state.toolCallIdMap.get(o);return t?(e.sessionId&&this.state.toolCallToSessionMap.set(t,e.sessionId),t):(this.state.toolCallIdMap.set(o,o),e.sessionId&&this.state.toolCallToSessionMap.set(o,e.sessionId),o)}const n=`tc_${e.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return e.sessionId&&this.state.toolCallToSessionMap.set(n,e.sessionId),n}handleChatEvent(e){const t=(e.runId?this.state.promptsByRunId.get(e.runId):void 0)??this.state.findSessionByKey(e.sessionKey);if(!t)return;if(!this.state.isCurrentStreamOwner(t,e.runId)){const s=!t.currentRunId&&void 0===t.pendingPromptId;if(!e.runId||!s||this.state.isRunCompleted(e.runId))return;if(!this.adoptBackgroundRun(t,e.runId))return}t.sessionId;const s=e.message;if(s){this.touchStreamActivity(t);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 i=toISO(s.timestamp);if("thinking"===a){if(t.hasAgentThinkingStream)continue;const e=normalizeText(n.thinking)??normalizeText(n.text);if(!e)continue;const s=dedupKey("chat:thinking",e);if(t.chatReplayDedupKeys.has(s))continue;t.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...i?{timestamp:i}:{}};this.sendTurnUpdate(t,o)&&t.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(t,i),t.hasAgentToolStartStream)continue;const e=this.resolveToolCallId(t,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",e,JSON.stringify(o));if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const r=buildToolCallPayload({toolCallId:e,toolName:s,args:o,timestamp:i});this.sendTurnUpdate(t,r)&&t.assistantMq?.push(r);continue}if("toolResult"===a){if(this.sendToolCallSpacer(t,i),t.hasAgentToolResultStream)continue;const e=this.resolveToolCallId(t,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 r=dedupKey("chat:tool_result",e,a);if(t.chatReplayDedupKeys.has(r))continue;t.chatReplayDedupKeys.add(r),this.emitNormalizedToolResult(t,{toolCallId:e,toolName:s,rawText:a,rawResult:o,timestamp:i}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)});continue}if("text"===a&&n.text){if(t.hasAgentAssistantStream||t.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:t.sessionId,runId:e.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const r=t.chatAssistantTextSoFar??"";let l;if(t.chatAssistantTextSoFar=s,s.startsWith(r))l=s.slice(r.length);else{if(r.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${e.seq} runId=${e.runId} len=${l.length} prevLen=${r.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...i?{timestamp:i}:{}};this.sendTurnUpdate(t,c),t.assistantMq?.push(c)}}}}else"delta"===e.state&&this.touchStreamActivity(t);if(this.maybeCompletePromptFromChatState(t,e),!t.promptDone)if("final"===e.state)this.observeLifecycle(this.completePrompt(t,e.stopReason||"end_turn"),"chat_final",t);else if("aborted"===e.state||"cancelled"===e.state||"cancel"===e.state)this.observeLifecycle(this.completePrompt(t,"cancelled"),"chat_cancelled",t);else if("error"===e.state){const s=e.errorMessage||Errors.AgentError.message,o=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});"interrupted"===o.kind?this.observeLifecycle(this.interruptPrompt(t,o),"chat_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,o.publicMessage),"chat_failure",t)}}handleAgentEvent(e){const t=(e.runId?this.state.promptsByRunId.get(e.runId):void 0)??(e.sessionKey?this.state.findSessionByKey(e.sessionKey):void 0);if(!t)return;if(!this.state.isCurrentStreamOwner(t,e.runId)){const s=!t.currentRunId&&void 0===t.pendingPromptId;if(!e.runId||!s||this.state.isRunCompleted(e.runId))return;if(!this.adoptBackgroundRun(t,e.runId))return}e.runId&&t.cronJobId&&!this.state.runIdToCronJobId.has(e.runId)&&this.state.setRunCronJobId(e.runId,t.cronJobId);t.sessionId;this.touchStreamActivity(t);const s=toISO(e.ts),o=e=>{this.sendTurnUpdate(t,e),t.assistantMq?.push(e)};if("assistant"===e.stream){t.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(t,e.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=t.agentAssistantTextSoFar??a,i=classifyHeartbeatFilter(n);i.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:i.reason,sessionId:t.sessionId,runId:e.runId,cumulative:n,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${e.seq} runId=${e.runId} len=${a.length} source=${e.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],i=e.data;i&&(Array.isArray(i.content)?a.push(...i.content):isRecord(i.content)?a.push(i.content):"string"==typeof i.type&&a.push(i));for(const e of a){if(!isRecord(e))continue;const n=asString(e.type);if("thinking"===n){t.hasAgentThinkingStream=!0;const o=normalizeText(e.thinking)??normalizeText(e.text);if(o){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}continue}if("toolCall"===n){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(t,e,"start"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",i=resolveToolArgs(e);o(buildToolCallPayload({toolCallId:n,toolName:a,args:i,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0;const n=this.resolveToolCallId(t,e,"result"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",i=e.result,r=normalizeText(e.text)??normalizeText(e.result)??(isRecord(i)||Array.isArray(i)?JSON.stringify(i,null,2):void 0);r&&this.emitNormalizedToolResult(t,{toolCallId:n,toolName:a,rawText:r,rawResult:i,timestamp:s,pushUpdate:o}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)});continue}}}else if("thinking"===e.stream){t.hasAgentThinkingStream=!0;const o=normalizeText(e.data?.delta)??normalizeText(e.data?.text);if(o){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}else if("tool"===e.stream){const n=e.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const e=this.resolveToolCallId(t,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",i=resolveToolArgs(n);o(buildToolCallPayload({toolCallId:e,toolName:a,args:i,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0,this.handleToolResult(t,e,s).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${e instanceof Error?e.message:e}`)});else{const e=n?.toolCall;if(e){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const a=asString(e?.name)??asString(n?.name)??asString(n?.toolName)??"tool",i={...n,...e},r=this.resolveToolCallId(t,i,"start"),l=resolveToolArgs(i);o(buildToolCallPayload({toolCallId:r,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===e.stream){const s=e.data,o="string"==typeof s?.phase?s.phase:void 0,n=(o,n)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${e.runId} sessionKey=${e.sessionKey} message=${o} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:o,source:"live_gateway",runtimeKind:"openclaw"});"interrupted"===a.kind?this.observeLifecycle(this.interruptPrompt(t,a),"agent_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,a.publicMessage),n,t)};if("end"===o){const e=lifecycleEndFailureMessage(s);if(e)n(e,"agent_failure");else{const e="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.completePrompt(t,e||"end_turn"),"agent_final",t)}}else if("cancelled"===o||"cancel"===o)this.observeLifecycle(this.completePrompt(t,"cancelled"),"agent_cancelled",t);else if("error"===o){const e=s?.error,t=e&&"object"==typeof e?e:void 0;n(("string"==typeof s?.message?s.message:void 0)??("string"==typeof e?e:void 0)??("string"==typeof t?.message?t.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(t?JSON.stringify(t):void 0)??"gateway lifecycle error","agent_failure")}}}async handleToolResult(e,t,s){const o=t.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),i=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${t.runId}_${t.seq}`,r=("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,c=a??i,d=o?.result;await this.emitNormalizedToolResult(e,{toolCallId:i,normalizeToolCallId:c,toolName:r,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(e,t){const s=t.pushUpdate??(t=>{this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)});await emitNormalizedToolResultForTurn({session:e,envelope:{toolCallId:t.toolCallId,toolName:t.toolName,rawText:t.rawText,rawResult:t.rawResult,timestamp:t.timestamp},normalizeToolCallId:t.normalizeToolCallId,turnId:resolveTurnId(e),config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>(s(e),!0)})}adoptBackgroundRun(e,t){this.logger.debug?.(`[gateway-events] Adopting background run ${t} on session ${e.sessionId} (prev=${e.currentRunId})`);if(!this.adoptBackgroundMutation(e,t).ok)return this.observeLifecycle(this.abortGatewayRun(e.gatewaySessionKey,t),"background_run_conflict_abort",e),!1;e.currentRunId=t,e.promptDone=!1,this.state.promptsByRunId.set(t,e);const s=this.state.runIdToCronJobId.get(t);return s&&(e.cronJobId=s),e.assistantStreamStartedAt=Date.now(),e.lastStreamActivityTs=Date.now(),e.hasAgentAssistantStream=void 0,e.hasAgentThinkingStream=void 0,e.hasAgentToolStartStream=void 0,e.hasAgentToolResultStream=void 0,e.chatAssistantTextSoFar=void 0,e.agentAssistantTextSoFar=void 0,e.chatReplayDedupKeys?.clear(),e.presentedFileArtifactKeys=new Set,e.emittedToolResultKeys=new Set,e.assistantMq&&!e.assistantMq.closed||(e.assistantMq=new MessageQueue),this.schedulePromptTimeout(e),!0}handleCronEvent(e){this.transport.sendNotification("_beeos/cron_event",{payload:e});const t=e??{};"string"==typeof t.runId&&"string"==typeof t.jobId&&this.state.setRunCronJobId(t.runId,t.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(t),"added"===t.action&&this.maybeBackfillBeeosChannel(t),"finished"===t.action&&"string"==typeof t.summary&&t.summary&&this.publishCronResult(t)}async reportCronEventToAgentGateway(e){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const t={...e},s="string"==typeof e.sessionKey?e.sessionKey:void 0;s&&null==t.sessionKey&&(t.sessionKey=s);const o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(t.sessionId=o),("string"!=typeof t.name||!t.name)&&"string"==typeof e.jobId&&e.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)if(isRecord(s)&&s.id===e.jobId&&"string"==typeof s.name&&s.name){t.name=s.name;break}}catch(e){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${e instanceof Error?e.message:e}`)}const n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:t})})}catch(e){this.logger.warn?.(`[gateway-events] event report failed: ${e instanceof Error?e.message:e}`)}}async maybeBackfillBeeosChannel(e){const t="string"==typeof e.jobId?e.jobId:void 0;if(!t)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(t))return;let s;try{const e=await this.gatewayRequest("cron.list",{}),o=e?.jobs;if(Array.isArray(o))for(const e of o){if(!e||"object"!=typeof e)continue;const o=e;if(o.id===t){s=o;break}}}catch(e){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${e instanceof Error?e.message:e}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const e=decodeBeeosChannelId(o);return void(e&&this.setCronChannelCacheLocal(t,e))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const i="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!i)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${t}`);const r=this.state.findSessionByKey(i),l=r?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${i} job=${t}`);const c=encodeBeeosChannelTag(o,l);try{await this.gatewayRequest("cron.update",{id:t,patch:{description:c}}),this.setCronChannelCacheLocal(t,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${t} channel=${l}`)}catch(e){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${e instanceof Error?e.message:e}`)}}setCronChannelCacheLocal(e,t){this.cronChannelCache&&this.cronChannelCache.set(e,t)}async publishCronResult(e){const t=this.getMessageClient?.();if(!t)return;const s=this.getMsgPrincipalId?.();if(!s)return;const o="string"==typeof e.jobId?e.jobId:void 0;if(!o)return;const n=e.summary;let a=this.cronChannelCache?.get(o);if(!a&&this.gatewayRequest)try{const e=await this.gatewayRequest("cron.list",{}),t=e?.jobs;if(Array.isArray(t))for(const e of t){if(!e||"object"!=typeof e)continue;const t=e;if(t.id===o){a=decodeBeeosChannelId(t.description);break}}}catch(e){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${e instanceof Error?e.message:e}`)}if(!a)return;const i=randomUUID();try{await t.messages.send({conversationId:a,id:i,type:"agent_reply",content:{text:n,cron_job_id:o,session_key:e.sessionKey??void 0}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}catch(e){const t=e instanceof Error?e.message:String(e);if(isChannelGoneError(e,t))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${o} err=${t}`),void this.cronChannelCache?.delete(o);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${t}`)}}logHeartbeatSkip(e){const t=previewSanitizedText(e.cumulative),s=void 0!==e.delta?previewSanitizedText(e.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${e.source} reason=${e.reason} sessionId=${e.sessionId} runId=${e.runId??"n/a"} cumLen=${e.cumulative.length}`+(void 0!==e.delta?` deltaLen=${e.delta.length}`:"")+` cumulative="${t}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${e.source} drop (${e.reason})`,payload:{source:e.source,reason:e.reason,sessionId:e.sessionId,runId:e.runId,cumulativeLen:e.cumulative.length,cumulativePreview:t,...void 0!==e.delta?{deltaLen:e.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(e,t){const s=t.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(e)){const t=e.status??e.statusCode??e.code;if(404===t||410===t)return!0}return!1}
1
+ import{createHash,randomUUID}from"node:crypto";import{Errors}from"../errors/index.js";import{BEEOS_CHANNEL_TAG_RE,decodeBeeosChannelId,encodeBeeosChannelTag}from"./bridge-core.js";import{classifyAgentRuntimeFailure}from"../runtime/agent-runtime-failure-classifier.js";import{stripTransportMetadata,normalizeTransportText,previewSanitizedText}from"../message-sanitization/transport-metadata-sanitizer.js";import{classifyHeartbeatFilter}from"../message-sanitization/message-classifier.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{DEFAULT_STREAM_IDLE_TIMEOUT_MS,DEFAULT_STREAM_STALE_TIMEOUT_MS}from"./types.js";import{MessageQueue}from"../utils/mq.js";import{observeLifecyclePromise}from"../runtime/lifecycle-promise-observer.js";export function lifecycleEndFailureMessage(e){if(!e)return;const t=!0===e.isError||!0===e.is_error,s=e.error,o=s&&"object"==typeof s?s:void 0,n="string"==typeof s||void 0!==o||"string"==typeof e.message||"string"==typeof e.reason;if(t||n){if(!t){const t="string"==typeof e.stopReason?e.stopReason:"";if("error"!==t&&"timeout"!==t&&"aborted"!==t)return}return("string"==typeof e.message?e.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof o?.message?o.message:void 0)??("string"==typeof e.reason?e.reason:void 0)??(o?JSON.stringify(o):void 0)??"gateway lifecycle error"}}function asString(e){if("string"==typeof e){return e.trim()||void 0}}function isRecord(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function normalizeText(e){if("string"==typeof e)return normalizeTransportText(e.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(e,...t){const s=`${e}:${t.join(":")}`;if(s.length<=256)return s;return`${e}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(e){if(!(null==e||e<=0))try{return new Date(e).toISOString()}catch{return}}function resolveToolArgs(e){const t=e.arguments??e.args??e.input;if(isRecord(t))return t;if("string"==typeof t){const e=t.trim();if(e.startsWith("{")||e.startsWith("["))try{return JSON.parse(e)}catch{}}return{}}export function resolveTurnId(e){if(e.currentRunId)return e.currentRunId;const t=e.pendingPromptId;return void 0!==t?"string"==typeof t?`req:${t}`:`req:${String(t)}`:void 0}export function roleFromSessionUpdate(e){const t=e.sessionUpdate;if(t)return"user_message_chunk"===t?"user":"system_message_chunk"===t?"system":t.startsWith("agent_")||t.startsWith("tool_")?"assistant":"history_turn"===t?"user"===e.role?"user":"system"===e.role?"system":"assistant":void 0}export function recordTurnIdForRole(e,t,s){if(!s)return;const o=roleFromSessionUpdate(t);o&&(e.lastTurnIdByRole||(e.lastTurnIdByRole={}),e.lastTurnIdByRole[o]=s)}export function recordToolCallTurnForUpdate(e,t,s){s&&"tool_call"===t.sessionUpdate&&t.toolCallId&&(e.toolCallTurns??=new Map,e.toolCallTurns.set(t.toolCallId,{turnId:s,..."string"==typeof t.name&&t.name?{toolName:t.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;completePrompt;failPrompt;interruptPrompt;schedulePromptTimeout;adoptBackgroundMutation;abortGatewayRun;maybeCompletePromptFromChatState;agentFetchFn;getMessageClient;getMsgPrincipalId;gatewayRequest;getCanvasState;cronChannelCache;constructor(e){this.state=e.state,this.transport=e.transport,this.config=e.config,this.logger=e.logger,this.writeObs=e.writeObs,this.completePrompt=e.completePrompt,this.failPrompt=e.failPrompt,this.interruptPrompt=e.interruptPrompt,this.schedulePromptTimeout=e.schedulePromptTimeout,this.adoptBackgroundMutation=e.adoptBackgroundMutation,this.abortGatewayRun=e.abortGatewayRun,this.maybeCompletePromptFromChatState=e.maybeCompletePromptFromChatState,this.agentFetchFn=e.agentFetch,this.getMessageClient=e.getMessageClient,this.getMsgPrincipalId=e.getMsgPrincipalId,this.gatewayRequest=e.gatewayRequest,this.getCanvasState=e.getCanvasState,this.cronChannelCache=e.cronChannelCache}observeLifecycle(e,t,s){observeLifecyclePromise(e,this.logger,{component:"gateway-events",action:t,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(e){e.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(e)}resetStreamIdleTimer(e){this.state.clearStreamIdleTimer(e);const t=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;e.streamIdleTimer=setTimeout(()=>{if(!e.lastStreamActivityTs)return;const t=Date.now()-e.lastStreamActivityTs;t>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${e.sessionId} stale=${t}ms`),this.observeLifecycle(this.interruptPrompt(e,classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${t}ms`,source:"live_gateway",runtimeKind:"openclaw"})),"stream_stale_timeout",e))},t),"object"==typeof e.streamIdleTimer&&"unref"in e.streamIdleTimer&&e.streamIdleTimer.unref()}sendTurnUpdate(e,t){const s=resolveTurnId(e);return s&&void 0===t._turnId&&(t._turnId=s),recordTurnIdForRole(e,t,s),recordToolCallTurnForUpdate(e,t,s),!!e.sessionLoadInProgress||this.transport.sendSessionUpdate(e.sessionId,t,{turnId:s})}sendToolCallSpacer(e,t){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...t?{timestamp:t}:{}};this.sendTurnUpdate(e,s),e.assistantMq?.push(s)}resolveToolCallId(e,t,s){const o=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.callId)??asString(t.id);if(o){const t=this.state.toolCallIdMap.get(o);return t?(e.sessionId&&this.state.toolCallToSessionMap.set(t,e.sessionId),t):(this.state.toolCallIdMap.set(o,o),e.sessionId&&this.state.toolCallToSessionMap.set(o,e.sessionId),o)}const n=`tc_${e.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return e.sessionId&&this.state.toolCallToSessionMap.set(n,e.sessionId),n}handleChatEvent(e){const t=(e.runId?this.state.promptsByRunId.get(e.runId):void 0)??this.state.findSessionByKey(e.sessionKey);if(!t)return;if(!this.state.isCurrentStreamOwner(t,e.runId)){const s=!t.currentRunId&&void 0===t.pendingPromptId;if(!e.runId||!s||this.state.isRunCompleted(e.runId))return;if(!this.adoptBackgroundRun(t,e.runId))return}t.sessionId;const s=e.message;if(s){this.touchStreamActivity(t);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 i=toISO(s.timestamp);if("thinking"===a){if(t.hasAgentThinkingStream)continue;const e=normalizeText(n.thinking)??normalizeText(n.text);if(!e)continue;const s=dedupKey("chat:thinking",e);if(t.chatReplayDedupKeys.has(s))continue;t.chatReplayDedupKeys.add(s);const o={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...i?{timestamp:i}:{}};this.sendTurnUpdate(t,o)&&t.assistantMq?.push(o);continue}if("toolCall"===a){if(this.sendToolCallSpacer(t,i),t.hasAgentToolStartStream)continue;const e=this.resolveToolCallId(t,n,"start"),s=asString(n.name)??asString(n.toolName)??asString(n.tool_name)??"tool",o=resolveToolArgs(n),a=dedupKey("chat:tool_start",e,JSON.stringify(o));if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const r=buildToolCallPayload({toolCallId:e,toolName:s,args:o,timestamp:i});this.sendTurnUpdate(t,r)&&t.assistantMq?.push(r);continue}if("toolResult"===a){if(this.sendToolCallSpacer(t,i),t.hasAgentToolResultStream)continue;const e=this.resolveToolCallId(t,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 r=dedupKey("chat:tool_result",e,a);if(t.chatReplayDedupKeys.has(r))continue;t.chatReplayDedupKeys.add(r),this.emitNormalizedToolResult(t,{toolCallId:e,toolName:s,rawText:a,rawResult:o,timestamp:i}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)});continue}if("text"===a&&n.text){if(t.hasAgentAssistantStream||t.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:t.sessionId,runId:e.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const r=t.chatAssistantTextSoFar??"";let l;if(t.chatAssistantTextSoFar=s,s.startsWith(r))l=s.slice(r.length);else{if(r.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${e.seq} runId=${e.runId} len=${l.length} prevLen=${r.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...i?{timestamp:i}:{}};this.sendTurnUpdate(t,c),t.assistantMq?.push(c)}}}}else"delta"===e.state&&this.touchStreamActivity(t);if(this.maybeCompletePromptFromChatState(t,e),!t.promptDone)if("final"===e.state)this.observeLifecycle(this.completePrompt(t,e.stopReason||"end_turn"),"chat_final",t);else if("aborted"===e.state||"cancelled"===e.state||"cancel"===e.state)this.observeLifecycle(this.completePrompt(t,"cancelled"),"chat_cancelled",t);else if("error"===e.state){const s=e.errorMessage||Errors.AgentError.message,o=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});"interrupted"===o.kind?this.observeLifecycle(this.interruptPrompt(t,o),"chat_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,o.publicMessage),"chat_failure",t)}}handleAgentEvent(e){const t=(e.runId?this.state.promptsByRunId.get(e.runId):void 0)??(e.sessionKey?this.state.findSessionByKey(e.sessionKey):void 0);if(!t)return;if(!this.state.isCurrentStreamOwner(t,e.runId)){const s=!t.currentRunId&&void 0===t.pendingPromptId;if(!e.runId||!s||this.state.isRunCompleted(e.runId))return;if(!this.adoptBackgroundRun(t,e.runId))return}e.runId&&t.cronJobId&&!this.state.runIdToCronJobId.has(e.runId)&&this.state.setRunCronJobId(e.runId,t.cronJobId);t.sessionId;this.touchStreamActivity(t);const s=toISO(e.ts),o=e=>{this.sendTurnUpdate(t,e),t.assistantMq?.push(e)};if("assistant"===e.stream){t.hasAgentAssistantStream=!0;const n=this.state.extractAgentAssistantDelta(t,e.data??{});if(n){const a=normalizeTransportText(n);if(a){const n=t.agentAssistantTextSoFar??a,i=classifyHeartbeatFilter(n);i.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:i.reason,sessionId:t.sessionId,runId:e.runId,cumulative:n,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${e.seq} runId=${e.runId} len=${a.length} source=${e.data?.delta?"delta":"cumulative"}`),o({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],i=e.data;i&&(Array.isArray(i.content)?a.push(...i.content):isRecord(i.content)?a.push(i.content):"string"==typeof i.type&&a.push(i));for(const e of a){if(!isRecord(e))continue;const n=asString(e.type);if("thinking"===n){t.hasAgentThinkingStream=!0;const o=normalizeText(e.thinking)??normalizeText(e.text);if(o){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}continue}if("toolCall"===n){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const n=this.resolveToolCallId(t,e,"start"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",i=resolveToolArgs(e);o(buildToolCallPayload({toolCallId:n,toolName:a,args:i,timestamp:s}));continue}if("toolResult"===n){this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0;const n=this.resolveToolCallId(t,e,"result"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",i=e.result,r=normalizeText(e.text)??normalizeText(e.result)??(isRecord(i)||Array.isArray(i)?JSON.stringify(i,null,2):void 0);r&&this.emitNormalizedToolResult(t,{toolCallId:n,toolName:a,rawText:r,rawResult:i,timestamp:s,pushUpdate:o}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)});continue}}}else if("thinking"===e.stream){t.hasAgentThinkingStream=!0;const o=normalizeText(e.data?.delta)??normalizeText(e.data?.text);if(o){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:o},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}else if("tool"===e.stream){const n=e.data,a="string"==typeof n?.phase?n.phase:void 0;if("start"===a){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const e=this.resolveToolCallId(t,n,"start"),a=asString(n?.name)??asString(n?.toolName)??asString(n?.tool_name)??"tool",i=resolveToolArgs(n);o(buildToolCallPayload({toolCallId:e,toolName:a,args:i,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0,this.handleToolResult(t,e,s).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${e instanceof Error?e.message:e}`)});else{const e=n?.toolCall;if(e){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const a=asString(e?.name)??asString(n?.name)??asString(n?.toolName)??"tool",i={...n,...e},r=this.resolveToolCallId(t,i,"start"),l=resolveToolArgs(i);o(buildToolCallPayload({toolCallId:r,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===e.stream){const s=e.data,o="string"==typeof s?.phase?s.phase:void 0,n=(o,n)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${e.runId} sessionKey=${e.sessionKey} message=${o} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:o,source:"live_gateway",runtimeKind:"openclaw"});"interrupted"===a.kind?this.observeLifecycle(this.interruptPrompt(t,a),"agent_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,a.publicMessage),n,t)};if("end"===o){const e=lifecycleEndFailureMessage(s);if(e)n(e,"agent_failure");else{const e="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.completePrompt(t,e||"end_turn"),"agent_final",t)}}else if("cancelled"===o||"cancel"===o)this.observeLifecycle(this.completePrompt(t,"cancelled"),"agent_cancelled",t);else if("error"===o){const e=s?.error,t=e&&"object"==typeof e?e:void 0;n(("string"==typeof s?.message?s.message:void 0)??("string"==typeof e?e:void 0)??("string"==typeof t?.message?t.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(t?JSON.stringify(t):void 0)??"gateway lifecycle error","agent_failure")}}}async handleToolResult(e,t,s){const o=t.data,n=o?.toolCall,a=("string"==typeof n?.id?n.id:void 0)??("string"==typeof o?.toolCallId?o.toolCallId:void 0),i=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${t.runId}_${t.seq}`,r=("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,c=a??i,d=o?.result;await this.emitNormalizedToolResult(e,{toolCallId:i,normalizeToolCallId:c,toolName:r,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(e,t){const s=t.pushUpdate??(t=>{this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)});await emitNormalizedToolResultForTurn({session:e,envelope:{toolCallId:t.toolCallId,toolName:t.toolName,rawText:t.rawText,rawResult:t.rawResult,timestamp:t.timestamp},normalizeToolCallId:t.normalizeToolCallId,turnId:resolveTurnId(e),config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:e=>(s(e),!0)})}adoptBackgroundRun(e,t){this.logger.debug?.(`[gateway-events] Adopting background run ${t} on session ${e.sessionId} (prev=${e.currentRunId})`);if(!this.adoptBackgroundMutation(e,t).ok)return this.observeLifecycle(this.abortGatewayRun(e.gatewaySessionKey,t),"background_run_conflict_abort",e),!1;e.currentRunId=t,e.promptDone=!1,this.state.promptsByRunId.set(t,e);const s=this.state.runIdToCronJobId.get(t);return s&&(e.cronJobId=s),e.assistantStreamStartedAt=Date.now(),e.lastStreamActivityTs=Date.now(),e.hasAgentAssistantStream=void 0,e.hasAgentThinkingStream=void 0,e.hasAgentToolStartStream=void 0,e.hasAgentToolResultStream=void 0,e.chatAssistantTextSoFar=void 0,e.agentAssistantTextSoFar=void 0,e.chatReplayDedupKeys?.clear(),e.presentedFileArtifactKeys=new Set,e.emittedToolResultKeys=new Set,e.assistantMq&&!e.assistantMq.closed||(e.assistantMq=new MessageQueue),this.schedulePromptTimeout(e),!0}handleCronEvent(e){this.transport.sendNotification("_beeos/cron_event",{payload:e});const t=e??{};"string"==typeof t.runId&&"string"==typeof t.jobId&&this.state.setRunCronJobId(t.runId,t.jobId),this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(t),"added"===t.action&&this.maybeBackfillBeeosChannel(t),"finished"===t.action&&"string"==typeof t.summary&&t.summary&&this.publishCronResult(t)}async reportCronEventToAgentGateway(e){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const t={...e},s="string"==typeof e.sessionKey?e.sessionKey:void 0;s&&null==t.sessionKey&&(t.sessionKey=s);const o=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(o&&(t.sessionId=o),("string"!=typeof t.name||!t.name)&&"string"==typeof e.jobId&&e.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),o=s?.jobs;if(Array.isArray(o))for(const s of o)if(isRecord(s)&&s.id===e.jobId&&"string"==typeof s.name&&s.name){t.name=s.name;break}}catch(e){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${e instanceof Error?e.message:e}`)}const n=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:t})})}catch(e){this.logger.warn?.(`[gateway-events] event report failed: ${e instanceof Error?e.message:e}`)}}async maybeBackfillBeeosChannel(e){const t="string"==typeof e.jobId?e.jobId:void 0;if(!t)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(t))return;let s;try{const e=await this.gatewayRequest("cron.list",{}),o=e?.jobs;if(Array.isArray(o))for(const e of o){if(!e||"object"!=typeof e)continue;const o=e;if(o.id===t){s=o;break}}}catch(e){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${e instanceof Error?e.message:e}`)}if(!s)return;const o="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(o)){const e=decodeBeeosChannelId(o);return void(e&&this.setCronChannelCacheLocal(t,e))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const n=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(n&&"string"==typeof n.kind?n.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const i="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!i)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${t}`);const r=this.state.findSessionByKey(i),l=r?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${i} job=${t}`);const c=encodeBeeosChannelTag(o,l);try{await this.gatewayRequest("cron.update",{id:t,patch:{description:c}}),this.setCronChannelCacheLocal(t,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${t} channel=${l}`)}catch(e){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${e instanceof Error?e.message:e}`)}}setCronChannelCacheLocal(e,t){this.cronChannelCache&&this.cronChannelCache.set(e,t)}async publishCronResult(e){const t=this.getMessageClient?.();if(!t)return;const s=this.getMsgPrincipalId?.();if(!s)return;const o="string"==typeof e.jobId?e.jobId:void 0;if(!o)return;const n=e.summary;let a=this.cronChannelCache?.get(o);if(!a&&this.gatewayRequest)try{const e=await this.gatewayRequest("cron.list",{}),t=e?.jobs;if(Array.isArray(t))for(const e of t){if(!e||"object"!=typeof e)continue;const t=e;if(t.id===o){a=decodeBeeosChannelId(t.description);break}}}catch(e){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${e instanceof Error?e.message:e}`)}if(!a)return;const i=randomUUID();try{await t.messages.send({conversationId:a,clientMessageId:i,idempotencyKey:i,type:"agent_reply",content:{text:n,cron_job_id:o,..."string"==typeof e.sessionKey?{session_key:e.sessionKey}:{}}}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${o}`)}catch(e){const t=e instanceof Error?e.message:String(e);if(isChannelGoneError(e,t))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${a} job=${o} err=${t}`),void this.cronChannelCache?.delete(o);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${t}`)}}logHeartbeatSkip(e){const t=previewSanitizedText(e.cumulative),s=void 0!==e.delta?previewSanitizedText(e.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${e.source} reason=${e.reason} sessionId=${e.sessionId} runId=${e.runId??"n/a"} cumLen=${e.cumulative.length}`+(void 0!==e.delta?` deltaLen=${e.delta.length}`:"")+` cumulative="${t}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${e.source} drop (${e.reason})`,payload:{source:e.source,reason:e.reason,sessionId:e.sessionId,runId:e.runId,cumulativeLen:e.cumulative.length,cumulativePreview:t,...void 0!==e.delta?{deltaLen:e.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(e,t){const s=t.toLowerCase();if(s.includes("not found")||s.includes("no such channel")||s.includes("channel does not exist")||s.includes("channel gone")||s.includes("410")||/\b404\b/.test(s))return!0;if(isRecord(e)){const t=e.status??e.statusCode??e.code;if(404===t||410===t)return!0}return!1}
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{AgentGatewayHttpProjectionTransport,AgentGatewayTemplateOriginReporter,AgentProjectionProduction,OpenClawAgentProjectionSource}from"./runtime/agent-projection-production.js";import{join}from"node:path";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{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";import{MessageClient}from"@beeos-ai/message-sdk";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpToolBridge;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;messageClient;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).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 chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r)):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setMessageClient(e,t){this.messageClient.setConfig(e),this.msgPrincipalId=t,this.logger.info?.(`[acp-server] MessageEnvelope v3 streamer ready (principal=${t})`)}getMessageClient(){return this.messageClient}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}ensureImInboundSession(e,t,i){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i}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,i,n,r,s,a,o,c,l,h,g,d){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpToolBridge=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities={canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),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),this.messageClient=new MessageClient({logger:this.logger});const u=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0,m=this.runtimeAuthority&&this.config.agentGatewayUrl?new AgentGatewayTemplateOriginReporter({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch}):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,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:u,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.runtimeAuthority?()=>this.agentProjection?.markDirty():void 0,getAgentProjection:this.runtimeAuthority?()=>this.agentProjection:void 0,templateOriginReporter:m,mcpToolBridge:this.mcpToolBridge,mcpCredentialReferences:this.mcpCredentialReferences}),this.runtimeAuthority&&this.agentFetchFn&&this.config.agentGatewayUrl&&(this.agentProjection=new AgentProjectionProduction({authority:this.runtimeAuthority,stateFile:join(this.runtimeAuthority.stateDirectory,"runtime-agent-projection.json"),journal:this.runtimeAuthority.journal,identities:this.runtimeAuthority.identities,source:new OpenClawAgentProjectionSource(()=>this.gateway),transport:new AgentGatewayHttpProjectionTransport({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch,getRuntimeLeaseCredential:e=>{const t=this.runtimeAuthority?.authorityLease;if(!t||t.instanceId!==e.instanceId||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new Error("agent projection fenced by current runtime lease");return t.runtimeLeaseCredential},onAccepted:e=>{e.size>0&&this.core.setAgentMapping(e)}}),capabilities:this.runtimeAgentCapabilities,logger:this.logger})),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,i)=>this.core.failPrompt(e,t,i),interruptPrompt:(e,t)=>this.core.interruptPrompt(e,t),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient,getMsgPrincipalId:()=>this.msgPrincipalId??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},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=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1,projectionStarted:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}this.agentProjection&&(e.projectionStarted=!0,this.agentProjection.start());const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(!this.shouldContinueStart(e))return;if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");const c=Date.now();if(await this.connectGateway(e),this.shouldContinueStart(e)){this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-c}ms`);try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){if(a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}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()-s}ms`)}}}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.agentProjection?this.agentProjection.markDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),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())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),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 s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,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();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return void this.agentProjection.onGatewayReconnect();if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],n=3e5,r=()=>{const r=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},n))}const s=i[Math.min(e,i.length-1)],a=Math.round(s/1e3),o=`[acp-server] agent sync retry #${r} in ${a}s`;r>0&&r%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${r} attempts — retrying every ${a}s`):r<=i.length?this.logger.info?.(o):this.logger.debug?.(o),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,boundAgentFetch:this.boundAgentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):r()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),r()}finally{this.agentSyncInFlight=!1}}async connectGateway(e){const t=this.config.gateway,i=new GatewayClient({url:t.url,token:t.token||"",clientId:t.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}});if(e&&!this.shouldContinueStart(e))return i.stop(),void i.removeAllListeners();e&&(e.gateway=i),this.gateway=i,i.on("chat",e=>this.events.handleChatEvent(e)),i.on("agent",e=>this.events.handleAgentEvent(e)),i.on("cron",e=>this.events.handleCronEvent(e)),i.on("sessionMessage",e=>this.core.handleSessionMessage(e)),i.on("response",e=>this.core.handleGatewayResponse(e));const n=i;this.canvasOrch.updateDeps({gateway:n?{get isConnected(){return n.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,i)=>this.gateway.agentSend(e,t,i)}:null}),this.config.openclawCanvasCompat&&(i.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),i.on("connected",()=>{this.agentProjection?.onGatewayReconnect(),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.tryAgentSync()}),i.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),i.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),i.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),i.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),i.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})}),i.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),i.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{if(e){const t=await this.waitForGeneration(i.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Gateway connection timed out")}else await i.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;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),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",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),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"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}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 i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);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 i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}onAgentMappingUpdated(e){this.core.onAgentMappingUpdated(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}getPlatformAgentMappings(){const e=this.core.agentMapping;return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.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 i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),e.projectionStarted&&(e.projectionStarted=!1,o.push(a("agent-projection",()=>this.agentProjection?.stop()))),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.gateway,l=e.bridge,h=e.bridgeSocket,g=e.terminalOutbound,d=e.runtimeConsumer,u=this.client&&this.client!==h?this.client:null,m=this.wss;if(e.gateway=void 0,e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.gateway===c&&(this.gateway=null),this.bridgeInstance===l&&(this.bridgeInstance=null),this.client!==u&&this.client!==h||(this.client=null),this.terminalOutbound===g&&(this.terminalOutbound=null),this.runtimeRpcConsumer===d&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,c&&o.push(a("gateway",async()=>{try{await c.stop()}finally{c.removeAllListeners()}})),u&&o.push(a("incoming-client",()=>{u.removeAllListeners(),u.close()})),l&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(l)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),g&&o.push(a("terminal-outbound",e=>g.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),d&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(d,e))),m&&o.push(a("websocket-server",()=>{m.removeAllListeners(),m.close()})),await Promise.all(o),await a("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await a("terminal-handler",()=>this.terminalHandler.destroy()),await a("terminal-manager",()=>this.terminalMgr.destroy()),n&&clearTimeout(n),r||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
1
+ import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{AgentGatewayHttpProjectionTransport,AgentGatewayTemplateOriginReporter,AgentProjectionProduction,OpenClawAgentProjectionSource}from"./runtime/agent-projection-production.js";import{join}from"node:path";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{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpToolBridge;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;messageClient;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).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 chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r)):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setMessageClient(e,t){if(this.messageClient&&this.messageClient!==e)throw new Error("AcpServer already owns a different MessageClient");this.messageClient=e,this.msgPrincipalId=t,this.logger.info?.(`[acp-server] MessageEnvelope v3 streamer ready (principal=${t})`)}getMessageClient(){if(!this.messageClient)throw new Error("MessageClient has not been composed");return this.messageClient}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}ensureImInboundSession(e,t,i){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i}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,i,n,r,s,a,o,c,l,h,g,d){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpToolBridge=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities={canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),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 u=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0,m=this.runtimeAuthority&&this.config.agentGatewayUrl?new AgentGatewayTemplateOriginReporter({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch}):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,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:u,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.runtimeAuthority?()=>this.agentProjection?.markDirty():void 0,getAgentProjection:this.runtimeAuthority?()=>this.agentProjection:void 0,templateOriginReporter:m,mcpToolBridge:this.mcpToolBridge,mcpCredentialReferences:this.mcpCredentialReferences}),this.runtimeAuthority&&this.agentFetchFn&&this.config.agentGatewayUrl&&(this.agentProjection=new AgentProjectionProduction({authority:this.runtimeAuthority,stateFile:join(this.runtimeAuthority.stateDirectory,"runtime-agent-projection.json"),journal:this.runtimeAuthority.journal,identities:this.runtimeAuthority.identities,source:new OpenClawAgentProjectionSource(()=>this.gateway),transport:new AgentGatewayHttpProjectionTransport({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch,getRuntimeLeaseCredential:e=>{const t=this.runtimeAuthority?.authorityLease;if(!t||t.instanceId!==e.instanceId||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new Error("agent projection fenced by current runtime lease");return t.runtimeLeaseCredential},onAccepted:e=>{e.size>0&&this.core.setAgentMapping(e)}}),capabilities:this.runtimeAgentCapabilities,logger:this.logger})),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,i)=>this.core.failPrompt(e,t,i),interruptPrompt:(e,t)=>this.core.interruptPrompt(e,t),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient??null,getMsgPrincipalId:()=>this.msgPrincipalId??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},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=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1,projectionStarted:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}this.agentProjection&&(e.projectionStarted=!0,this.agentProjection.start());const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(!this.shouldContinueStart(e))return;if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");const c=Date.now();if(await this.connectGateway(e),this.shouldContinueStart(e)){this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-c}ms`);try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){if(a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}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()-s}ms`)}}}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.agentProjection?this.agentProjection.markDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),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())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),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 s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,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();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return void this.agentProjection.onGatewayReconnect();if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],n=3e5,r=()=>{const r=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},n))}const s=i[Math.min(e,i.length-1)],a=Math.round(s/1e3),o=`[acp-server] agent sync retry #${r} in ${a}s`;r>0&&r%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${r} attempts — retrying every ${a}s`):r<=i.length?this.logger.info?.(o):this.logger.debug?.(o),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,boundAgentFetch:this.boundAgentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):r()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),r()}finally{this.agentSyncInFlight=!1}}async connectGateway(e){const t=this.config.gateway,i=new GatewayClient({url:t.url,token:t.token||"",clientId:t.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}});if(e&&!this.shouldContinueStart(e))return i.stop(),void i.removeAllListeners();e&&(e.gateway=i),this.gateway=i,i.on("chat",e=>this.events.handleChatEvent(e)),i.on("agent",e=>this.events.handleAgentEvent(e)),i.on("cron",e=>this.events.handleCronEvent(e)),i.on("sessionMessage",e=>this.core.handleSessionMessage(e)),i.on("response",e=>this.core.handleGatewayResponse(e));const n=i;this.canvasOrch.updateDeps({gateway:n?{get isConnected(){return n.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,i)=>this.gateway.agentSend(e,t,i)}:null}),this.config.openclawCanvasCompat&&(i.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),i.on("connected",()=>{this.agentProjection?.onGatewayReconnect(),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.tryAgentSync()}),i.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),i.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),i.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),i.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),i.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})}),i.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),i.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{if(e){const t=await this.waitForGeneration(i.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Gateway connection timed out")}else await i.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;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),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",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),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"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}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 i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);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 i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}onAgentMappingUpdated(e){this.core.onAgentMappingUpdated(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}resolveLocalAgentIdForPlatformAgent(e){return this.core.resolveLocalAgentId(e)}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.core.agentMapping;return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.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 i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),e.projectionStarted&&(e.projectionStarted=!1,o.push(a("agent-projection",()=>this.agentProjection?.stop()))),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.gateway,l=e.bridge,h=e.bridgeSocket,g=e.terminalOutbound,d=e.runtimeConsumer,u=this.client&&this.client!==h?this.client:null,m=this.wss;if(e.gateway=void 0,e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.gateway===c&&(this.gateway=null),this.bridgeInstance===l&&(this.bridgeInstance=null),this.client!==u&&this.client!==h||(this.client=null),this.terminalOutbound===g&&(this.terminalOutbound=null),this.runtimeRpcConsumer===d&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,c&&o.push(a("gateway",async()=>{try{await c.stop()}finally{c.removeAllListeners()}})),u&&o.push(a("incoming-client",()=>{u.removeAllListeners(),u.close()})),l&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(l)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),g&&o.push(a("terminal-outbound",e=>g.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),d&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(d,e))),m&&o.push(a("websocket-server",()=>{m.removeAllListeners(),m.close()})),await Promise.all(o),await a("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await a("terminal-handler",()=>this.terminalHandler.destroy()),await a("terminal-manager",()=>this.terminalMgr.destroy()),n&&clearTimeout(n),r||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
package/dist/config.js CHANGED
@@ -1 +1 @@
1
- import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";export const CRON_DURATION_FLOOR_MS=3e4;export const CRON_DURATION_CAP_DEFAULT_MS=72e5;function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const CURRENT_CONFIG_PROTOCOL=1;function shouldTrustOperationalConfig(e){const r=e._generator;if(!r)return!1;return("number"==typeof r.configProtocol?r.configProtocol:0)>=1}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},a=e.openclawConfig??{},o=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=shouldTrustOperationalConfig(r),l=pick([{source:"pluginConfig",value:trimStr(r.agentGatewayUrl)},{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.AGENT_GATEWAY_URL)}],{source:"default",value:""}),n=r.gateway??{},s=(a.gateway??{}).auth??{},t=pick([...u?[{source:"pluginConfig",value:trimStr(n.url)}]:[],{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(n.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),c=pick([...u?[{source:"pluginConfig",value:readInt(n.protocol)}]:[]],{source:"default",value:3}),v=pick([...u?[{source:"pluginConfig",value:trimStr(n.clientId)}]:[]],{source:"default",value:`beeos-claw-${process.pid}`}),p=pick([...u?[{source:"pluginConfig",value:trimStr(n.agentId)}]:[]],{source:"default",value:""}),d=r.bridge??{},g=pick([{source:"pluginConfig",value:trimStr(r.keyFile)},{source:"pluginConfig",value:trimStr(d.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),E=pick([...u?[{source:"pluginConfig",value:trimStr(d.service)}]:[],{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),m=d.shell??{},_=pick([...u?[{source:"pluginConfig",value:readBool(m.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),S=r.retry??{},A=pick([...u?[{source:"pluginConfig",value:readInt(S.baseMs)}]:[]],{source:"default",value:1e3}),C=pick([...u?[{source:"pluginConfig",value:readInt(S.maxMs)}]:[]],{source:"default",value:6e5}),I=pick([...u?[{source:"pluginConfig",value:readInt(S.maxAttempts)}]:[]],{source:"default",value:0}),M=r.log??{},O=pick([...u?[{source:"pluginConfig",value:readBool(M.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),T=pick([...u?[{source:"pluginConfig",value:readBool(M.verbose)}]:[]],{source:"default",value:!1}),R=pick([{source:"pluginConfig",value:trimStr(M.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),B=pick([...u?[{source:"pluginConfig",value:readInt(d.promptTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:6e5}),k=pick([...u?[{source:"pluginConfig",value:readInt(r.terminalWsPort)}]:[],{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),L=(u?trimStr(r.terminalWsAllowedOrigins):void 0)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),y=L?L.split(",").map(e=>e.trim()).filter(Boolean):void 0,N=pick([{source:L?"pluginConfig":"default",value:y?.length?y:void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),w=pick([{source:"env",value:readBool(process.env.BEEOS_TERMINAL_SERVICE_ENABLED)},...u?[{source:"pluginConfig",value:readBool(r.terminalServiceEnabled)}]:[]],{source:"default",value:!1}),b=pick([...u?[{source:"pluginConfig",value:readBool(r.legacyTerminalWsEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LEGACY_TERMINAL_WS_ENABLED)}],{source:"default",value:!1}),U=pick([...u?[{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),h=pick([...u?[{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),G=pick([...u?[{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),D=pick([{source:"pluginConfig",value:readInt(r.cronMaxDurationDefaultMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_MAX_DURATION_DEFAULT_MS)}],{source:"default",value:6e5}),W=pick([{source:"pluginConfig",value:readInt(r.cronMaxDurationCapMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_MAX_DURATION_CAP_MS)}],{source:"default",value:72e5}),P=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),x=pick([...u?[{source:"pluginConfig",value:readBool(r.canvasEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),F=pick([...u?[{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)}]:[],{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),H=r.messageBus??{},Y=pick([{source:"pluginConfig",value:trimStr(H.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),j=pick([{source:"pluginConfig",value:trimStr(H.url)},{source:"env",value:trimStr(process.env.CENTRIFUGO_URL)}],{source:"default",value:""}),$=pick([{source:"pluginConfig",value:trimStr(H.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),V=pick([{source:"pluginConfig",value:readBool(H.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),K=pick([{source:"pluginConfig",value:readBool(H.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!Y.value&&!l.value)}),X={agentGatewayUrl:l.value.replace(/\/+$/,""),keyFile:g.value,bridge:{url:f.value,service:E.value,mode:trimStr(d.mode),shell:{enabled:_.value},promptTimeoutMs:B.value},gateway:{url:t.value,token:i.value,protocol:c.value,clientId:v.value,agentId:p.value},retry:{baseMs:Math.max(0,A.value),maxMs:Math.max(0,C.value),maxAttempts:Math.max(0,I.value)},log:{enabled:O.value,verbose:T.value,dir:R.value},promptTimeoutMs:B.value,terminalWsPort:k.value,terminalWsAllowedOrigins:N.value,terminalServiceEnabled:w.value,legacyTerminalWsEnabled:b.value,streamIdleTimeoutMs:Math.max(0,U.value),streamStaleTimeoutMs:Math.max(0,h.value),cronSignalWindowMs:Math.max(0,G.value),cronMaxDurationDefaultMs:Math.max(3e4,D.value),cronMaxDurationCapMs:Math.max(6e4,W.value),canvasRelayUrl:P.value.replace(/\/+$/,""),canvasEnabled:x.value,openclawCanvasCompat:F.value,messageBus:{enabled:K.value,serviceUrl:Y.value,url:j.value,principalId:$.value,a2aEnabled:V.value}},q={agentGatewayUrl:l.source,keyFile:g.source,bridge:{url:f.source,service:E.source,shell:{enabled:_.source}},gateway:{url:t.source,token:i.source,protocol:c.source,clientId:v.source,agentId:p.source},retry:{baseMs:A.source,maxMs:C.source,maxAttempts:I.source},log:{enabled:O.source,verbose:T.source,dir:R.source},promptTimeoutMs:B.source,terminalWsPort:k.source,terminalWsAllowedOrigins:N.source,terminalServiceEnabled:w.source,legacyTerminalWsEnabled:b.source,streamIdleTimeoutMs:U.source,streamStaleTimeoutMs:h.source,cronSignalWindowMs:G.source,cronMaxDurationDefaultMs:D.source,cronMaxDurationCapMs:W.source,canvasRelayUrl:P.source,canvasEnabled:x.source,openclawCanvasCompat:F.source,messageBus:{enabled:K.source,serviceUrl:Y.source,url:j.source,principalId:$.source,a2aEnabled:V.source}},J=[];return X.agentGatewayUrl||J.push({code:"CONFIG_AGENT_GATEWAY_URL_MISSING",severity:"error",message:"agentGatewayUrl is required for file uploads and Agent Gateway callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.agentGatewayUrl in ${o}`,"Or set the AGENT_GATEWAY_URL environment variable"]}),X.gateway.token||J.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${o}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${o}`]}),{config:X,sources:q,validation:J,openclawConfigPath:o}}export function resolveConfig(e){const{config:r,validation:a}=resolveConfigWithMeta({pluginConfig:e}),o=a.find(e=>"error"===e.severity);if(o)throw new Error(`[beeos-claw] ${o.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
1
+ import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";export const CRON_DURATION_FLOOR_MS=3e4;export const CRON_DURATION_CAP_DEFAULT_MS=72e5;function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const CURRENT_CONFIG_PROTOCOL=1;function shouldTrustOperationalConfig(e){const r=e._generator;if(!r)return!1;return("number"==typeof r.configProtocol?r.configProtocol:0)>=1}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},a=e.openclawConfig??{},o=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=shouldTrustOperationalConfig(r),l=pick([{source:"pluginConfig",value:trimStr(r.agentGatewayUrl)},{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.AGENT_GATEWAY_URL)}],{source:"default",value:""}),n=r.gateway??{},s=(a.gateway??{}).auth??{},t=pick([...u?[{source:"pluginConfig",value:trimStr(n.url)}]:[],{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),i=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(n.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),c=pick([...u?[{source:"pluginConfig",value:readInt(n.protocol)}]:[]],{source:"default",value:3}),v=pick([...u?[{source:"pluginConfig",value:trimStr(n.clientId)}]:[]],{source:"default",value:`beeos-claw-${process.pid}`}),p=pick([...u?[{source:"pluginConfig",value:trimStr(n.agentId)}]:[]],{source:"default",value:""}),d=r.bridge??{},g=pick([{source:"pluginConfig",value:trimStr(r.keyFile)},{source:"pluginConfig",value:trimStr(d.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),f=pick([{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),E=pick([...u?[{source:"pluginConfig",value:trimStr(d.service)}]:[],{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),m=d.shell??{},_=pick([...u?[{source:"pluginConfig",value:readBool(m.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),S=r.retry??{},A=pick([...u?[{source:"pluginConfig",value:readInt(S.baseMs)}]:[]],{source:"default",value:1e3}),C=pick([...u?[{source:"pluginConfig",value:readInt(S.maxMs)}]:[]],{source:"default",value:6e5}),I=pick([...u?[{source:"pluginConfig",value:readInt(S.maxAttempts)}]:[]],{source:"default",value:0}),M=r.log??{},O=pick([...u?[{source:"pluginConfig",value:readBool(M.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),T=pick([...u?[{source:"pluginConfig",value:readBool(M.verbose)}]:[]],{source:"default",value:!1}),R=pick([{source:"pluginConfig",value:trimStr(M.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),B=pick([...u?[{source:"pluginConfig",value:readInt(d.promptTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_PROMPT_TIMEOUT_MS)}],{source:"default",value:6e5}),k=pick([...u?[{source:"pluginConfig",value:readInt(r.terminalWsPort)}]:[],{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),y=(u?trimStr(r.terminalWsAllowedOrigins):void 0)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),L=y?y.split(",").map(e=>e.trim()).filter(Boolean):void 0,N=pick([{source:y?"pluginConfig":"default",value:L?.length?L:void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),w=pick([{source:"env",value:readBool(process.env.BEEOS_TERMINAL_SERVICE_ENABLED)},...u?[{source:"pluginConfig",value:readBool(r.terminalServiceEnabled)}]:[]],{source:"default",value:!1}),b=pick([...u?[{source:"pluginConfig",value:readBool(r.legacyTerminalWsEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LEGACY_TERMINAL_WS_ENABLED)}],{source:"default",value:!1}),h=pick([...u?[{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),U=pick([...u?[{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),D=pick([...u?[{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),G=pick([{source:"pluginConfig",value:readInt(r.cronMaxDurationDefaultMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_MAX_DURATION_DEFAULT_MS)}],{source:"default",value:6e5}),W=pick([{source:"pluginConfig",value:readInt(r.cronMaxDurationCapMs)},{source:"env",value:readInt(process.env.BEEOS_CRON_MAX_DURATION_CAP_MS)}],{source:"default",value:72e5}),P=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),x=pick([...u?[{source:"pluginConfig",value:readBool(r.canvasEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),F=pick([...u?[{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)}]:[],{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),H=r.messageBus??{},Y=pick([{source:"pluginConfig",value:trimStr(H.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),j=pick([{source:"pluginConfig",value:trimStr(H.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),$=pick([{source:"pluginConfig",value:readBool(H.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),V=pick([{source:"pluginConfig",value:readBool(H.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!Y.value&&!l.value)}),K={agentGatewayUrl:l.value.replace(/\/+$/,""),keyFile:g.value,bridge:{url:f.value,service:E.value,mode:trimStr(d.mode),shell:{enabled:_.value},promptTimeoutMs:B.value},gateway:{url:t.value,token:i.value,protocol:c.value,clientId:v.value,agentId:p.value},retry:{baseMs:Math.max(0,A.value),maxMs:Math.max(0,C.value),maxAttempts:Math.max(0,I.value)},log:{enabled:O.value,verbose:T.value,dir:R.value},promptTimeoutMs:B.value,terminalWsPort:k.value,terminalWsAllowedOrigins:N.value,terminalServiceEnabled:w.value,legacyTerminalWsEnabled:b.value,streamIdleTimeoutMs:Math.max(0,h.value),streamStaleTimeoutMs:Math.max(0,U.value),cronSignalWindowMs:Math.max(0,D.value),cronMaxDurationDefaultMs:Math.max(3e4,G.value),cronMaxDurationCapMs:Math.max(6e4,W.value),canvasRelayUrl:P.value.replace(/\/+$/,""),canvasEnabled:x.value,openclawCanvasCompat:F.value,messageBus:{enabled:V.value,serviceUrl:Y.value,principalId:j.value,a2aEnabled:$.value}},X={agentGatewayUrl:l.source,keyFile:g.source,bridge:{url:f.source,service:E.source,shell:{enabled:_.source}},gateway:{url:t.source,token:i.source,protocol:c.source,clientId:v.source,agentId:p.source},retry:{baseMs:A.source,maxMs:C.source,maxAttempts:I.source},log:{enabled:O.source,verbose:T.source,dir:R.source},promptTimeoutMs:B.source,terminalWsPort:k.source,terminalWsAllowedOrigins:N.source,terminalServiceEnabled:w.source,legacyTerminalWsEnabled:b.source,streamIdleTimeoutMs:h.source,streamStaleTimeoutMs:U.source,cronSignalWindowMs:D.source,cronMaxDurationDefaultMs:G.source,cronMaxDurationCapMs:W.source,canvasRelayUrl:P.source,canvasEnabled:x.source,openclawCanvasCompat:F.source,messageBus:{enabled:V.source,serviceUrl:Y.source,principalId:j.source,a2aEnabled:$.source}},q=[];return K.agentGatewayUrl||q.push({code:"CONFIG_AGENT_GATEWAY_URL_MISSING",severity:"error",message:"agentGatewayUrl is required for file uploads and Agent Gateway callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.agentGatewayUrl in ${o}`,"Or set the AGENT_GATEWAY_URL environment variable"]}),K.gateway.token||q.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${o}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${o}`]}),{config:K,sources:X,validation:q,openclawConfigPath:o}}export function resolveConfig(e){const{config:r,validation:a}=resolveConfigWithMeta({pluginConfig:e}),o=a.find(e=>"error"===e.severity);if(o)throw new Error(`[beeos-claw] ${o.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
@@ -1 +1 @@
1
- import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",SEEN_MESSAGE_IDS_MAX=256,resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>{if("string"==typeof e.conversationId&&e.conversationId)return e.conversationId;const n=e.content??{};return"string"==typeof n.conversation_id&&n.conversation_id?n.conversation_id:"string"==typeof n.channel_id&&n.channel_id?n.channel_id:""};export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:s,client:t,localServer:o,promptRunStore:a,sessionMutationCoordinator:i,logger:r,writeObs:c,principalIdOverride:d,promptTimeoutMs:p,dispatch:l=dispatchImInboundPrompt,activeRunAuthority:m}=e,g=new Map;return{onChatMessage:async e=>{const h=extractChatPrompt(e,{source:"chat_message",localAgentId:o.getLocalAgentId()});if(!h)return;if(!h.channelId||!h.messageId)return void r.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${h.sessionKey}) — drop`);const I=promptRunKey(h.channelId,h.messageId);if(g.has(I))return void r.info?.(`[beeos-claw] duplicate chat_message replay dropped (message_id=${h.messageId}, channelId=${h.channelId})`);if(g.size>=256){const e=g.keys().next().value;void 0!==e&&g.delete(e)}g.set(I,!0);const _=d||o.getDefaultPlatformAgent()?.instanceId||"";if(!_)return void r.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${h.sessionKey})`);const u=(e.content??{}).metadata??void 0,y="string"==typeof u?.delivery_principal?u.delivery_principal:"";if(y&&y!==_)return void r.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${y}, selfPrincipalId=${_}, message_id=${h.messageId})`);o.ensureImInboundSession(h.sessionKey,h.channelId,h.messageId);const f=resolveOrigin(u?.protocol);r.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${h.sessionKey}, channelId=${h.channelId}, origin=${f})`);const v=e=>{c({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await l({runtime:n,pluginConfig:s,prompt:{channelId:h.channelId,inboundMessageId:h.messageId,selfPrincipalId:_,dispatchInput:{message:h.message,attachments:h.files},sessionKey:h.sessionKey},client:t,logger:r,promptRunStore:a,sessionMutationCoordinator:i,abortTransport:o,origin:f,promptTimeoutMs:p,onLifecycleEvent:v,activeRunAuthority:m})}catch(e){r.error?.(`[beeos-claw] chat_message dispatch failed channelId=${h.channelId} in_reply_to=${h.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async e=>{const n=e.content??{},s="string"==typeof n.target_message_id?n.target_message_id:"",t="string"==typeof n.reason?n.reason:"unknown";if(!s)return void r.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${t})`);const o=conversationIdFromMessage(e);if(!o)return void r.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${s}, reason=${t})`);const i=`chat_cancel:${t}`,c=await a.abort(promptRunKey(o,s),i);c.ok?r.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${s}, requestId=${c.requestId}, channelId=${c.channelId}, reason=${t})`):r.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${s}, reason=${t}, why=${c.reason})`)}}};
1
+ import{extractChatPrompt}from"@beeos-ai/message-sdk/chat-envelope";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",SEEN_MESSAGE_IDS_MAX=256,resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>{if("string"==typeof e.conversationId&&e.conversationId)return e.conversationId;const n=e.content??{};return"string"==typeof n.conversation_id&&n.conversation_id?n.conversation_id:"string"==typeof n.channel_id&&n.channel_id?n.channel_id:""};export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:t,client:s,localServer:a,promptRunStore:o,sessionMutationCoordinator:i,logger:r,writeObs:c,principalIdOverride:d,promptTimeoutMs:l,dispatch:p=dispatchImInboundPrompt,activeRunAuthority:g}=e,m=new Map;return{onChatMessage:async e=>{const _=(e.content??{}).metadata??void 0,h="string"==typeof _?.target_agent_id?_.target_agent_id:"",I=h?a.resolveLocalAgentId?.(h):void 0;h&&!I&&r.warn?.(`[beeos-claw] chat_message target_agent_id=${h} not resolvable to a local agent — falling back to ${a.getLocalAgentId()}`);const u=I||a.getLocalAgentId(),y=extractChatPrompt(e,{source:"chat_message",localAgentId:u});if(!y)return;if(!y.channelId||!y.messageId)return void r.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${y.sessionKey}) — drop`);const f=promptRunKey(y.channelId,y.messageId);if(m.has(f))return void r.info?.(`[beeos-claw] duplicate chat_message replay dropped (message_id=${y.messageId}, channelId=${y.channelId})`);if(m.size>=256){const e=m.keys().next().value;void 0!==e&&m.delete(e)}m.set(f,!0);const v=d||a.getDefaultPlatformAgent()?.instanceId||"";if(!v)return void r.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${y.sessionKey})`);const $="string"==typeof _?.delivery_principal?_.delivery_principal:"";if($&&$!==v)return void r.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${$}, selfPrincipalId=${v}, message_id=${y.messageId})`);a.ensureImInboundSession(y.sessionKey,y.channelId,y.messageId);const b=resolveOrigin(_?.protocol);r.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${y.sessionKey}, channelId=${y.channelId}, origin=${b})`);const w=e=>{c({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await p({runtime:n,pluginConfig:t,prompt:{channelId:y.channelId,inboundMessageId:y.messageId,selfPrincipalId:v,dispatchInput:{message:y.message,attachments:y.files},sessionKey:y.sessionKey},client:s,logger:r,promptRunStore:o,sessionMutationCoordinator:i,abortTransport:a,origin:b,promptTimeoutMs:l,onLifecycleEvent:w,activeRunAuthority:g})}catch(e){r.error?.(`[beeos-claw] chat_message dispatch failed channelId=${y.channelId} in_reply_to=${y.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async e=>{const n=e.content??{},t="string"==typeof n.target_message_id?n.target_message_id:"",s="string"==typeof n.reason?n.reason:"unknown";if(!t)return void r.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${s})`);const a=conversationIdFromMessage(e);if(!a)return void r.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${t}, reason=${s})`);const i=`chat_cancel:${s}`,c=await o.abort(promptRunKey(a,t),i);c.ok?r.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${t}, requestId=${c.requestId}, channelId=${c.channelId}, reason=${s})`):r.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${t}, reason=${s}, why=${c.reason})`)}}};
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{SESSION_RECOVERY_REQUIRED_CODE,SESSION_RECOVERY_REQUIRED_MESSAGE}from"./runtime/session-mutation-coordinator.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";import{maybeRenameImConversationTitle}from"./runtime/conversation-title.js";const normalizePromptTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},ABORT_TERMINAL_GRACE_MS=1e4,formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},deterministicMessageId=(e,n,a,r)=>`${e}_${createHash("sha256").update(`${n}:${a}:${r}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:a,client:r,logger:t,promptRunStore:i,sessionMutationCoordinator:s,abortTransport:o,activeRunAuthority:c}=e;if(!hasRuntimeReplyApi(n))return t.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${a.channelId} in_reply_to=${a.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:a.channelId,inboundMessageId:a.inboundMessageId}),!1;const d=`im_req_${randomUUID().replace(/-/g,"")}`,l=randomUUID(),u=Date.now(),m=e.origin??"im",p={protocol:"im",requestId:d,sessionId:a.sessionKey??"agent:main",sourceMessageId:a.inboundMessageId,chatId:a.channelId,origin:m},I=normalizePromptTimeoutMs(e.promptTimeoutMs),g=promptRunKey(a.channelId,a.inboundMessageId),h=a.sessionKey??"agent:main",y=new AbortController;let f,_,E;const R=new Promise((e,n)=>{E=n});let b;const w=()=>{void 0!==b&&(clearTimeout(b),b=void 0)},$=async e=>{if(_??=e,y.abort(e),!f)return{ok:!0};const n=await s.requestCancel(f,o);return n.ok?n.terminalObservation.then(e=>{e.ok||E(new Error(SESSION_RECOVERY_REQUIRED_CODE))}):E(new Error(SESSION_RECOVERY_REQUIRED_CODE)),{ok:n.ok}};if(!i.register(g,{requestId:d,channelId:a.channelId,requestCancel:$,startedAt:u,sessionKey:h}))return t.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${a.inboundMessageId} requestId=${d} channel=${a.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const M=r.messages.startStream({conversationId:a.channelId,id:deterministicMessageId("asst",a.channelId,a.inboundMessageId,a.selfPrincipalId),replyTo:e.inReplyTo??a.inboundMessageId,type:"agent_reply"},{onError:(e,n)=>{const r="terminal"===n?t.warn:t.debug;r?.(`[im] runtime stream ${n} PATCH error requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId}: ${e.message}`)}},{sender:a.selfPrincipalId});let S,v=!1;try{v=!0===(await M.opened()).idempotent}catch(e){t.debug?.(`[im] runtime reply stream open probe failed requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId}: ${formatErrMessage(e)}; continuing so terminal fallback can land an error row`)}if(v)return t.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:a.channelId,inboundMessageId:a.inboundMessageId}),i.unregister(g),!1;const q=new SerializedCallbackQueue,C=createImReplySink({stream:M,openReasoningStream:()=>(S=r.messages.startStream({conversationId:a.channelId,id:deterministicMessageId("rsn",a.channelId,a.inboundMessageId,a.selfPrincipalId),replyTo:e.inReplyTo??a.inboundMessageId,type:"agent_reasoning"},{onError:(e,n)=>{const r="terminal"===n?t.warn:t.debug;r?.(`[im] runtime reasoning stream ${n} PATCH error requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId}: ${e.message}`)}},{sender:a.selfPrincipalId}),S),abortSignal:y.signal,callbackQueue:q,logger:t,requestId:d,runId:l,channelId:a.channelId,inboundMessageId:a.inboundMessageId,origin:m,onAgentRunStart:e=>{s.bindRun(f,e)}}),k=interceptAgentRuntimeFailures(C,"im_runtime",e=>t.warn?.(`[im] runtime failure reply suppressed requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} error_code=${e.errorCode}`)),O=k.sink,T=async(e,n)=>{if(!S)return!1;try{return"cancel"===e?await S.cancel({stopReason:"user_stop"}):"fail"===e?await S.fail(n?{stopReason:n}:void 0):await S.finalize({stopReason:"end_turn"}),!1}catch(e){return t.warn?.(`[im] runtime reasoning stream terminal write failed requestId=${d} channel=${a.channelId}: ${formatErrMessage(e)}`),!0}};let A,D,U=!1;const K=async e=>{O.dispatcher.markComplete(e);try{await O.dispatcher.waitForIdle()}catch{}},N=()=>{w();const n=normalizePromptTimeoutMs(e.abortTerminalGraceMs)??1e4;b=setTimeout(()=>{b=void 0,(async()=>{if(M.isTerminated)return;const e=y.signal.reason??_,n=void 0!==D,r=!n&&"string"==typeof e&&e.startsWith("chat_cancel");if(await K("failure"),!M.isTerminated)try{r?(await T("cancel"),await M.cancel({stopReason:"user_stop"})):n?(await T("fail","timeout"),await M.fail({body:D?.message??"IM runtime dispatch timed out",stopReason:"timeout"})):(await T("fail"),await M.fail({body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"})),t.warn?.(`[im] abort settlement watchdog terminalized reply requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} user_cancel=${r} timeout=${n}`)}catch(e){t.warn?.(`[im] abort settlement watchdog terminal write failed requestId=${d} channel=${a.channelId}: ${formatErrMessage(e)}`)}})()},n)};y.signal.aborted?N():y.signal.addEventListener("abort",N,{once:!0});const P=()=>{if(!y.signal.aborted)return;const e=y.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))};let F,L,Q;const z=async e=>{if(!f)throw new Error("session_mutation_not_acquired");const n=await s.converge(f,e);return n.ok?(F=n.permit,!0):(t.error?.(`[im] session mutation recovery required requestId=${d} sessionKey=${h} reason=${n.recovery.reason}`),L={code:n.recovery.code,reason:n.recovery.reason,sessionKey:n.recovery.sessionKey,operationId:n.recovery.operationId,surface:"message_service",...n.recovery.terminalKind?{terminalKind:n.recovery.terminalKind}:{}},!1)};try{P();const o=s.tryStartInteractive({sessionKey:h,label:"im_chat_message"},"message_service");if(!o.ok){try{await M.fail({body:SESSION_BUSY_PUBLIC_MESSAGE,stopReason:"error"})}catch(e){t.warn?.(`[im] session busy terminal write failed channel=${a.channelId} in_reply_to=${a.inboundMessageId}: ${formatErrMessage(e)}`)}return t.warn?.(`[im] session write gate busy sessionKey=${h} channel=${a.channelId} in_reply_to=${a.inboundMessageId} active_operation=${o.activeOperationId??"unknown"}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,error:"session_busy",cause:"runtime_error"}),!1}if(f=o.token,s.bindRun(f,l),c){const e=s.snapshot(h);Q={runtimeConversationUid:a.channelId,runtimeSessionKey:a.channelId,historyGeneration:c.historyGeneration(a.channelId),targetMessageId:a.inboundMessageId,runtimeRunId:l,activeRunRegistrationId:`im:${e?.operationId??l}`},c.activeRuns.register(Q,async()=>{const e=`chat_cancel:activeRun:${Q.activeRunRegistrationId}`,n=await i.abort(g,e);return n.ok||"already_aborted"===n.reason?"accepted":"terminal_committing"===n.reason||"not_found"===n.reason?"already_terminal":"recovery_required"})}P(),t.info?.(`[im] runtime dispatching requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} origin=${m}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId});const y=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:a.dispatchInput,replyRoute:p,replySinks:{im:O},onDispatchInvoked:()=>{U=!0}});void 0!==I&&(A=setTimeout(()=>{D=new Error(`IM runtime dispatch timed out after ${I}ms`),t.warn?.(`[im] runtime dispatch timeout requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} timeout_ms=${I}`),$(D)},I));const _=Promise.race([y,R]),E=await _.finally(()=>{A&&(clearTimeout(A),A=void 0)}),b=k.detectedFailure();if(b)throw new Error(b.diagnosticMessage);if(P(),await K("success"),P(),!i.claimTerminal(g,"completion")){if(P(),"cancelling"===i.phase(g))throw new Error("cancellation in progress");throw new Error("terminal outcome already claimed")}if(!await z("completed"))return await T("fail"),await M.fail({body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:"runtime_error",...L?{recovery:L}:{}}),!1;const w=await T("complete");let S=!1;try{await M.finalize({stopReason:"end_turn"})}catch(e){t.warn?.(`[im] runtime reply stream finalize failed requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId}: ${formatErrMessage(e)}; retrying`);try{await M.finalize({stopReason:"end_turn"})}catch(e){S=!0,t.warn?.(`[im] runtime reply stream finalize retry failed requestId=${d} channel=${a.channelId}: ${formatErrMessage(e)}; falling back to fail()`);try{await M.fail({stopReason:"error"}),S=!1}catch(e){t.warn?.(`[im] runtime reply stream fail fallback failed requestId=${d} channel=${a.channelId}: ${formatErrMessage(e)}`)}}}c&&Q&&c.activeRuns.markTerminal(Q,"completed"),t.info?.(`[im] runtime dispatch completed requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} counts=${JSON.stringify(E.counts)} elapsed_ms=${Date.now()-u}`),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,counts:E.counts,...S?{terminalWriteFailed:!0}:{},...w?{reasoningTerminalWriteFailed:!0}:{}});const v="string"==typeof a.dispatchInput.message?a.dispatchInput.message:"",q=O.dispatcher.getAccumulatedBody?.()??"";return maybeRenameImConversationTitle({client:r,conversationId:a.channelId,question:v,answer:q,logger:t}),!0}catch(n){await K("failure");const r=y.signal.aborted,s=r?y.signal.reason:void 0,o="cancelling"===i.phase(g),l=void 0!==D,u=s??_,m=!l&&"string"==typeof u&&u.startsWith("chat_cancel");let p="runtime_error";l?p="timeout":(r||o)&&(p="abort");const I=o||m||l||r?"cancellation":"completion";if(!i.claimTerminal(g,I)){if(t.warn?.(`[im] terminal claim raced requestId=${d} channel=${a.channelId}; best-effort envelope terminal if still streaming`),!M.isTerminated)try{m?(await T("cancel"),await M.cancel({stopReason:"user_stop"})):l?(await T("fail","timeout"),await M.fail({body:D?.message??formatErrMessage(n),stopReason:"timeout"})):(await T("fail"),await M.fail({body:formatErrMessage(n),stopReason:"error"}))}catch(e){t.warn?.(`[im] raced terminal write failed requestId=${d}: ${formatErrMessage(e)}`)}throw n}const h=m?"cancelled":l?"timeout":"error";if(c&&Q&&c.activeRuns.markTerminal(Q,"cancelled"===h?"cancelled":"failed"),f&&!await z(h))throw m?(await T("cancel"),await M.cancel({stopReason:"user_stop"}).catch(()=>{})):(await T("fail"),await M.fail({body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{})),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:p,...L?{recovery:L}:{}}),n;const E=classifyAgentRuntimeFailure({raw:n,source:"im_runtime",runtimeKind:"openclaw",requestAborted:m}),R=m?E:k.detectedFailure()??E;m?await T("cancel"):l?await T("fail","timeout"):await T("fail");try{m?await M.cancel({stopReason:"user_stop"}):await M.fail({body:R.publicMessage,stopReason:messageStopReasonForRuntimeFailure(R)})}catch(e){t.warn?.(`[im] runtime stream terminal write failed requestId=${d} channel=${a.channelId}: ${formatErrMessage(e)}`)}throw t.warn?.(`[im] runtime dispatch failed requestId=${d} channel=${a.channelId} in_reply_to=${a.inboundMessageId} cause=${p} dispatchInvoked=${U} error_code=${R.errorCode}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:a.channelId,inboundMessageId:a.inboundMessageId,error:R.errorCode,cause:p}),n}finally{w(),i.unregister(g),U?F&&s.release(F):s.releaseBeforeRuntime(f)}};
1
+ import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{SESSION_RECOVERY_REQUIRED_CODE,SESSION_RECOVERY_REQUIRED_MESSAGE}from"./runtime/session-mutation-coordinator.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";import{maybeRenameImConversationTitle}from"./runtime/conversation-title.js";const normalizePromptTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},ABORT_TERMINAL_GRACE_MS=1e4,formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},failMessageStream=async(e,n)=>{void 0!==n.body&&e.setBody(n.body),await e.fail(void 0===n.stopReason?void 0:{stopReason:n.stopReason})};export const deterministicReplyMessageId=(e,n,t,a)=>`${e}_${createHash("sha256").update(`${n}:${t}:${a}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:t,client:a,logger:r,promptRunStore:i,sessionMutationCoordinator:s,abortTransport:o,activeRunAuthority:c}=e;if(!hasRuntimeReplyApi(n))return r.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),!1;const d=`im_req_${randomUUID().replace(/-/g,"")}`,l=randomUUID(),u=Date.now(),m=e.origin??"im",p={protocol:"im",requestId:d,sessionId:t.sessionKey??"agent:main",sourceMessageId:t.inboundMessageId,chatId:t.channelId,origin:m},I=normalizePromptTimeoutMs(e.promptTimeoutMs),g=promptRunKey(t.channelId,t.inboundMessageId),h=t.sessionKey??"agent:main",y=new AbortController;let _,f,R;const E=new Promise((e,n)=>{R=n});let w;const b=()=>{void 0!==w&&(clearTimeout(w),w=void 0)},M=async e=>{if(f??=e,y.abort(e),!_)return{ok:!0};const n=await s.requestCancel(_,o);return n.ok?n.terminalObservation.then(e=>{e.ok||R(new Error(SESSION_RECOVERY_REQUIRED_CODE))}):R(new Error(SESSION_RECOVERY_REQUIRED_CODE)),{ok:n.ok}};if(!i.register(g,{requestId:d,channelId:t.channelId,requestCancel:M,startedAt:u,sessionKey:h}))return r.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${t.inboundMessageId} requestId=${d} channel=${t.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const $=deterministicReplyMessageId("asst",t.channelId,t.inboundMessageId,t.selfPrincipalId),S=a.messages.startStream({conversationId:t.channelId,clientMessageId:$,idempotencyKey:$,replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reply",content:{}});let v,q=!1;try{q="duplicate"===(await S.opened()).outcome}catch(n){let s="unconfirmed";try{s=(await a.messages.getEnvelope(t.channelId,$)).state}catch{}return r.warn?.(`[im] runtime reply stream open outcome unknown requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} reply_id=${$} reconciled_state=${s}: ${formatErrMessage(n)}; runtime_not_invoked`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_open_outcome_unknown",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(g),!1}if(q)return r.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(g),!1;const k=new SerializedCallbackQueue,C=createImReplySink({stream:S,openReasoningStream:()=>{const n=deterministicReplyMessageId("rsn",t.channelId,t.inboundMessageId,t.selfPrincipalId);return v=a.messages.startStream({conversationId:t.channelId,clientMessageId:n,idempotencyKey:n,replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reasoning",content:{}}),v},abortSignal:y.signal,callbackQueue:k,logger:r,requestId:d,runId:l,channelId:t.channelId,inboundMessageId:t.inboundMessageId,origin:m,onAgentRunStart:e=>{s.bindRun(_,e)}}),O=interceptAgentRuntimeFailures(C,"im_runtime",e=>r.warn?.(`[im] runtime failure reply suppressed requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} error_code=${e.errorCode}`)),T=O.sink,A=async(e,n)=>{if(!v)return!1;try{return"cancel"===e?await v.cancel({stopReason:"user_stop"}):"fail"===e?await v.fail(n?{stopReason:n}:void 0):await v.finalize({stopReason:"end_turn"}),!1}catch(e){return r.warn?.(`[im] runtime reasoning stream terminal write failed requestId=${d} channel=${t.channelId}: ${formatErrMessage(e)}`),!0}};let D,U,K=!1;const N=async e=>{T.dispatcher.markComplete(e);try{await T.dispatcher.waitForIdle()}catch{}},F=()=>{b();const n=normalizePromptTimeoutMs(e.abortTerminalGraceMs)??1e4;w=setTimeout(()=>{w=void 0,(async()=>{if(S.isTerminated)return;const e=y.signal.reason??f,n=void 0!==U,a=!n&&"string"==typeof e&&e.startsWith("chat_cancel");if(await N("failure"),!S.isTerminated)try{a?(await A("cancel"),await S.cancel({stopReason:"user_stop"})):n?(await A("fail","timeout"),await failMessageStream(S,{body:U?.message??"IM runtime dispatch timed out",stopReason:"timeout"})):(await A("fail"),await failMessageStream(S,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"})),r.warn?.(`[im] abort settlement watchdog terminalized reply requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} user_cancel=${a} timeout=${n}`)}catch(e){r.warn?.(`[im] abort settlement watchdog terminal write failed requestId=${d} channel=${t.channelId}: ${formatErrMessage(e)}`)}})()},n)};y.signal.aborted?F():y.signal.addEventListener("abort",F,{once:!0});const L=()=>{if(!y.signal.aborted)return;const e=y.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))};let Q,P,Y;const G=async e=>{if(!_)throw new Error("session_mutation_not_acquired");const n=await s.converge(_,e);return n.ok?(Q=n.permit,!0):(r.error?.(`[im] session mutation recovery required requestId=${d} sessionKey=${h} reason=${n.recovery.reason}`),P={code:n.recovery.code,reason:n.recovery.reason,sessionKey:n.recovery.sessionKey,operationId:n.recovery.operationId,surface:"message_service",...n.recovery.terminalKind?{terminalKind:n.recovery.terminalKind}:{}},!1)};try{L();const o=s.tryStartInteractive({sessionKey:h,label:"im_chat_message"},"message_service");if(!o.ok){try{await failMessageStream(S,{body:SESSION_BUSY_PUBLIC_MESSAGE,stopReason:"error"})}catch(e){r.warn?.(`[im] session busy terminal write failed channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}return r.warn?.(`[im] session write gate busy sessionKey=${h} channel=${t.channelId} in_reply_to=${t.inboundMessageId} active_operation=${o.activeOperationId??"unknown"}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"session_busy",cause:"runtime_error"}),!1}if(_=o.token,s.bindRun(_,l),c){const e=s.snapshot(h);Y={runtimeConversationUid:t.channelId,runtimeSessionKey:t.channelId,historyGeneration:c.historyGeneration(t.channelId),targetMessageId:t.inboundMessageId,runtimeRunId:l,activeRunRegistrationId:`im:${e?.operationId??l}`},c.activeRuns.register(Y,async()=>{const e=`chat_cancel:activeRun:${Y.activeRunRegistrationId}`,n=await i.abort(g,e);return n.ok||"already_aborted"===n.reason?"accepted":"terminal_committing"===n.reason||"not_found"===n.reason?"already_terminal":"recovery_required"})}L(),r.info?.(`[im] runtime dispatching requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} origin=${m}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId});const y=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:t.dispatchInput,replyRoute:p,replySinks:{im:T},onDispatchInvoked:()=>{K=!0}});void 0!==I&&(D=setTimeout(()=>{U=new Error(`IM runtime dispatch timed out after ${I}ms`),r.warn?.(`[im] runtime dispatch timeout requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} timeout_ms=${I}`),M(U)},I));const f=Promise.race([y,E]),R=await f.finally(()=>{D&&(clearTimeout(D),D=void 0)}),w=O.detectedFailure();if(w)throw new Error(w.diagnosticMessage);if(L(),await N("success"),L(),!i.claimTerminal(g,"completion")){if(L(),"cancelling"===i.phase(g))throw new Error("cancellation in progress");throw new Error("terminal outcome already claimed")}if(!await G("completed"))return await A("fail"),await failMessageStream(S,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:"runtime_error",...P?{recovery:P}:{}}),!1;const b=await A("complete");let $=!1;try{await S.finalize({stopReason:"end_turn"})}catch(e){$=!0,r.warn?.(`[im] runtime reply stream finalize outcome unknown requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}c&&Y&&c.activeRuns.markTerminal(Y,"completed"),r.info?.(`[im] runtime dispatch completed requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} counts=${JSON.stringify(R.counts)} elapsed_ms=${Date.now()-u}`),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,counts:R.counts,...$?{terminalWriteFailed:!0}:{},...b?{reasoningTerminalWriteFailed:!0}:{}});const v="string"==typeof t.dispatchInput.message?t.dispatchInput.message:"",q=T.dispatcher.getAccumulatedBody?.()??"";return maybeRenameImConversationTitle({client:a,conversationId:t.channelId,question:v,answer:q,logger:r}),!0}catch(n){await N("failure");const a=y.signal.aborted,s=a?y.signal.reason:void 0,o="cancelling"===i.phase(g),l=void 0!==U,u=s??f,m=!l&&"string"==typeof u&&u.startsWith("chat_cancel");let p="runtime_error";l?p="timeout":(a||o)&&(p="abort");const I=o||m||l||a?"cancellation":"completion";if(!i.claimTerminal(g,I)){if(r.warn?.(`[im] terminal claim raced requestId=${d} channel=${t.channelId}; best-effort envelope terminal if still streaming`),!S.isTerminated)try{m?(await A("cancel"),await S.cancel({stopReason:"user_stop"})):l?(await A("fail","timeout"),await failMessageStream(S,{body:U?.message??formatErrMessage(n),stopReason:"timeout"})):(await A("fail"),await failMessageStream(S,{body:formatErrMessage(n),stopReason:"error"}))}catch(e){r.warn?.(`[im] raced terminal write failed requestId=${d}: ${formatErrMessage(e)}`)}throw n}const h=m?"cancelled":l?"timeout":"error";if(c&&Y&&c.activeRuns.markTerminal(Y,"cancelled"===h?"cancelled":"failed"),_&&!await G(h))throw m?(await A("cancel"),await S.cancel({stopReason:"user_stop"}).catch(()=>{})):(await A("fail"),await failMessageStream(S,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{})),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:p,...P?{recovery:P}:{}}),n;const R=classifyAgentRuntimeFailure({raw:n,source:"im_runtime",runtimeKind:"openclaw",requestAborted:m}),E=m?R:O.detectedFailure()??R;m?await A("cancel"):l?await A("fail","timeout"):await A("fail");try{m?await S.cancel({stopReason:"user_stop"}):await failMessageStream(S,{body:E.publicMessage,stopReason:messageStopReasonForRuntimeFailure(E)})}catch(e){r.warn?.(`[im] runtime stream terminal write failed requestId=${d} channel=${t.channelId}: ${formatErrMessage(e)}`)}throw r.warn?.(`[im] runtime dispatch failed requestId=${d} channel=${t.channelId} in_reply_to=${t.inboundMessageId} cause=${p} dispatchInvoked=${K} error_code=${E.errorCode}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:d,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:E.errorCode,cause:p}),n}finally{b(),i.unregister(g),K?Q&&s.release(Q):s.releaseBeforeRuntime(_)}};
@@ -0,0 +1 @@
1
+ import{appendFile,mkdir,readFile}from"node:fs/promises";import{dirname}from"node:path";export class FileInboundExecutionJournal{path;claimed=new Set;loaded;writes=Promise.resolve();constructor(e){this.path=e}async claim(e){if(!e)throw new Error("inbound execution command key is required");await(this.loaded??=this.load());const i=this.writes.then(async()=>this.claimed.has(e)?"existing":(await mkdir(dirname(this.path),{recursive:!0,mode:448}),await appendFile(this.path,`${JSON.stringify({version:1,commandKey:e})}\n`,{encoding:"utf8",mode:384}),this.claimed.add(e),"acquired"));return this.writes=i.then(()=>{},()=>{}),await i}async load(){let e;try{e=await readFile(this.path,"utf8")}catch(e){if(isNodeError(e)&&"ENOENT"===e.code)return;throw e}const i=e.split("\n");""===i.at(-1)&&i.pop();for(const e of i){const i=JSON.parse(e);if(1!==i.version||"string"!=typeof i.commandKey||0===i.commandKey.length)throw new Error("inbound execution journal is corrupt");this.claimed.add(i.commandKey)}}}function isNodeError(e){return e instanceof Error&&"code"in e}