beeos-claw 0.6.36 → 0.6.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-gateway/background-run-im-egress.js +1 -1
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-gateway/gateway-events.js +1 -1
- package/dist/acp-gateway/mcp-desired-state-controller.js +1 -1
- package/dist/acp-gateway/mcp-native-config-projector.js +1 -0
- package/dist/acp-gateway/mcp-service.js +1 -1
- package/dist/acp-gateway/session-state.js +1 -1
- package/dist/acp-gateway/tool-result-emitter.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
|
@@ -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{awaitBackgroundRunArtifactQuiescence,backgroundRunEgressMessageId,backgroundRunFileParts,buildBackgroundRunImEgress,dedupeArtifacts,extractMessageToolCaption,trackBackgroundRunArtifactWork}from"./background-run-im-egress.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,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof e.message||"string"==typeof e.reason;if(t||o){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 n?.message?n.message:void 0)??("string"==typeof e.reason?e.reason:void 0)??(n?JSON.stringify(n):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 n=roleFromSessionUpdate(t);n&&(e.lastTurnIdByRole||(e.lastTurnIdByRole={}),e.lastTurnIdByRole[n]=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 n=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.callId)??asString(t.id);if(n){const t=this.state.toolCallIdMap.get(n);return t?(e.sessionId&&this.state.toolCallToSessionMap.set(t,e.sessionId),t):(this.state.toolCallIdMap.set(n,n),e.sessionId&&this.state.toolCallToSessionMap.set(n,e.sessionId),n)}const o=`tc_${e.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return e.sessionId&&this.state.toolCallToSessionMap.set(o,e.sessionId),o}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 n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(t.hasAgentThinkingStream)continue;const e=normalizeText(o.thinking)??normalizeText(o.text);if(!e)continue;const s=dedupKey("chat:thinking",e);if(t.chatReplayDedupKeys.has(s))continue;t.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...r?{timestamp:r}:{}};this.sendTurnUpdate(t,n)&&t.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(t,r),t.hasAgentToolStartStream)continue;const e=this.resolveToolCallId(t,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",e,JSON.stringify(n));if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:e,toolName:s,args:n,timestamp:r});this.sendTurnUpdate(t,i)&&t.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(t,r),t.hasAgentToolResultStream)continue;const e=this.resolveToolCallId(t,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",e,a);if(t.chatReplayDedupKeys.has(i))continue;t.chatReplayDedupKeys.add(i),trackBackgroundRunArtifactWork(t,this.emitNormalizedToolResult(t,{toolCallId:e,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)}));continue}if("text"===a&&o.text){if(t.hasAgentAssistantStream||t.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.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 i=t.chatAssistantTextSoFar??"";let l;if(t.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${e.seq} runId=${e.runId} len=${l.length} prevLen=${i.length}`);const c={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};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.maybePublishBackgroundRunToIm(t),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,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});"interrupted"===n.kind?this.observeLifecycle(this.interruptPrompt(t,n),"chat_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,n.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),n=e=>{this.sendTurnUpdate(t,e),t.assistantMq?.push(e)};if("assistant"===e.stream){t.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(t,e.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=t.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:t.sessionId,runId:e.runId,cumulative:o,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"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=e.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const e of a){if(!isRecord(e))continue;const o=asString(e.type);if("thinking"===o){t.hasAgentThinkingStream=!0;const n=normalizeText(e.thinking)??normalizeText(e.text);if(n){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}continue}if("toolCall"===o){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(t,e,"start"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",r=resolveToolArgs(e);n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(t,e,"result"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",r=e.result,i=normalizeText(e.text)??normalizeText(e.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&trackBackgroundRunArtifactWork(t,this.emitNormalizedToolResult(t,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).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 n=normalizeText(e.data?.delta)??normalizeText(e.data?.text);if(n){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}else if("tool"===e.stream){const o=e.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const e=this.resolveToolCallId(t,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.captureBackgroundRunCaption(t,a,r),n(buildToolCallPayload({toolCallId:e,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0,trackBackgroundRunArtifactWork(t,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=o?.toolCall;if(e){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const a=asString(e?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...e},i=this.resolveToolCallId(t,r,"start"),l=resolveToolArgs(r);this.captureBackgroundRunCaption(t,a,l),n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===e.stream){const s=e.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${e.runId} sessionKey=${e.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,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),o,t)};if("end"===n){const e=lifecycleEndFailureMessage(s);if(e)o(e,"agent_failure");else{const e="string"==typeof s?.stopReason?s.stopReason:void 0;this.maybePublishBackgroundRunToIm(t),this.observeLifecycle(this.completePrompt(t,e||"end_turn"),"agent_final",t)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.completePrompt(t,"cancelled"),"agent_cancelled",t);else if("error"===n){const e=s?.error,t=e&&"object"==typeof e?e:void 0;o(("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 n=t.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${t.runId}_${t.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,c=a??r,d=n?.result;await this.emitNormalizedToolResult(e,{toolCallId:r,normalizeToolCallId:c,toolName:i,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.backgroundRunAdopted=!0,e.backgroundRunArtifacts=[],e.assistantMq&&!e.assistantMq.closed||(e.assistantMq=new MessageQueue),this.schedulePromptTimeout(e),!0}maybePublishBackgroundRunToIm(e){const t=buildBackgroundRunImEgress(e);if(!t.publish&&"no_publishable_content"!==t.reason)return void("not_background_run"!==t.reason&&"cron_owned"!==t.reason&&this.logger.debug?.(`[gateway-events] background run not published to IM (reason=${t.reason} sessionId=${e.sessionId} runId=${e.currentRunId??"n/a"})`));e.backgroundRunAdopted=!1,e.backgroundRunCaption=void 0,e.backgroundRunEgressSettling=!0;const s=t.publish?t.egress:{conversationId:e.channelId.trim(),runId:e.currentRunId.trim(),text:"",artifacts:[],...e.inboundMessageId?.trim()?{replyTo:e.inboundMessageId.trim()}:{}};this.observeLifecycle(this.settleAndPublishBackgroundRun(e,s),"background_run_im_egress",e)}async settleAndPublishBackgroundRun(e,t){try{await awaitBackgroundRunArtifactQuiescence(e)}finally{e.backgroundRunEgressSettling=!1}const s=dedupeArtifacts([...t.artifacts,...e.backgroundRunArtifacts??[]]);e.backgroundRunArtifacts=void 0,t.text||0!==s.length?await this.publishBackgroundRunResult({...t,artifacts:s}):this.logger.debug?.(`[gateway-events] background run not published to IM (reason=no_publishable_content sessionId=${e.sessionId} runId=${t.runId})`)}captureBackgroundRunCaption(e,t,s){if(!0!==e.backgroundRunAdopted)return;const n=extractMessageToolCaption(t,s);n&&(e.backgroundRunCaption=n)}async publishBackgroundRunResult(e){const t=this.getMessageClient?.();if(!t)return;const s=backgroundRunFileParts(e.artifacts),n=backgroundRunEgressMessageId(e.runId);try{const o=t.messages.startStream({conversationId:e.conversationId,clientMessageId:n,idempotencyKey:n,type:"agent_reply",content:{background_run_id:e.runId},...e.replyTo?{replyTo:e.replyTo}:{}});await o.opened(),e.text&&o.setBody(e.text);for(const e of s)o.addPart(e);await o.finalize({stopReason:"end_turn"}),this.logger.info?.(`[gateway-events] published background run to conversation=${e.conversationId} run=${e.runId} fileParts=${s.length} textLen=${e.text.length}`),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"background_run_im_egress.published",severity:"info",summary:`background run published to ${e.conversationId}`,payload:{conversationId:e.conversationId,runId:e.runId,filePartCount:s.length,textLen:e.text.length,hasReplyTo:Boolean(e.replyTo)}})}catch(t){const s=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,s))return void this.logger.info?.(`[gateway-events] origin conversation gone, skipping background run publish: conversation=${e.conversationId} run=${e.runId} err=${s}`);this.logger.warn?.(`[gateway-events] publishBackgroundRunResult failed: ${s}`),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"background_run_im_egress.failed",severity:"warn",summary:s,payload:{conversationId:e.conversationId,runId:e.runId}})}}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 n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(t.sessionId=n),("string"!=typeof t.name||!t.name)&&"string"==typeof e.jobId&&e.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)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 o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{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",{}),n=e?.jobs;if(Array.isArray(n))for(const e of n){if(!e||"object"!=typeof e)continue;const n=e;if(n.id===t){s=n;break}}}catch(e){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${e instanceof Error?e.message:e}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const e=decodeBeeosChannelId(n);return void(e&&this.setCronChannelCacheLocal(t,e))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${t}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${t}`);const c=encodeBeeosChannelTag(n,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 n="string"==typeof e.jobId?e.jobId:void 0;if(!n)return;const o=e.summary;let a=this.cronChannelCache?.get(n);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===n){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 r=randomUUID();try{const s=t.messages.startStream({conversationId:a,clientMessageId:r,idempotencyKey:r,type:"agent_reply",content:{cron_job_id:n,..."string"==typeof e.sessionKey?{session_key:e.sessionKey}:{}}});await s.opened(),s.setBody(o),await s.finalize({stopReason:"end_turn"}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}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=${n} err=${t}`),void this.cronChannelCache?.delete(n);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{awaitBackgroundRunArtifactQuiescence,beginBackgroundRunUploadBarrier,backgroundRunEgressMessageId,backgroundRunFileParts,buildBackgroundRunImEgress,clearBackgroundRunUploadBarriers,dedupeArtifacts,extractMessageToolCaption,trackBackgroundRunArtifactWork}from"./background-run-im-egress.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,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof e.message||"string"==typeof e.reason;if(t||o){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 n?.message?n.message:void 0)??("string"==typeof e.reason?e.reason:void 0)??(n?JSON.stringify(n):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 n=roleFromSessionUpdate(t);n&&(e.lastTurnIdByRole||(e.lastTurnIdByRole={}),e.lastTurnIdByRole[n]=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 n=asString(t.toolCallId)??asString(t.tool_call_id)??asString(t.callId)??asString(t.id);if(n){const t=this.state.toolCallIdMap.get(n);return t?(e.sessionId&&this.state.toolCallToSessionMap.set(t,e.sessionId),t):(this.state.toolCallIdMap.set(n,n),e.sessionId&&this.state.toolCallToSessionMap.set(n,e.sessionId),n)}const o=`tc_${e.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return e.sessionId&&this.state.toolCallToSessionMap.set(o,e.sessionId),o}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 n of s.content){const o=n;if(!isRecord(o))continue;const a=asString(o.type);if(!a)continue;const r=toISO(s.timestamp);if("thinking"===a){if(t.hasAgentThinkingStream)continue;const e=normalizeText(o.thinking)??normalizeText(o.text);if(!e)continue;const s=dedupKey("chat:thinking",e);if(t.chatReplayDedupKeys.has(s))continue;t.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:e},...r?{timestamp:r}:{}};this.sendTurnUpdate(t,n)&&t.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(t,r),t.hasAgentToolStartStream)continue;const e=this.resolveToolCallId(t,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",e,JSON.stringify(n));if(t.chatReplayDedupKeys.has(a))continue;t.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:e,toolName:s,args:n,timestamp:r});beginBackgroundRunUploadBarrier(t,e,s),this.sendTurnUpdate(t,i)&&t.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(t,r),t.hasAgentToolResultStream)continue;const e=this.resolveToolCallId(t,o,"result"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=o.result,a=normalizeText(o.text)??normalizeText(o.result)??(isRecord(n)||Array.isArray(n)?JSON.stringify(n,null,2):void 0);if(!a)continue;const i=dedupKey("chat:tool_result",e,a);if(t.chatReplayDedupKeys.has(i))continue;t.chatReplayDedupKeys.add(i),trackBackgroundRunArtifactWork(t,this.emitNormalizedToolResult(t,{toolCallId:e,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(e=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${e instanceof Error?e.message:e}`)}));continue}if("text"===a&&o.text){if(t.hasAgentAssistantStream||t.agentAssistantTextSoFar)continue;const s=stripTransportMetadata(o.text);if(!s)continue;const n=classifyHeartbeatFilter(s);if(n.filtered){this.logHeartbeatSkip({source:"chat-text",reason:n.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 i=t.chatAssistantTextSoFar??"";let l;if(t.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${e.seq} runId=${e.runId} len=${l.length} prevLen=${i.length}`);const d={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(t,d),t.assistantMq?.push(d)}}}}else"delta"===e.state&&this.touchStreamActivity(t);if(this.maybeCompletePromptFromChatState(t,e),!t.promptDone)if("final"===e.state)this.maybePublishBackgroundRunToIm(t),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,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});"interrupted"===n.kind?this.observeLifecycle(this.interruptPrompt(t,n),"chat_interrupt",t):this.observeLifecycle(this.failPrompt(t,Errors.AgentError.rpcCode,n.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),n=e=>{this.sendTurnUpdate(t,e),t.assistantMq?.push(e)};if("assistant"===e.stream){t.hasAgentAssistantStream=!0;const o=this.state.extractAgentAssistantDelta(t,e.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=t.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:t.sessionId,runId:e.runId,cumulative:o,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"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=e.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const e of a){if(!isRecord(e))continue;const o=asString(e.type);if("thinking"===o){t.hasAgentThinkingStream=!0;const n=normalizeText(e.thinking)??normalizeText(e.text);if(n){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}continue}if("toolCall"===o){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(t,e,"start"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",r=resolveToolArgs(e);beginBackgroundRunUploadBarrier(t,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(t,e,"result"),a=asString(e.name)??asString(e.toolName)??asString(e.tool_name)??"tool",r=e.result,i=normalizeText(e.text)??normalizeText(e.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&trackBackgroundRunArtifactWork(t,this.emitNormalizedToolResult(t,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).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 n=normalizeText(e.data?.delta)??normalizeText(e.data?.text);if(n){const e={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}else if("tool"===e.stream){const o=e.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const e=this.resolveToolCallId(t,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);beginBackgroundRunUploadBarrier(t,e,a),this.captureBackgroundRunCaption(t,a,r),n(buildToolCallPayload({toolCallId:e,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(t,s),t.hasAgentToolResultStream=!0,trackBackgroundRunArtifactWork(t,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=o?.toolCall;if(e){this.sendToolCallSpacer(t,s),t.hasAgentToolStartStream=!0;const a=asString(e?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...e},i=this.resolveToolCallId(t,r,"start"),l=resolveToolArgs(r);this.captureBackgroundRunCaption(t,a,l),n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===e.stream){const s=e.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${e.runId} sessionKey=${e.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,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),o,t)};if("end"===n){const e=lifecycleEndFailureMessage(s);if(e)o(e,"agent_failure");else{const e="string"==typeof s?.stopReason?s.stopReason:void 0;this.maybePublishBackgroundRunToIm(t),this.observeLifecycle(this.completePrompt(t,e||"end_turn"),"agent_final",t)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.completePrompt(t,"cancelled"),"agent_cancelled",t);else if("error"===n){const e=s?.error,t=e&&"object"==typeof e?e:void 0;o(("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 n=t.data,o=n?.toolCall,a=("string"==typeof o?.id?o.id:void 0)??("string"==typeof n?.toolCallId?n.toolCallId:void 0),r=(a?this.state.toolCallIdMap.get(a):void 0)??a??`tc_${t.runId}_${t.seq}`,i=("string"==typeof o?.name?o.name:void 0)??("string"==typeof n?.name?n.name:void 0)??"tool",l="string"==typeof n?.text?n.text:void 0,d=a??r,c=n?.result;await this.emitNormalizedToolResult(e,{toolCallId:r,normalizeToolCallId:d,toolName:i,rawText:l,rawResult:c,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.backgroundRunAdopted=!0,e.backgroundRunArtifacts=[],clearBackgroundRunUploadBarriers(e),e.assistantMq&&!e.assistantMq.closed||(e.assistantMq=new MessageQueue),this.schedulePromptTimeout(e),!0}maybePublishBackgroundRunToIm(e){const t=buildBackgroundRunImEgress(e);if(!t.publish&&"no_publishable_content"!==t.reason)return void("not_background_run"!==t.reason&&"cron_owned"!==t.reason&&this.logger.debug?.(`[gateway-events] background run not published to IM (reason=${t.reason} sessionId=${e.sessionId} runId=${e.currentRunId??"n/a"})`));e.backgroundRunAdopted=!1,e.backgroundRunCaption=void 0,e.backgroundRunEgressSettling=!0;const s=t.publish?t.egress:{conversationId:e.channelId.trim(),runId:e.currentRunId.trim(),text:"",artifacts:[],...e.inboundMessageId?.trim()?{replyTo:e.inboundMessageId.trim()}:{}};this.observeLifecycle(this.settleAndPublishBackgroundRun(e,s),"background_run_im_egress",e)}async settleAndPublishBackgroundRun(e,t){try{await awaitBackgroundRunArtifactQuiescence(e)}finally{e.backgroundRunEgressSettling=!1}const s=dedupeArtifacts([...t.artifacts,...e.backgroundRunArtifacts??[]]),n=(e.backgroundRunExpectedUploadToolCalls?.size??0)>0;e.backgroundRunArtifacts=void 0,clearBackgroundRunUploadBarriers(e),n&&0===s.length?await this.publishBackgroundRunResult({...t,text:"The generated attachment could not be delivered.",artifacts:[],deliveryFailure:"attachment_delivery_failed"}):t.text||0!==s.length?await this.publishBackgroundRunResult({...t,artifacts:s}):this.logger.debug?.(`[gateway-events] background run not published to IM (reason=no_publishable_content sessionId=${e.sessionId} runId=${t.runId})`)}captureBackgroundRunCaption(e,t,s){if(!0!==e.backgroundRunAdopted)return;const n=extractMessageToolCaption(t,s);n&&(e.backgroundRunCaption=n)}async publishBackgroundRunResult(e){const t=this.getMessageClient?.();if(!t)return;const s=backgroundRunFileParts(e.artifacts),n=backgroundRunEgressMessageId(e.runId);try{const o=t.messages.startStream({conversationId:e.conversationId,clientMessageId:n,idempotencyKey:n,type:"agent_reply",content:{background_run_id:e.runId},...e.replyTo?{replyTo:e.replyTo}:{}});await o.opened(),e.text&&o.setBody(e.text);for(const e of s)o.addPart(e);e.deliveryFailure?await o.fail({stopReason:e.deliveryFailure}):await o.finalize({stopReason:"end_turn"});const a={conversationId:e.conversationId,runId:e.runId,filePartCount:s.length,textLen:e.text.length,hasReplyTo:Boolean(e.replyTo)};e.deliveryFailure?(this.logger.warn?.(`[gateway-events] failed background run delivery conversation=${e.conversationId} run=${e.runId} reason=${e.deliveryFailure}`),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"background_run_im_egress.failed",severity:"warn",summary:e.deliveryFailure,payload:a})):(this.logger.info?.(`[gateway-events] published background run to conversation=${e.conversationId} run=${e.runId} fileParts=${s.length} textLen=${e.text.length}`),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"background_run_im_egress.published",severity:"info",summary:`background run published to ${e.conversationId}`,payload:a}))}catch(t){const s=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,s))return void this.logger.info?.(`[gateway-events] origin conversation gone, skipping background run publish: conversation=${e.conversationId} run=${e.runId} err=${s}`);this.logger.warn?.(`[gateway-events] publishBackgroundRunResult failed: ${s}`),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"background_run_im_egress.failed",severity:"warn",summary:s,payload:{conversationId:e.conversationId,runId:e.runId}})}}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 n=s?this.state.findSessionByKey(s)?.sessionId:void 0;if(n&&(t.sessionId=n),("string"!=typeof t.name||!t.name)&&"string"==typeof e.jobId&&e.jobId&&this.gatewayRequest)try{const s=await this.gatewayRequest("cron.list",{}),n=s?.jobs;if(Array.isArray(n))for(const s of n)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 o=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(o,{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",{}),n=e?.jobs;if(Array.isArray(n))for(const e of n){if(!e||"object"!=typeof e)continue;const n=e;if(n.id===t){s=n;break}}}catch(e){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${e instanceof Error?e.message:e}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const e=decodeBeeosChannelId(n);return void(e&&this.setCronChannelCacheLocal(t,e))}if("main"===("string"==typeof s.sessionTarget?s.sessionTarget:void 0))return;const o=isRecord(s.payload)?s.payload:void 0;if("systemEvent"===(o&&"string"==typeof o.kind?o.kind:void 0))return;const a=isRecord(s.delivery)?s.delivery:void 0;if(a&&"none"===a.mode)return;const r="string"==typeof s.sessionKey?s.sessionKey:void 0;if(!r)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: job has no sessionKey job=${t}`);const i=this.state.findSessionByKey(r),l=i?.channelId;if(!l)return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel: no channelId on ACPSession for sessionKey=${r} job=${t}`);const d=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:t,patch:{description:d}}),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 n="string"==typeof e.jobId?e.jobId:void 0;if(!n)return;const o=e.summary;let a=this.cronChannelCache?.get(n);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===n){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 r=randomUUID();try{const s=t.messages.startStream({conversationId:a,clientMessageId:r,idempotencyKey:r,type:"agent_reply",content:{cron_job_id:n,..."string"==typeof e.sessionKey?{session_key:e.sessionKey}:{}}});await s.opened(),s.setBody(o),await s.finalize({stopReason:"end_turn"}),this.logger.info?.(`[gateway-events] published cron result to channel=${a} job=${n}`)}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=${n} err=${t}`),void this.cronChannelCache?.delete(n);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{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;bridge;credentials;references;releases;receiptHistory;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;authorityCloseBlocked=!1;constructor(e,t,i,r,s,n){this.store=e,this.bridge=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=n,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0)})}async reconnect(e){await this.exclusive(()=>this.reconcileAll(e))}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.bridge.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,connected:Boolean(r),tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.bridge.unset(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r}))}async unset(e,t){return await this.exclusive(async()=>await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.bridge.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r}))}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const n=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,n,i))return blocked(s.revision);const a=i?await this.findAttemptActive(s,n,i):void 0;if((i||this.releases)&&!a){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.bridge.unset(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&n?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!n){this.activeCredentialRevisions.delete(e),await this.bridge.unset(e);const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,void 0,i,a)}const o=a?releaseKey(a):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.bridge.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,n,i,a)}const c=await this.reconcileServer(n,s,t,a,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),withOperationReceipt(c,s,n,i,a)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear(),this.authorityEpoch=null,await this.bridge.close()})}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));const i=this.bridge.detachMany(this.credentialServers.keys()).then(()=>({ok:!0}),e=>({ok:!1,error:e}));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t,i))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t,i){const r=await i;if(!r.ok)throw this.authorityCloseBlocked=!0,r.error;const s=await this.store.load();if(this.rememberDesired(s),t!==this.authorityGeneration||null===e||this.authorityCloseBlocked)return;const n=this.credentials?.currentAuthority();n&&n.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,s))}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.bridge.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.projections.set(e.name,blocked(r.revision)),await this.bridge.unset(e.name)}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.bridge.list())t.has(e.name)||await this.bridge.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),n=new Set(i.servers.map(e=>e.name));for(const e of this.bridge.list())n.has(e.name)||await this.bridge.unset(e.name);for(const t of i.servers){let n=r.find(e=>releaseMatchesDesired(e,i,t));if(!n){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.bridge.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}n=r}if(t.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==n.runtimeEpoch){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.bridge.unset(t.name),this.projections.set(t.name,blocked(i.revision));continue}const a=releaseKey(n);if(this.liveReleaseKeys.get(t.name)===a&&this.bridge.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,n)).state&&this.liveReleaseKeys.set(t.name,releaseKey(n))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,n){try{e.credentialRef&&this.bridge.list().some(t=>t.name===e.name)&&await this.bridge.detach(e.name),await this.connectServer(e,t,i,r,s,n);const a=synced(t.revision);return this.projections.set(e.name,a),a}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.bridge.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}const r=pending(t.revision);return this.projections.set(e.name,r),r}}async connectServer(e,t,i,r,s,n){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(n){if(!s||n.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,n.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const a={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&n.runtimeEpoch===s.runtimeEpoch},o=await this.bridge.set(e.name,n.config,n.credential,a);if(!a.isCurrent())throw await this.bridge.detach(e.name),new Error("MCP operation-scoped activation lost authority during bridge connection");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.bridge.set(e.name,e.config);if(i?.aborted)throw await this.bridge.detach(e.name),new Error("MCP projection attempt lost authority during bridge connection");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const a=this.credentials.currentAuthority();if(!a||null===this.authorityEpoch||a.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),u=this.revoked.get(e.credentialRef);if(void 0!==u&&l<=u)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const m=await this.bridge.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw await this.bridge.detach(e.name),new Error("runtime-scoped MCP credential authority changed during bridge connection");return this.activeCredentialRevisions.set(e.name,l),m}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef)}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const n=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:n})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,n=s.payload,a=i.effectCommitted,o=a?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||n.operationId!==e.operationId||n.requestMessageId!==r.requestMessageId||n.requestHash!==r.requestHash||n.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==n.method:"mcp/unset"!==n.method)||n.command.name!==e.method||n.dispatchEpoch!==e.runtimeEpoch||!a||!o||a.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||a.channel_id!==e.operationId||a.in_reply_to!==r.requestMessageId||a.publisher_id!==n.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==n.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==n.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===n.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===n.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
1
|
+
import{RUNTIME_MESSAGE_TYPES}from"../runtime/runtime-message-channel-contract.js";export class McpCredentialRevokedError extends Error{credentialRef;constructor(e){super("MCP credential is revoked"),this.credentialRef=e}}export class McpDesiredStateController{store;bridge;credentials;references;releases;receiptHistory;nativeProjection;logger;tail=Promise.resolve();unsubscribe;started=!1;revoked=new Map;activeCredentialRevisions=new Map;projections=new Map;credentialServers=new Map;liveReleaseKeys=new Map;resolvedNativeMaterial=new Map;credentialRenewalTimers=new Map;desiredRevision="0";authorityEpoch;authorityGeneration=0;authorityCloseBlocked=!1;constructor(e,t,i,r,s,a,n,o){this.store=e,this.bridge=t,this.credentials=i,this.references=r,this.releases=s,this.receiptHistory=a,this.nativeProjection=n,this.logger=o,this.authorityEpoch=i?.currentAuthority()?.runtimeEpoch??null}setRuntimeReceiptHistoryPort(e){this.receiptHistory=e}async start(e){await this.exclusive(async()=>{this.started||(this.started=!0,this.authorityEpoch=this.credentials?.currentAuthority()?.runtimeEpoch??null,this.unsubscribe=this.credentials?.subscribe(e=>this.acceptCredentialEvent(e)));const t=await this.store.load();await this.reconcileAll(e,t),await this.collectLocalGarbage(void 0,!0),this.scheduleNativeConverge()})}async reconnect(e){await this.exclusive(async()=>{await this.reconcileAll(e),this.scheduleNativeConverge()})}async list(){return await this.exclusive(async()=>{const e=await this.store.load();this.rememberDesired(e);const t=new Map(this.bridge.list().map(e=>[e.name,e]));return{revision:e.revision,servers:e.servers.map(i=>{const r=t.get(i.name);return Object.freeze({...i,connected:Boolean(r),tools:r?.tools??Object.freeze([]),projection:this.projections.get(i.name)??pending(e.revision)})})}})}async set(e,t){return await this.exclusive(async()=>{const i=await this.store.transaction(async i=>{const r=await i.set(e);return e.credentialRef&&this.references?.owns(e.credentialRef)&&await this.references.commit(e.credentialRef).catch(()=>{}),this.rememberDesired(r),this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.resolvedNativeMaterial.delete(e.name),this.clearCredentialRenewal(e.name),await this.bridge.unset(e.name),this.projections.set(e.name,pending(r.revision)),await this.recordDesiredStateCommit(r,e.name,t),await this.collectLocalGarbage(i),r});return this.scheduleNativeConverge(),i})}async unset(e,t){return await this.exclusive(async()=>{const i=await this.store.transaction(async i=>{const r=await i.unset(e);return this.rememberDesired(r),this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.resolvedNativeMaterial.delete(e),this.clearCredentialRenewal(e),await this.bridge.unset(e),this.projections.set(e,pending(r.revision)),await this.recordDesiredStateCommit(r,e,t),await this.collectLocalGarbage(i),r});return this.scheduleNativeConverge(),i})}async isCredentialReferenced(e){return await this.exclusive(async()=>(await this.store.load()).servers.some(t=>t.credentialRef===e))}async removeLocalCredentialIfUnreferenced(e){await this.exclusive(async()=>{this.references?.owns(e)&&await this.store.transaction(async t=>{(await t.load()).servers.some(t=>t.credentialRef===e)||(await t.assertOwned(),await this.references.remove(e))})})}async project(e,t,i,r){return await this.exclusive(async()=>{const s=await this.store.load();this.rememberDesired(s);const a=s.servers.find(t=>t.name===e);if(t?.aborted)return pending(s.revision);if(i&&!matchesProjectionAttempt(e,a,i))return blocked(s.revision);const n=i?await this.findAttemptActive(s,a,i):void 0;if((i||this.releases)&&!n){this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),await this.bridge.unset(e);const t=pending(s.revision);return this.projections.set(e,t),t}if(i&&!r&&a?.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==i.runtimeEpoch)return this.projections.set(e,blocked(s.revision)),blocked(s.revision);if(!a){this.activeCredentialRevisions.delete(e),this.resolvedNativeMaterial.delete(e),this.clearCredentialRenewal(e),await this.bridge.unset(e);const t=synced(s.revision);return this.projections.set(e,t),this.scheduleNativeConverge(),withOperationReceipt(t,s,void 0,i,n)}const o=n?releaseKey(n):void 0;if(o&&this.liveReleaseKeys.get(e)===o&&this.bridge.list().some(t=>t.name===e)){const t=synced(s.revision);return this.projections.set(e,t),withOperationReceipt(t,s,a,i,n)}const c=await this.reconcileServer(a,s,t,n,i,r);return"synced"===c.state&&o&&this.liveReleaseKeys.set(e,o),this.scheduleNativeConverge(),withOperationReceipt(c,s,a,i,n)})}async stop(){await this.exclusive(async()=>{this.started=!1,this.unsubscribe?.(),this.unsubscribe=void 0,this.activeCredentialRevisions.clear(),this.revoked.clear(),this.projections.clear(),this.credentialServers.clear(),this.liveReleaseKeys.clear(),this.resolvedNativeMaterial.clear();for(const e of[...this.credentialRenewalTimers.keys()])this.clearCredentialRenewal(e);this.authorityEpoch=null,await this.bridge.close()})}nudgeNativeProjection(){this.scheduleNativeConverge()}scheduleNativeConverge(){this.nativeProjection&&this.nativeProjection.scheduleConverge(()=>this.computeNativeTarget())}scheduleCredentialRenewal(e,t){if(this.clearCredentialRenewal(e),!t)return;const i=Date.parse(t);if(!Number.isFinite(i))return void this.logger?.warn?.(`[mcp-desired] ignoring invalid credential expiresAt name=${e}`);const r=Math.max(i-Date.now()-3e5,3e4),s=setTimeout(()=>{this.credentialRenewalTimers.delete(e),this.exclusive(async()=>{this.resolvedNativeMaterial.delete(e),this.scheduleNativeConverge()})},r);s.unref?.(),this.credentialRenewalTimers.set(e,s)}clearCredentialRenewal(e){const t=this.credentialRenewalTimers.get(e);t&&(clearTimeout(t),this.credentialRenewalTimers.delete(e))}async computeNativeTarget(){return await this.exclusive(async()=>{const e=await this.store.load(),t=new Map;for(const i of e.servers){if(!i.config)continue;if(!i.credentialRef){t.set(i.name,{config:i.config});continue}let e=this.resolvedNativeMaterial.get(i.name);if(!e&&this.credentials&&!this.revoked.has(i.credentialRef))try{const t=await this.credentials.resolve({credentialRef:i.credentialRef,serverName:i.name});e=t.credential,this.resolvedNativeMaterial.set(i.name,t.credential),this.scheduleCredentialRenewal(i.name,t.expiresAt)}catch(e){this.logger?.warn?.(`[mcp-desired] native credential resolve failed name=${i.name}: ${e instanceof Error?e.message:e}`)}e&&t.set(i.name,{config:i.config,credential:e})}return t})}acceptCredentialEvent(e){if("authority"===e.type){this.authorityEpoch=null;const t=++this.authorityGeneration;for(const e of this.credentialServers.keys())this.activeCredentialRevisions.delete(e),this.liveReleaseKeys.delete(e),this.projections.set(e,pending(this.desiredRevision));const i=this.bridge.detachMany(this.credentialServers.keys()).then(()=>({ok:!0}),e=>({ok:!1,error:e}));return void this.exclusive(()=>this.onAuthorityEvent(e.runtimeEpoch,t,i))}this.exclusive(()=>this.onCredentialEvent(e))}async onAuthorityEvent(e,t,i){const r=await i;if(!r.ok)throw this.authorityCloseBlocked=!0,r.error;const s=await this.store.load();if(this.rememberDesired(s),t!==this.authorityGeneration||null===e||this.authorityCloseBlocked)return;const a=this.credentials?.currentAuthority();a&&a.runtimeEpoch===e&&(this.authorityEpoch=e,await this.reconcileAll(void 0,s),this.scheduleNativeConverge())}async onCredentialEvent(e){const t=BigInt(e.credentialRevision),i=this.revoked.get(e.credentialRef)??-1n,r=await this.store.load();this.rememberDesired(r);const s=r.servers.filter(t=>t.credentialRef===e.credentialRef);if("revoked"!==e.type){if(!(t<=i)){this.revoked.delete(e.credentialRef);for(const e of s){if(t>(this.activeCredentialRevisions.get(e.name)??-1n)){if(this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),await this.bridge.unset(e.name),!this.releases){await this.reconcileServer(e,r);continue}this.projections.set(e.name,blocked(r.revision))}}this.scheduleNativeConverge()}}else{if(t<i)return;this.revoked.set(e.credentialRef,t);for(const e of s)this.activeCredentialRevisions.delete(e.name),this.liveReleaseKeys.delete(e.name),this.resolvedNativeMaterial.delete(e.name),this.clearCredentialRenewal(e.name),this.projections.set(e.name,blocked(r.revision)),await this.bridge.unset(e.name);this.scheduleNativeConverge()}}async reconcileAll(e,t){const i=t??await this.store.load();if(this.rememberDesired(i),!this.releases){const t=new Set(i.servers.map(e=>e.name));for(const e of this.bridge.list())t.has(e.name)||await this.bridge.unset(e.name);for(const t of i.servers)await this.reconcileServer(t,i,e);return}const r=await this.releases.listMcpActiveDesiredStates(),s=await this.releases.listMcpLiveReleases(),a=new Set(i.servers.map(e=>e.name));for(const e of this.bridge.list())a.has(e.name)||await this.bridge.unset(e.name);for(const t of i.servers){let a=r.find(e=>releaseMatchesDesired(e,i,t));if(!a){const e=s.find(e=>releaseMatchesDesired(e,i,t)),r=void 0===e?"pending":await this.recoverPendingServiceRelease(e,i,t);if("string"==typeof r){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.bridge.unset(t.name),this.projections.set(t.name,"pending"===r?pending(i.revision):blocked(i.revision));continue}a=r}if(t.credentialRef&&this.credentials?.currentAuthority()?.runtimeEpoch!==a.runtimeEpoch){this.activeCredentialRevisions.delete(t.name),this.liveReleaseKeys.delete(t.name),await this.bridge.unset(t.name),this.projections.set(t.name,blocked(i.revision));continue}const n=releaseKey(a);if(this.liveReleaseKeys.get(t.name)===n&&this.bridge.list().some(e=>e.name===t.name)){this.projections.set(t.name,synced(i.revision));continue}"synced"===(await this.reconcileServer(t,i,e,a)).state&&this.liveReleaseKeys.set(t.name,releaseKey(a))}}async recoverPendingServiceRelease(e,t,i){if("service"!==e.source)return"blocked";if(!this.releases||!releaseMatchesDesired(e,t,i))return"blocked";let r;try{r=await this.releases.get(e.operationId)}catch{return"pending"}if(!r||!releaseMatchesJournalRecord(e,r))return"blocked";const s=await this.readReceiptHistory(e.operationId);if("unavailable"===s.status)return"pending";if("not_found"===s.status||"expired"===s.status)return"blocked";if(!serviceHistoryMatchesRelease(e,r,s.snapshot))return"blocked";try{const r=(await this.releases.promoteMcpDesiredStateActive({operationId:e.operationId,source:"service",effectRecordHash:e.effectRecordHash,terminalMessageId:s.snapshot.final.message_id})).mcpActiveDesiredState;return r&&releaseMatchesDesired(r,t,i)?r:"blocked"}catch{return"pending"}}async readReceiptHistory(e){const t=this.receiptHistory;if(!t)return{status:"unavailable"};const i=async()=>{try{return await t.readOperationHistory({operationId:e})}catch{return{status:"unavailable"}}},r=await i();return"unavailable"!==r.status?r:(await Promise.resolve(),await i())}async reconcileServer(e,t,i,r,s,a){try{e.credentialRef&&this.bridge.list().some(t=>t.name===e.name)&&await this.bridge.detach(e.name),await this.connectServer(e,t,i,r,s,a);const n=synced(t.revision);return this.projections.set(e.name,n),n}catch(i){if(i instanceof McpCredentialRevokedError){this.revoked.set(e.credentialRef,this.revoked.get(e.credentialRef)??0n),this.activeCredentialRevisions.delete(e.name),await this.bridge.unset(e.name);const i=blocked(t.revision);return this.projections.set(e.name,i),i}this.logger?.warn?.(`[mcp-desired] projection failed name=${e.name}: ${i instanceof Error?i.message:i}`);const r=pending(t.revision);return this.projections.set(e.name,r),r}}async connectServer(e,t,i,r,s,a){if(i?.aborted)throw new Error("MCP projection attempt is no longer authoritative");if(a){if(!s||a.runtimeEpoch!==s.runtimeEpoch||r&&!releaseAuthorizesServer(r,e,t,a.runtimeEpoch,s))throw new Error("MCP operation-scoped activation is no longer authoritative");const n={generation:this.authorityGeneration,isCurrent:()=>!i?.aborted&&a.runtimeEpoch===s.runtimeEpoch};a.credential?this.resolvedNativeMaterial.set(e.name,a.credential):this.resolvedNativeMaterial.delete(e.name);const o=await this.bridge.set(e.name,a.config,a.credential,n);if(!n.isCurrent())throw this.resolvedNativeMaterial.delete(e.name),await this.bridge.detach(e.name),new Error("MCP operation-scoped activation lost authority during bridge connection");return o}if(r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("MCP live release proof no longer authorizes this server");if(!e.credentialRef){if(this.activeCredentialRevisions.delete(e.name),this.resolvedNativeMaterial.delete(e.name),!e.config)throw new Error("MCP desired state has no persisted public config for this server");const t=await this.bridge.set(e.name,e.config);if(i?.aborted)throw await this.bridge.detach(e.name),new Error("MCP projection attempt lost authority during bridge connection");return t}if(!this.credentials)throw new Error("runtime-scoped MCP credential resolver is unavailable");if(this.revoked.has(e.credentialRef))throw new McpCredentialRevokedError(e.credentialRef);const n=this.credentials.currentAuthority();if(!n||null===this.authorityEpoch||n.runtimeEpoch!==this.authorityEpoch)throw new Error("runtime-scoped MCP credential authority is pending");const o=this.authorityGeneration,c=this.authorityEpoch,d={generation:o,isCurrent:()=>o===this.authorityGeneration&&this.authorityEpoch===c&&this.credentials?.currentAuthority()?.runtimeEpoch===c};if(!d.isCurrent())throw new Error("runtime-scoped MCP credential authority changed");const h=await this.credentials.resolve({credentialRef:e.credentialRef,serverName:e.name,...i?{signal:i}:{}});if(i?.aborted||h.runtimeEpoch!==c||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw new Error("runtime-scoped MCP credential authority changed during resolution");const l=BigInt(h.credentialRevision),u=this.revoked.get(e.credentialRef);if(void 0!==u&&l<=u)throw new McpCredentialRevokedError(e.credentialRef);if(!e.config)throw new Error("MCP desired state has no persisted public config for this server");const v=await this.bridge.set(e.name,e.config,h.credential,d);if(i?.aborted||!d.isCurrent()||r&&!releaseAuthorizesServer(r,e,t,this.credentials?.currentAuthority()?.runtimeEpoch,s))throw await this.bridge.detach(e.name),new Error("runtime-scoped MCP credential authority changed during bridge connection");return this.activeCredentialRevisions.set(e.name,l),this.resolvedNativeMaterial.set(e.name,h.credential),this.scheduleCredentialRenewal(e.name,h.expiresAt),v}rememberDesired(e){this.desiredRevision=e.revision,this.credentialServers.clear();for(const t of e.servers)t.credentialRef&&this.credentialServers.set(t.name,t.credentialRef)}async recordDesiredStateCommit(e,t,i){if(!i)return;if(!this.releases)throw new Error("MCP desired state requires the shared command journal release authority");const r=e.servers.find(e=>e.name===t);await this.releases.recordMcpDesiredStateCommit({schemaVersion:1,...i,desiredStateRevision:e.revision,desiredStateHash:e.sha256,server:r?{name:r.name,definitionRef:r.definitionRef,version:r.version,definitionSha256:r.sha256,...r.credentialRef?{credentialRef:r.credentialRef}:{}}:{name:t,removed:!0}})}async findAttemptActive(e,t,i){const r=(await(this.releases?.listMcpActiveDesiredStates())??[]).find(e=>e.operationId===i.operationId&&e.method===i.method&&e.source===i.source);if(r&&releaseMatchesAttempt(r,e,t,i))return r}async collectLocalGarbage(e,t=!1){if(!this.references)return;if(!e)return void await this.store.transaction(async e=>this.collectLocalGarbage(e,t));const i=await this.references.listOwned(),r=t?await(this.references.listPendingOwned?.())??[]:[];for(const t of[...new Set([...i,...r])]){const i=(await e.load()).servers.some(e=>e.credentialRef===t);await e.assertOwned(),i?r.includes(t)&&await this.references.commit(t).catch(()=>{}):await this.references.remove(t).catch(()=>{})}}async exclusive(e){const t=this.tail.catch(()=>{}).then(e);return this.tail=t.catch(()=>{}),await t}}function pending(e){return Object.freeze({revision:e,state:"pending"})}function synced(e){return Object.freeze({revision:e,state:"synced"})}function blocked(e){return Object.freeze({revision:e,state:"blocked"})}function matchesProjectionAttempt(e,t,i){return i.expected.name===e&&(i.expected.absent?void 0===t:void 0!==t&&t.name===i.expected.name&&t.definitionRef===i.expected.definitionRef&&t.version===i.expected.version&&t.sha256===i.expected.sha256&&t.credentialRef===i.expected.credentialRef)}function withOperationReceipt(e,t,i,r,s){if(!r||!s||"synced"!==e.state)return e;const a=Object.freeze({schemaVersion:1,operationId:r.operationId,method:r.method,runtimeEpoch:r.runtimeEpoch,desiredStateRevision:t.revision,desiredStateHash:t.sha256,server:Object.freeze(i?{name:i.name,definitionRef:i.definitionRef,definitionSha256:i.sha256}:{name:r.expected.name,removed:!0})});return Object.freeze({...e,mcpReceipt:a})}function releaseMatchesDesired(e,t,i){return"mcp.set"===e.method&&e.desiredStateRevision===t.revision&&e.desiredStateHash===t.sha256&&e.server.name===i.name&&e.server.definitionRef===i.definitionRef&&e.server.version===i.version&&e.server.definitionSha256===i.sha256&&e.server.credentialRef===i.credentialRef}function releaseMatchesJournalRecord(e,t){const i=t.mcpDesiredStateCommit,r=t.mcpLiveRelease;return t.operationId===e.operationId&&t.command===e.method&&t.paramsHash===e.paramsHash&&t.runtimeEpoch===e.runtimeEpoch&&("effect_committed"===t.phase||"projection_committed"===t.phase)&&void 0!==t.messageBinding&&void 0!==i&&void 0!==r&&"service"===i.source&&sameMcpRelease(r,e)&&r.effectRecordHash===e.effectRecordHash&&sameMcpRelease(i,e)}function serviceHistoryMatchesRelease(e,t,i){const r=t.messageBinding;if(!r||!e.effectRecordHash)return!1;const s=i.request,a=s.payload,n=i.effectCommitted,o=n?.payload;if(i.operationId!==e.operationId||i.requestMessageId!==r.requestMessageId||i.requestHash!==r.requestHash||s.type!==RUNTIME_MESSAGE_TYPES.request||s.channel_id!==e.operationId||s.message_id!==r.requestMessageId||a.operationId!==e.operationId||a.requestMessageId!==r.requestMessageId||a.requestHash!==r.requestHash||a.paramsHash!==e.paramsHash||("mcp.set"===e.method?"mcp/set"!==a.method:"mcp/unset"!==a.method)||a.command.name!==e.method||a.dispatchEpoch!==e.runtimeEpoch||!n||!o||n.type!==RUNTIME_MESSAGE_TYPES.effectCommitted||n.channel_id!==e.operationId||n.in_reply_to!==r.requestMessageId||n.publisher_id!==a.instanceId||o.operationId!==e.operationId||o.requestMessageId!==r.requestMessageId||o.requestHash!==r.requestHash||o.dispatchEpoch!==e.runtimeEpoch||o.method!==a.method||o.effectRecordHash!==e.effectRecordHash||o.effectRecord.operationId!==e.operationId||o.effectRecord.method!==a.method)return!1;if(i.cancelRequested)return!1;if(!i.terminal)return!1;const c=i.final;return"final_message"===i.terminalSource&&"succeeded"===i.status&&void 0!==c&&c.type===RUNTIME_MESSAGE_TYPES.final&&c.channel_id===e.operationId&&c.in_reply_to===r.requestMessageId&&c.publisher_id===a.instanceId&&c.payload.operationId===e.operationId&&c.payload.requestMessageId===r.requestMessageId&&c.payload.requestHash===r.requestHash&&c.payload.dispatchEpoch===e.runtimeEpoch&&c.payload.method===a.method&&"succeeded"===c.payload.outcome&&c.payload.effectRecordHash===e.effectRecordHash}function sameMcpRelease(e,t){return e.source===t.source&&e.operationId===t.operationId&&e.method===t.method&&e.paramsHash===t.paramsHash&&e.runtimeEpoch===t.runtimeEpoch&&e.desiredStateRevision===t.desiredStateRevision&&e.desiredStateHash===t.desiredStateHash&&e.server.name===t.server.name&&e.server.definitionRef===t.server.definitionRef&&e.server.version===t.server.version&&e.server.definitionSha256===t.server.definitionSha256&&e.server.credentialRef===t.server.credentialRef&&e.server.removed===t.server.removed}function releaseMatchesAttempt(e,t,i,r){return!(e.operationId!==r.operationId||e.method!==r.method||e.source!==r.source||e.paramsHash!==r.paramsHash||e.runtimeEpoch!==r.runtimeEpoch||e.desiredStateRevision!==t.revision||e.desiredStateHash!==t.sha256||"service"===r.source&&!e.effectRecordHash||"acp_local"===r.source&&void 0!==e.effectRecordHash)&&(r.expected.absent?void 0===i&&e.server.name===r.expected.name&&!0===e.server.removed:void 0!==i&&releaseMatchesDesired(e,t,i)&&i.definitionRef===r.expected.definitionRef&&i.version===r.expected.version&&i.sha256===r.expected.sha256&&i.credentialRef===r.expected.credentialRef)}function releaseAuthorizesServer(e,t,i,r,s){return"mcp.set"===e.method&&(!t.credentialRef||e.runtimeEpoch===r)&&releaseMatchesDesired(e,i,t)&&(!s||releaseMatchesAttempt(e,i,t,s))}function releaseKey(e){return[e.source,e.operationId,e.paramsHash,e.runtimeEpoch,e.desiredStateRevision,e.desiredStateHash,e.effectRecordHash??"local"].join("\0")}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{listMcpServers,setMcpServer,unsetMcpServer}from"../openclaw-cli.js";export const MCP_NATIVE_MANAGED_KEY="managedBy";export const MCP_NATIVE_MANAGED_VALUE="beeos-claw";const DEFAULT_DEBOUNCE_MS=500;export class McpNativeConfigProjector{logger;cli;debounceMs;timer;pendingCompute;tail=Promise.resolve();disposed=!1;constructor(e={}){this.logger=e.logger,this.cli={list:e.cli?.list??listMcpServers,set:e.cli?.set??setMcpServer,unset:e.cli?.unset??unsetMcpServer},this.debounceMs=e.debounceMs??500}scheduleConverge(e){this.disposed||(this.pendingCompute=e,this.timer&&clearTimeout(this.timer),this.timer=setTimeout(()=>{this.timer=void 0,this.runPending()},this.debounceMs),this.timer.unref?.())}async flush(){this.timer&&(clearTimeout(this.timer),this.timer=void 0),this.runPending(),await this.tail.catch(()=>{})}dispose(){this.disposed=!0,this.timer&&(clearTimeout(this.timer),this.timer=void 0),this.pendingCompute=void 0}runPending(){const e=this.pendingCompute;this.pendingCompute=void 0,e&&(this.tail=this.tail.catch(()=>{}).then(async()=>{try{await this.converge(await e())}catch(e){this.logger?.warn?.(`[mcp-native] target computation failed: ${e instanceof Error?e.message:e}`)}}))}async converge(e){const t={applied:[],removed:[],failed:[]};let i;try{i=await this.cli.list()}catch(e){const i=e instanceof Error?e.message:String(e);return this.logger?.warn?.(`[mcp-native] list failed, converge skipped: ${i}`),t.failed.push(`list: ${i}`),t}for(const[n,s]of e){const e=renderNativeMcpServer(s),r=i[n];if(!r||canonical(r)!==canonical(e))if(!r||isManagedEntry(r))try{await this.cli.set(n,e),t.applied.push(n)}catch(e){const i=e instanceof Error?e.message:String(e);this.logger?.warn?.(`[mcp-native] set failed name=${n}: ${i}`),t.failed.push(`${n}: ${i}`)}else this.logger?.warn?.(`[mcp-native] skipping "${n}": unmanaged entry with same name exists`),t.failed.push(`${n}: unmanaged entry with same name exists`)}for(const[n,s]of Object.entries(i))if(!e.has(n)&&isManagedEntry(s))try{await this.cli.unset(n),t.removed.push(n)}catch(e){const i=e instanceof Error?e.message:String(e);this.logger?.warn?.(`[mcp-native] unset failed name=${n}: ${i}`),t.failed.push(`${n}: ${i}`)}return(t.applied.length||t.removed.length||t.failed.length)&&this.logger?.info?.(`[mcp-native] converge applied=[${t.applied.join(",")}] removed=[${t.removed.join(",")}] failed=${t.failed.length}`),t}}function isManagedEntry(e){return"beeos-claw"===e.managedBy}export function renderNativeMcpServer(e){const{config:t,credential:i}=e,n=i?.headers&&Object.keys(i.headers).length?{...i.headers}:void 0,s=i?.env&&Object.keys(i.env).length?{...i.env}:void 0;return{..."stdio"!==t.transport?{transport:t.transport}:{},..."string"==typeof t.url?{url:t.url}:{},..."string"==typeof t.command?{command:t.command}:{},...Array.isArray(t.args)?{args:[...t.args]}:{},..."string"==typeof t.cwd?{cwd:t.cwd}:{},..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{},...n?{headers:n}:{},...s?{env:s}:{},[MCP_NATIVE_MANAGED_KEY]:"beeos-claw"}}function canonical(e){return null===e||"object"!=typeof e?JSON.stringify(e)??"undefined":Array.isArray(e)?`[${e.map(canonical).join(",")}]`:`{${Object.entries(e).filter(([,e])=>void 0!==e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${canonical(t)}`).join(",")}}`}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{listMcpServers,setMcpServer,unsetMcpServer,OpenClawCliError}from"../openclaw-cli.js";import{Errors}from"../errors/index.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";const RESTART_DEBOUNCE_MS=750;export class McpService{transport;logger;state;exit;restartTimer;list;set;unset;resolveDefinition;desiredState;desiredStateStarted=!1;constructor(e){this.transport=e.transport,this.logger=e.logger,this.state=e.state,this.exit=e.exit??(e=>process.exit(e)),this.list=e.list??listMcpServers,this.set=e.set??setMcpServer,this.unset=e.unset??unsetMcpServer,this.resolveDefinition=e.resolveDefinition,this.desiredState=e.desiredState}setRuntimeReceiptHistoryPort(e){this.desiredState?.setRuntimeReceiptHistoryPort(e)}async execute(e,t,r){const i=e;if("mcp.list"===i.name){if(this.desiredState){await this.ensureDesiredState();const e=await this.desiredState.list();if(e.servers.some(e=>void 0===e.config))throw new Error("Service MCP config is available only within its active operation");return{servers:e.servers.map(e=>({name:e.name,definitionRef:e.definitionRef,transport:e.config.transport,...e.config.url?{url:e.config.url}:{},...e.config.command?{command:e.config.command}:{},...e.config.args?{args:e.config.args}:{},...e.config.cwd?{cwd:e.config.cwd}:{},connected:e.connected,...e.config.connectionTimeoutMs?{connectionTimeoutMs:e.config.connectionTimeoutMs}:{}}))}}const e=await this.list();return{servers:Object.entries(e).map(([e,t])=>sanitizeMcpConfig(e,t))}}if(this.hasInFlightPrompts())throw new Error("PROMPT_IN_FLIGHT");if("service"===r.origin&&!this.desiredState)throw new Error("Service MCP mutations require the desired-state controller");if("mcp.set"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const t=r.resources.resolveMcpDefinition?.bind(r.resources)??("acp"===r.origin?this.resolveDefinition:void 0);if(!t)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await t(e.params.definition);return this.desiredState?(await this.ensureDesiredState(),await this.desiredState.set(desiredServer(e.params.name,e.params.definition,publicConfig(s)),commitBinding(e,r)),{applied:!0,restartRequired:!1}):(await this.set(e.params.name,s),{applied:!0,restartRequired:!0})}const s=i.params.name;return requireSecretFreeMcpServerName(s),this.desiredState?(await this.ensureDesiredState(),await this.desiredState.unset(s,commitBinding(i,r)),{applied:!0,restartRequired:!1}):(await this.unset(s),{applied:!0,restartRequired:!0})}afterCommit(e){this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name||this.scheduleRestart()}async projectCommitted(e,t,r){if(!this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name)return;await this.ensureDesiredState();const i=e.params?.name;if("string"!=typeof i||!i)throw new Error("committed MCP projection lacks a server name");const s=r?"mcp.set"===e.name?{operationId:e.context.operationId,method:"mcp.set",source:"service"===r.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,definitionRef:e.params.definition.definitionRef,version:e.params.definition.version,sha256:e.params.definition.sha256,...e.params.definition.credentialRef?{credentialRef:e.params.definition.credentialRef}:{}}}:{operationId:e.context.operationId,method:"mcp.unset",source:"service"===r.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,absent:!0}}:void 0;let n;if("service"===r?.origin&&"mcp.set"===e.name){const t=e.params.definition,i=r.resources.resolveMcpDefinition?.bind(r.resources);if(!i)throw new Error("operation-scoped MCP definition resolver is not configured");n={config:publicConfig(await i(t)),runtimeEpoch:r.runtimeEpoch,...r.resources.resolveMcpCredential?{credential:await r.resources.resolveMcpCredential(t)}:{}}}return n?await this.desiredState.project(i,r?.abortSignal,s,n):await this.desiredState.project(i,r?.abortSignal,s)}async removeCredentialIfUnreferenced(e){return!!this.desiredState&&(await this.desiredState.removeLocalCredentialIfUnreferenced(e),!0)}async handleList(e){try{if(this.desiredState){await this.ensureDesiredState();const t=await this.desiredState.list();return void this.transport.sendResult(e.id,{revision:t.revision,servers:t.servers})}const t=await this.list();this.transport.sendResult(e.id,{servers:t})}catch(t){this.logger.warn?.(`[mcp-service] list failed: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!0)}}async handleSet(e){const t=e.params,r=t?.name,i=t?.server;if(r&&i&&"object"==typeof i){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(r),this.desiredState){const t=asDesiredServer(r,i);return await this.ensureDesiredState(),await this.desiredState.set(t),await this.desiredState.project(r),this.logger.info?.(`[mcp-service] desired state applied name=${r}`),void this.transport.sendResult(e.id,{ok:!0,restarting:!1})}await this.set(r,i),this.logger.info?.(`[mcp-service] set applied name=${r}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(t){this.logger.error?.(`[mcp-service] set failed name=${r}: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name' or 'server' config"})}async handleUnset(e){const t=e.params,r=t?.name;if(r){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(r),this.desiredState)return await this.ensureDesiredState(),await this.desiredState.unset(r),await this.desiredState.project(r),void this.transport.sendResult(e.id,{ok:!0,restarting:!1});await this.unset(r),this.logger.info?.(`[mcp-service] unset applied name=${r}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(t){this.logger.error?.(`[mcp-service] unset failed name=${r}: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name'"})}hasInFlightPrompts(){for(const e of this.state.sessions.values())if(void 0!==e.pendingPromptId||void 0!==e.currentRunId)return!0;return!1}rejectIfInFlight(e){return!!this.hasInFlightPrompts()&&(this.transport.sendError(e.id,Errors.InvalidParams.rpcCode,"Agent is busy",{code:Errors.InvalidParams.code,reason:"PROMPT_IN_FLIGHT",message:"Agent is processing a message; please retry after it completes."}),!0)}scheduleRestart(){this.restartTimer&&clearTimeout(this.restartTimer),this.restartTimer=setTimeout(()=>{this.logger.info?.("[mcp-service] mcp change applied — exiting for container restart"),this.exit(0)},750)}async ensureDesiredState(){if(!this.desiredStateStarted){if(!this.desiredState)throw new Error("MCP desired-state controller is unavailable");await this.desiredState.start(),this.desiredStateStarted=!0}}async start(){this.desiredState&&(this.desiredStateStarted?await this.desiredState.reconnect():await this.ensureDesiredState())}async stop(){this.desiredState&&this.desiredStateStarted&&(this.desiredStateStarted=!1,await this.desiredState.stop())}sendMappedError(e,t,r){const i=cliErrToAcpError(t,r);this.transport.sendError(e.id,i.code,i.message,i.data)}}function desiredServer(e,t,r){return{name:e,definitionRef:t.definitionRef,version:t.version,sha256:t.sha256,...t.credentialRef?{credentialRef:t.credentialRef}:{},config:r}}function commitBinding(e,t){return{source:"service"===t.origin?"service":"acp_local",operationId:e.context.operationId,method:e.name,paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch}}function publicConfig(e){if(void 0!==e.headers||void 0!==e.env)throw new Error("resolved MCP secrets must not enter desired state");return{transport:e.transport??(e.command?"stdio":"streamable-http"),..."string"==typeof e.url?{url:e.url}:{},..."string"==typeof e.command?{command:e.command}:{},...Array.isArray(e.args)?{args:e.args}:{},..."string"==typeof e.cwd?{cwd:e.cwd}:{},..."number"==typeof e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}}}function asDesiredServer(e,t){const r=t;if("string"!=typeof r.definitionRef||"string"!=typeof r.version||"string"!=typeof r.sha256)throw new Error("ACP mcp/set requires a verified definitionRef, version and sha256");if(void 0!==r.headers||void 0!==r.env)throw new Error("ACP mcp/set accepts credentialRef, not resolved headers or env");return desiredServer(e,{definitionRef:r.definitionRef,version:r.version,sha256:r.sha256,..."string"==typeof r.credentialRef?{credentialRef:r.credentialRef}:{},..."number"==typeof r.connectionTimeoutMs?{connectionTimeoutMs:r.connectionTimeoutMs}:{}},publicConfig(t))}function sanitizeMcpConfig(e,t){const r=t.transport??(t.command?"stdio":"streamable-http");return{name:e,definitionRef:`runtime-mcp:${encodeURIComponent(e)}`,transport:r,..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{},..."string"==typeof t.url?{url:t.url}:{},..."string"==typeof t.command?{command:t.command}:{},...Array.isArray(t.args)?{args:t.args}:{},..."string"==typeof t.cwd?{cwd:t.cwd}:{}}}export function cliErrToAcpError(e,t){if(e instanceof OpenClawCliError){const r=e.stderr??"";return null===e.exitCode||t?{code:Errors.InternalError.rpcCode,message:e.message,data:{code:Errors.InternalError.code}}:/No MCP server named/i.test(r)?{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code,reason:"NOT_FOUND"}}:{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code}}}return{code:Errors.InternalError.rpcCode,message:e instanceof Error?e.message:String(e),data:{code:Errors.InternalError.code}}}
|
|
1
|
+
import{listMcpServers,setMcpServer,unsetMcpServer,OpenClawCliError}from"../openclaw-cli.js";import{Errors}from"../errors/index.js";import{requireSecretFreeMcpServerName}from"./mcp-public-config-security.js";const RESTART_DEBOUNCE_MS=750;export class McpService{transport;logger;state;exit;restartTimer;list;set;unset;resolveDefinition;desiredState;desiredStateStarted=!1;constructor(e){this.transport=e.transport,this.logger=e.logger,this.state=e.state,this.exit=e.exit??(e=>process.exit(e)),this.list=e.list??listMcpServers,this.set=e.set??setMcpServer,this.unset=e.unset??unsetMcpServer,this.resolveDefinition=e.resolveDefinition,this.desiredState=e.desiredState}setRuntimeReceiptHistoryPort(e){this.desiredState?.setRuntimeReceiptHistoryPort(e)}async execute(e,t,r){const i=e;if("mcp.list"===i.name){if(this.desiredState){await this.ensureDesiredState();const e=await this.desiredState.list();if(e.servers.some(e=>void 0===e.config))throw new Error("Service MCP config is available only within its active operation");return{servers:e.servers.map(e=>({name:e.name,definitionRef:e.definitionRef,transport:e.config.transport,...e.config.url?{url:e.config.url}:{},...e.config.command?{command:e.config.command}:{},...e.config.args?{args:e.config.args}:{},...e.config.cwd?{cwd:e.config.cwd}:{},connected:e.connected,...e.config.connectionTimeoutMs?{connectionTimeoutMs:e.config.connectionTimeoutMs}:{}}))}}const e=await this.list();return{servers:Object.entries(e).map(([e,t])=>sanitizeMcpConfig(e,t))}}if(this.hasInFlightPrompts())throw new Error("PROMPT_IN_FLIGHT");if("service"===r.origin&&!this.desiredState)throw new Error("Service MCP mutations require the desired-state controller");if("mcp.set"===i.name){const e=i;requireSecretFreeMcpServerName(e.params.name);const t=r.resources.resolveMcpDefinition?.bind(r.resources)??("acp"===r.origin?this.resolveDefinition:void 0);if(!t)throw new Error("operation-scoped MCP definition resolver is not configured");const s=await t(e.params.definition);return this.desiredState?(await this.ensureDesiredState(),await this.desiredState.set(desiredServer(e.params.name,e.params.definition,publicConfig(s)),commitBinding(e,r)),{applied:!0,restartRequired:!1}):(await this.set(e.params.name,s),{applied:!0,restartRequired:!0})}const s=i.params.name;return requireSecretFreeMcpServerName(s),this.desiredState?(await this.ensureDesiredState(),await this.desiredState.unset(s,commitBinding(i,r)),{applied:!0,restartRequired:!1}):(await this.unset(s),{applied:!0,restartRequired:!0})}afterCommit(e){this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name||this.scheduleRestart()}async projectCommitted(e,t,r){if(!this.desiredState||"mcp.set"!==e.name&&"mcp.unset"!==e.name)return;await this.ensureDesiredState();const i=e.params?.name;if("string"!=typeof i||!i)throw new Error("committed MCP projection lacks a server name");const s=r?"mcp.set"===e.name?{operationId:e.context.operationId,method:"mcp.set",source:"service"===r.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,definitionRef:e.params.definition.definitionRef,version:e.params.definition.version,sha256:e.params.definition.sha256,...e.params.definition.credentialRef?{credentialRef:e.params.definition.credentialRef}:{}}}:{operationId:e.context.operationId,method:"mcp.unset",source:"service"===r.origin?"service":"acp_local",paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch,expected:{name:i,absent:!0}}:void 0;let n;if("service"===r?.origin&&"mcp.set"===e.name){const t=e.params.definition,i=r.resources.resolveMcpDefinition?.bind(r.resources);if(!i)throw new Error("operation-scoped MCP definition resolver is not configured");n={config:publicConfig(await i(t)),runtimeEpoch:r.runtimeEpoch,...r.resources.resolveMcpCredential?{credential:await r.resources.resolveMcpCredential(t)}:{}}}return n?await this.desiredState.project(i,r?.abortSignal,s,n):await this.desiredState.project(i,r?.abortSignal,s)}async removeCredentialIfUnreferenced(e){return!!this.desiredState&&(await this.desiredState.removeLocalCredentialIfUnreferenced(e),!0)}async handleList(e){try{if(this.desiredState){await this.ensureDesiredState();const t=await this.desiredState.list();return void this.transport.sendResult(e.id,{revision:t.revision,servers:t.servers})}const t=await this.list();this.transport.sendResult(e.id,{servers:t})}catch(t){this.logger.warn?.(`[mcp-service] list failed: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!0)}}async handleSet(e){const t=e.params,r=t?.name,i=t?.server;if(r&&i&&"object"==typeof i){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(r),this.desiredState){const t=asDesiredServer(r,i);return await this.ensureDesiredState(),await this.desiredState.set(t),await this.desiredState.project(r),this.logger.info?.(`[mcp-service] desired state applied name=${r}`),void this.transport.sendResult(e.id,{ok:!0,restarting:!1})}await this.set(r,i),this.logger.info?.(`[mcp-service] set applied name=${r}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(t){this.logger.error?.(`[mcp-service] set failed name=${r}: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name' or 'server' config"})}async handleUnset(e){const t=e.params,r=t?.name;if(r){if(!this.rejectIfInFlight(e))try{if(requireSecretFreeMcpServerName(r),this.desiredState)return await this.ensureDesiredState(),await this.desiredState.unset(r),await this.desiredState.project(r),void this.transport.sendResult(e.id,{ok:!0,restarting:!1});await this.unset(r),this.logger.info?.(`[mcp-service] unset applied name=${r}`),this.transport.sendResult(e.id,{ok:!0,restarting:!0}),this.scheduleRestart()}catch(t){this.logger.error?.(`[mcp-service] unset failed name=${r}: ${t instanceof Error?t.message:t}`),this.sendMappedError(e,t,!1)}}else this.transport.sendAcpError(e.id,Errors.InvalidParams,{message:"Missing 'name'"})}hasInFlightPrompts(){for(const e of this.state.sessions.values())if(void 0!==e.pendingPromptId||void 0!==e.currentRunId)return!0;return!1}rejectIfInFlight(e){return!!this.hasInFlightPrompts()&&(this.transport.sendError(e.id,Errors.InvalidParams.rpcCode,"Agent is busy",{code:Errors.InvalidParams.code,reason:"PROMPT_IN_FLIGHT",message:"Agent is processing a message; please retry after it completes."}),!0)}scheduleRestart(){this.restartTimer&&clearTimeout(this.restartTimer),this.restartTimer=setTimeout(()=>{this.logger.info?.("[mcp-service] mcp change applied — exiting for container restart"),this.exit(0)},750)}async ensureDesiredState(){if(!this.desiredStateStarted){if(!this.desiredState)throw new Error("MCP desired-state controller is unavailable");await this.desiredState.start(),this.desiredStateStarted=!0}}async start(){this.desiredState&&(this.desiredStateStarted?await this.desiredState.reconnect():await this.ensureDesiredState())}nudgeNativeProjection(){this.desiredState?.nudgeNativeProjection()}async stop(){this.desiredState&&this.desiredStateStarted&&(this.desiredStateStarted=!1,await this.desiredState.stop())}sendMappedError(e,t,r){const i=cliErrToAcpError(t,r);this.transport.sendError(e.id,i.code,i.message,i.data)}}function desiredServer(e,t,r){return{name:e,definitionRef:t.definitionRef,version:t.version,sha256:t.sha256,...t.credentialRef?{credentialRef:t.credentialRef}:{},config:r}}function commitBinding(e,t){return{source:"service"===t.origin?"service":"acp_local",operationId:e.context.operationId,method:e.name,paramsHash:e.context.paramsHash,runtimeEpoch:e.context.dispatchEpoch}}function publicConfig(e){if(void 0!==e.headers||void 0!==e.env)throw new Error("resolved MCP secrets must not enter desired state");return{transport:e.transport??(e.command?"stdio":"streamable-http"),..."string"==typeof e.url?{url:e.url}:{},..."string"==typeof e.command?{command:e.command}:{},...Array.isArray(e.args)?{args:e.args}:{},..."string"==typeof e.cwd?{cwd:e.cwd}:{},..."number"==typeof e.connectionTimeoutMs?{connectionTimeoutMs:e.connectionTimeoutMs}:{}}}function asDesiredServer(e,t){const r=t;if("string"!=typeof r.definitionRef||"string"!=typeof r.version||"string"!=typeof r.sha256)throw new Error("ACP mcp/set requires a verified definitionRef, version and sha256");if(void 0!==r.headers||void 0!==r.env)throw new Error("ACP mcp/set accepts credentialRef, not resolved headers or env");return desiredServer(e,{definitionRef:r.definitionRef,version:r.version,sha256:r.sha256,..."string"==typeof r.credentialRef?{credentialRef:r.credentialRef}:{},..."number"==typeof r.connectionTimeoutMs?{connectionTimeoutMs:r.connectionTimeoutMs}:{}},publicConfig(t))}function sanitizeMcpConfig(e,t){const r=t.transport??(t.command?"stdio":"streamable-http");return{name:e,definitionRef:`runtime-mcp:${encodeURIComponent(e)}`,transport:r,..."number"==typeof t.connectionTimeoutMs?{connectionTimeoutMs:t.connectionTimeoutMs}:{},..."string"==typeof t.url?{url:t.url}:{},..."string"==typeof t.command?{command:t.command}:{},...Array.isArray(t.args)?{args:t.args}:{},..."string"==typeof t.cwd?{cwd:t.cwd}:{}}}export function cliErrToAcpError(e,t){if(e instanceof OpenClawCliError){const r=e.stderr??"";return null===e.exitCode||t?{code:Errors.InternalError.rpcCode,message:e.message,data:{code:Errors.InternalError.code}}:/No MCP server named/i.test(r)?{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code,reason:"NOT_FOUND"}}:{code:Errors.InvalidParams.rpcCode,message:e.message,data:{code:Errors.InvalidParams.code}}}return{code:Errors.InternalError.rpcCode,message:e instanceof Error?e.message:String(e),data:{code:Errors.InternalError.code}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{MessageQueue}from"../utils/mq.js";import{isExternalInvocationSessionKey}from"./external-protocol-tags.js";const MAX_TOOL_CALL_MAP_ENTRIES=5e3,MAX_COMPLETED_RUN_IDS=200;export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;promptsByGatewayRequestId=new Map;sessionsByGatewayKey=new Map;deletedGatewaySessionKeys=new Set;toolCallIdMap=new Map;toolCallToSessionMap=new Map;runIdToCronJobId=new Map;nextToolCallSeq=0;completedRunIds=new Set;lastActiveSessionId;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;EXTERNAL_SESSION_IDLE_MS=3e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(e){this.logger=e}findSessionByKey(e){return this.sessionsByGatewayKey.get(e)}resolveSessionFromParams(e){const s="string"==typeof e?.sessionId?e.sessionId:void 0;if(s)return this.sessions.get(s)}isCurrentStreamOwner(e,s){return!s||!!e.currentRunId&&s===e.currentRunId}markRunCompleted(e){if(this.completedRunIds.add(e),this.completedRunIds.size>200){const e=this.completedRunIds.values().next().value;e&&this.completedRunIds.delete(e)}}isRunCompleted(e){return this.completedRunIds.has(e)}setRunCronJobId(e,s){if(this.runIdToCronJobId.set(e,s),this.runIdToCronJobId.size>400){const e=this.runIdToCronJobId.keys().next().value;void 0!==e&&this.runIdToCronJobId.delete(e)}}clearPromptTimeout(e){e.promptTimeoutTimer&&(clearTimeout(e.promptTimeoutTimer),e.promptTimeoutTimer=void 0)}clearStreamIdleTimer(e){e.streamIdleTimer&&(clearTimeout(e.streamIdleTimer),e.streamIdleTimer=void 0)}resetForNewRun(e,s,t){this.clearStreamIdleTimer(e),e.currentRunId&&(this.promptsByRunId.delete(e.currentRunId),this.runIdToCronJobId.delete(e.currentRunId)),e.gatewayRequestId&&this.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantMq&&!e.assistantMq.closed&&(this.logger.warn?.(`[session-state] resetForNewRun discarding stale assistantMq sessionId=${s}`),e.assistantMq.close()),e.assistantMq=new MessageQueue,e.pendingUserTurnUpdates=[],e.chatReplayDedupKeys.clear(),e.hasAgentAssistantStream=!1,e.hasAgentThinkingStream=!1,e.hasAgentToolStartStream=!1,e.hasAgentToolResultStream=!1,e.chatAssistantTextSoFar=void 0,e.agentAssistantTextSoFar=void 0,e.cronJobId=void 0,e.presentedFileArtifactKeys=new Set,e.emittedToolResultKeys=new Set,e.backgroundRunAdopted=!1,e.backgroundRunArtifacts=void 0,e.backgroundRunCaption=void 0,e.backgroundRunEgressSettling=!1,e.promptDone=!1,t.resetSessionUpdateCount(s),this.enforceToolCallMapLimits()}enforceToolCallMapLimits(){if(this.toolCallIdMap.size>5e3){const e=this.toolCallIdMap.size-5e3;let s=0;for(const t of this.toolCallIdMap.keys()){if(s>=e)break;this.toolCallToSessionMap.delete(this.toolCallIdMap.get(t)),this.toolCallIdMap.delete(t),s++}}if(this.toolCallToSessionMap.size>5e3){const e=this.toolCallToSessionMap.size-5e3;let s=0;for(const t of this.toolCallToSessionMap.keys()){if(s>=e)break;this.toolCallToSessionMap.delete(t),s++}}}extractAgentAssistantDelta(e,s){const t=(e.chatAssistantTextSoFar??"").length;if(s.delta){const o=e.agentAssistantTextSoFar??"",i=o+s.delta;if(e.agentAssistantTextSoFar=i,t>0&&o.length<t){if(i.length<=t)return;return i.slice(t)}return s.delta}const o=s.text;if(!o)return;const i=e.agentAssistantTextSoFar??"";e.agentAssistantTextSoFar=o;const n=Math.max(i.length,t);if(!(o.length<=n)){if(o.startsWith(i)||0===n)return o.slice(n)||void 0;if(!i.startsWith(o))return o}}startSessionGc(e,s){this.sessionGcTimer=setInterval(()=>this.gcSessions(e,s),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(e,s){const t=Date.now();let o=0;for(const[i,n]of this.sessions){if(void 0!==n.pendingPromptId)continue;(n.lastStreamActivityTs??n.createdAt)<t-(isExternalInvocationSessionKey(n.gatewaySessionKey)?this.EXTERNAL_SESSION_IDLE_MS:this.MAX_IDLE_SESSION_MS)&&(e(n),s?.(n),this.sessions.delete(i),this.sessionsByGatewayKey.delete(n.gatewaySessionKey),o++)}const i=new Set(Array.from(this.sessions.values()).map(e=>e.sessionId));for(const[e,s]of this.toolCallToSessionMap)i.has(s)||this.toolCallToSessionMap.delete(e);for(const[e,s]of this.toolCallIdMap)this.toolCallToSessionMap.has(s)||this.toolCallIdMap.delete(e);o>0&&(this.lastActiveSessionId&&!this.sessions.has(this.lastActiveSessionId)&&(this.lastActiveSessionId=void 0),this.logger.debug?.(`[session-state] GC evicted ${o} idle sessions, remaining=${this.sessions.size}`))}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const e of this.sessions.values())this.clearPromptTimeout(e),this.clearStreamIdleTimer(e),e.assistantMq?.close();this.promptsByRunId.clear(),this.promptsByGatewayRequestId.clear(),this.runIdToCronJobId.clear(),this.completedRunIds.clear(),this.toolCallIdMap.clear(),this.toolCallToSessionMap.clear(),this.sessionsByGatewayKey.clear(),this.deletedGatewaySessionKeys.clear(),this.sessions.clear(),this.lastActiveSessionId=void 0}}
|
|
1
|
+
import{MessageQueue}from"../utils/mq.js";import{isExternalInvocationSessionKey}from"./external-protocol-tags.js";import{clearBackgroundRunUploadBarriers}from"./background-run-im-egress.js";const MAX_TOOL_CALL_MAP_ENTRIES=5e3,MAX_COMPLETED_RUN_IDS=200;export class AcpGatewaySessionState{sessions=new Map;promptsByRunId=new Map;promptsByGatewayRequestId=new Map;sessionsByGatewayKey=new Map;deletedGatewaySessionKeys=new Set;toolCallIdMap=new Map;toolCallToSessionMap=new Map;runIdToCronJobId=new Map;nextToolCallSeq=0;completedRunIds=new Set;lastActiveSessionId;sessionGcTimer=null;MAX_IDLE_SESSION_MS=864e5;EXTERNAL_SESSION_IDLE_MS=3e5;SESSION_GC_INTERVAL_MS=3e5;logger;constructor(e){this.logger=e}findSessionByKey(e){return this.sessionsByGatewayKey.get(e)}resolveSessionFromParams(e){const s="string"==typeof e?.sessionId?e.sessionId:void 0;if(s)return this.sessions.get(s)}isCurrentStreamOwner(e,s){return!s||!!e.currentRunId&&s===e.currentRunId}markRunCompleted(e){if(this.completedRunIds.add(e),this.completedRunIds.size>200){const e=this.completedRunIds.values().next().value;e&&this.completedRunIds.delete(e)}}isRunCompleted(e){return this.completedRunIds.has(e)}setRunCronJobId(e,s){if(this.runIdToCronJobId.set(e,s),this.runIdToCronJobId.size>400){const e=this.runIdToCronJobId.keys().next().value;void 0!==e&&this.runIdToCronJobId.delete(e)}}clearPromptTimeout(e){e.promptTimeoutTimer&&(clearTimeout(e.promptTimeoutTimer),e.promptTimeoutTimer=void 0)}clearStreamIdleTimer(e){e.streamIdleTimer&&(clearTimeout(e.streamIdleTimer),e.streamIdleTimer=void 0)}resetForNewRun(e,s,t){this.clearStreamIdleTimer(e),e.currentRunId&&(this.promptsByRunId.delete(e.currentRunId),this.runIdToCronJobId.delete(e.currentRunId)),e.gatewayRequestId&&this.promptsByGatewayRequestId.delete(e.gatewayRequestId),e.currentRunId=void 0,e.gatewayRequestId=void 0,e.assistantMq&&!e.assistantMq.closed&&(this.logger.warn?.(`[session-state] resetForNewRun discarding stale assistantMq sessionId=${s}`),e.assistantMq.close()),e.assistantMq=new MessageQueue,e.pendingUserTurnUpdates=[],e.chatReplayDedupKeys.clear(),e.hasAgentAssistantStream=!1,e.hasAgentThinkingStream=!1,e.hasAgentToolStartStream=!1,e.hasAgentToolResultStream=!1,e.chatAssistantTextSoFar=void 0,e.agentAssistantTextSoFar=void 0,e.cronJobId=void 0,e.presentedFileArtifactKeys=new Set,e.emittedToolResultKeys=new Set,e.backgroundRunAdopted=!1,e.backgroundRunArtifacts=void 0,e.backgroundRunCaption=void 0,e.backgroundRunEgressSettling=!1,clearBackgroundRunUploadBarriers(e),e.promptDone=!1,t.resetSessionUpdateCount(s),this.enforceToolCallMapLimits()}enforceToolCallMapLimits(){if(this.toolCallIdMap.size>5e3){const e=this.toolCallIdMap.size-5e3;let s=0;for(const t of this.toolCallIdMap.keys()){if(s>=e)break;this.toolCallToSessionMap.delete(this.toolCallIdMap.get(t)),this.toolCallIdMap.delete(t),s++}}if(this.toolCallToSessionMap.size>5e3){const e=this.toolCallToSessionMap.size-5e3;let s=0;for(const t of this.toolCallToSessionMap.keys()){if(s>=e)break;this.toolCallToSessionMap.delete(t),s++}}}extractAgentAssistantDelta(e,s){const t=(e.chatAssistantTextSoFar??"").length;if(s.delta){const o=e.agentAssistantTextSoFar??"",i=o+s.delta;if(e.agentAssistantTextSoFar=i,t>0&&o.length<t){if(i.length<=t)return;return i.slice(t)}return s.delta}const o=s.text;if(!o)return;const i=e.agentAssistantTextSoFar??"";e.agentAssistantTextSoFar=o;const n=Math.max(i.length,t);if(!(o.length<=n)){if(o.startsWith(i)||0===n)return o.slice(n)||void 0;if(!i.startsWith(o))return o}}startSessionGc(e,s){this.sessionGcTimer=setInterval(()=>this.gcSessions(e,s),this.SESSION_GC_INTERVAL_MS),"object"==typeof this.sessionGcTimer&&"unref"in this.sessionGcTimer&&this.sessionGcTimer.unref()}gcSessions(e,s){const t=Date.now();let o=0;for(const[i,n]of this.sessions){if(void 0!==n.pendingPromptId)continue;(n.lastStreamActivityTs??n.createdAt)<t-(isExternalInvocationSessionKey(n.gatewaySessionKey)?this.EXTERNAL_SESSION_IDLE_MS:this.MAX_IDLE_SESSION_MS)&&(e(n),s?.(n),this.sessions.delete(i),this.sessionsByGatewayKey.delete(n.gatewaySessionKey),o++)}const i=new Set(Array.from(this.sessions.values()).map(e=>e.sessionId));for(const[e,s]of this.toolCallToSessionMap)i.has(s)||this.toolCallToSessionMap.delete(e);for(const[e,s]of this.toolCallIdMap)this.toolCallToSessionMap.has(s)||this.toolCallIdMap.delete(e);o>0&&(this.lastActiveSessionId&&!this.sessions.has(this.lastActiveSessionId)&&(this.lastActiveSessionId=void 0),this.logger.debug?.(`[session-state] GC evicted ${o} idle sessions, remaining=${this.sessions.size}`))}destroy(){this.sessionGcTimer&&(clearInterval(this.sessionGcTimer),this.sessionGcTimer=null);for(const e of this.sessions.values())this.clearPromptTimeout(e),this.clearStreamIdleTimer(e),e.assistantMq?.close();this.promptsByRunId.clear(),this.promptsByGatewayRequestId.clear(),this.runIdToCronJobId.clear(),this.completedRunIds.clear(),this.toolCallIdMap.clear(),this.toolCallToSessionMap.clear(),this.sessionsByGatewayKey.clear(),this.deletedGatewaySessionKeys.clear(),this.sessions.clear(),this.lastActiveSessionId=void 0}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{captureBackgroundRunArtifacts}from"./background-run-im-egress.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";export async function emitNormalizedToolResultForTurn(t){const{session:e,envelope:o}=t;return emitToolResultProjectionForTurn({session:e,projection:{...(await normalizeToolResultForAcpUpdate({toolCallId:t.normalizeToolCallId??o.toolCallId,toolName:o.toolName,rawText:o.rawText,rawResult:o.rawResult})).projection,toolName:o.toolName,rawOutput:o.rawResult,...o.timestamp?{timestamp:o.timestamp}:{}},toolCallId:o.toolCallId,config:t.config,logger:t.logger,writeObs:t.writeObs,getCanvasState:t.getCanvasState,turnId:t.turnId,sendUpdate:t.sendUpdate})}export async function emitToolResultProjectionForTurn(t){const{session:e,projection:o,toolCallId:r}=t,a={emitted:!1,outputText:o.outputText,...o.resourceLinks?.length?{resourceLinks:o.resourceLinks}:{}};if(captureBackgroundRunArtifacts(e,o.resourceLinks),!o.outputText&&!o.resourceLinks?.length)return a;const
|
|
1
|
+
import{captureBackgroundRunArtifacts,completeBackgroundRunUploadBarrier}from"./background-run-im-egress.js";import{presentFileArtifacts}from"./file-artifact-presenter.js";import{buildToolCallUpdatePayload}from"./tool-call-payload-builder.js";import{normalizeToolResultForAcpUpdate}from"./tool-result-payload-strategies.js";export async function emitNormalizedToolResultForTurn(t){const{session:e,envelope:o}=t;return emitToolResultProjectionForTurn({session:e,projection:{...(await normalizeToolResultForAcpUpdate({toolCallId:t.normalizeToolCallId??o.toolCallId,toolName:o.toolName,rawText:o.rawText,rawResult:o.rawResult})).projection,toolName:o.toolName,rawOutput:o.rawResult,...o.timestamp?{timestamp:o.timestamp}:{}},toolCallId:o.toolCallId,config:t.config,logger:t.logger,writeObs:t.writeObs,getCanvasState:t.getCanvasState,turnId:t.turnId,sendUpdate:t.sendUpdate})}export async function emitToolResultProjectionForTurn(t){const{session:e,projection:o,toolCallId:r}=t,a={emitted:!1,outputText:o.outputText,...o.resourceLinks?.length?{resourceLinks:o.resourceLinks}:{}};if(captureBackgroundRunArtifacts(e,o.resourceLinks),completeBackgroundRunUploadBarrier(e,r,o.toolName),!o.outputText&&!o.resourceLinks?.length)return a;const l=`${r}:${o.projectionKey}`;if(e.emittedToolResultKeys??=new Set,e.emittedToolResultKeys.has(l))return a;const n=buildToolCallUpdatePayload({toolCallId:r,toolName:o.toolName,outputText:o.outputText,rawOutput:o.rawOutput,resourceLinks:o.resourceLinks,timestamp:o.timestamp});stampTurnId(n,t.turnId);return!1!==t.sendUpdate(n)?(e.emittedToolResultKeys.add(l),a.emitted=!0,t.writeObs({component:"acp-gateway",domain:"tool_result",name:"tool_result.normalized",severity:"info",payload:{toolCallId:r,toolName:o.toolName,hasText:Boolean(o.outputText),resourceLinkCount:o.resourceLinks?.length??0,projectionKey:o.projectionKey}}),o.resourceLinks?.length&&await presentFileArtifacts(e,r,o.resourceLinks,{config:t.config,logger:t.logger,writeObs:t.writeObs,getCanvasState:t.getCanvasState,sendUpdate:e=>{stampTurnId(e,t.turnId),t.sendUpdate(e)}}),a):a}function stampTurnId(t,e){e&&void 0===t._turnId&&(t._turnId=e)}
|
package/dist/acp-server.js
CHANGED
|
@@ -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";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: ${e instanceof Error?e.message:String(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).catch(e=>{this.logger.warn?.("[acp-server] projection agent mapping session hydration failed: "+(e instanceof Error?e.message:String(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,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),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,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration 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.core.beginGatewaySessionPreload(),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.core.failGatewaySessionPreload(),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]}}
|
|
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: ${e instanceof Error?e.message:String(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()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}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).catch(e=>{this.logger.warn?.("[acp-server] projection agent mapping session hydration failed: "+(e instanceof Error?e.message:String(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,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),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,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration 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.core.beginGatewaySessionPreload(),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.core.failGatewaySessionPreload(),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]}}
|