beeos-claw 0.6.63-cloud.5 → 0.6.63-cloud.6

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.
Files changed (45) hide show
  1. package/dist/a2a/tools.js +1 -1
  2. package/dist/acp-gateway/beeos-file-resolver.js +1 -1
  3. package/dist/acp-gateway/bridge-core.js +1 -1
  4. package/dist/acp-gateway/gateway-events.js +1 -1
  5. package/dist/acp-server.js +1 -1
  6. package/dist/agent-sync.js +1 -1
  7. package/dist/automation/tools.js +1 -0
  8. package/dist/automation/turn-policy.js +1 -0
  9. package/dist/automation/wake.js +1 -0
  10. package/dist/canvas/relay-client.js +1 -1
  11. package/dist/canvas/ws-provider.js +1 -1
  12. package/dist/files/fetch-tool.js +1 -1
  13. package/dist/files/list-tool.js +1 -1
  14. package/dist/im-inbound-handler.js +1 -1
  15. package/dist/im-runtime-dispatch.js +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/mcp-connector-proxy.js +1 -1
  18. package/dist/runtime/agent-projection-production.js +1 -1
  19. package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
  20. package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
  21. package/dist/runtime/ms-runtime-rpc-adapter.js +1 -1
  22. package/dist/runtime/native-cloud-chat-composition.js +1 -1
  23. package/dist/runtime/native-cloud-prompt-bridge.js +1 -1
  24. package/dist/runtime/runtime-operation-resource-resolver.js +1 -1
  25. package/dist/runtime/runtime-production-authority.js +1 -1
  26. package/dist/runtime/runtime-registration-identity-store.js +1 -1
  27. package/dist/runtime/runtime-service-production-candidate.js +1 -1
  28. package/dist/runtime/service-runtime-attempt-ports-resolver.js +1 -1
  29. package/dist/terminal/runtime-terminal-outbound-registration.js +1 -1
  30. package/dist/terminal/terminal-lease-bound-bridge-client.js +1 -1
  31. package/dist/upload/desktop-screenshot-tool.js +1 -1
  32. package/dist/upload/finalize-hook.js +1 -1
  33. package/dist/upload/tool-artifact-enrichment.js +1 -1
  34. package/dist/upload/tool.js +1 -1
  35. package/dist/upload/upload-file.js +1 -1
  36. package/openclaw.plugin.json +15 -1
  37. package/package.json +4 -4
  38. package/dist/agent-auth.js +0 -1
  39. package/dist/bridge-discovery.js +0 -1
  40. package/dist/runtime/agent-gateway-runtime-attempt-authorization.js +0 -1
  41. package/dist/runtime/message-service-runtime-invocation-authority.js +0 -1
  42. package/dist/runtime/runtime-jwks-verifier.js +0 -1
  43. package/dist/runtime/runtime-registration.js +0 -1
  44. package/dist/runtime/runtime-transport-failure-policy.js +0 -1
  45. package/dist/runtime/split-runtime-authorization-verifier.js +0 -1
@@ -1 +1 @@
1
- import{createHash}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{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.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";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;agentFetchFn;getCronConversationDelivery;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.agentFetchFn=t.agentFetch,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?this.runOutputRouter.terminal(s,e):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);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(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,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",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.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:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.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_${e.runId}_${e.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,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.emittedToolResultKeys=new Set;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),"started"===e.action&&this.config.agentGatewayUrl&&this.agentFetchFn&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.config.agentGatewayUrl||!this.agentFetchFn)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}const i=`${this.config.agentGatewayUrl}/api/v1/agent/events`;try{await this.agentFetchFn(i,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({type:"cron",data:o})})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}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=${e}`);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=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewSanitizedText(t.cumulative),s=void 0!==t.delta?previewSanitizedText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.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(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
1
+ import{createHash}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{toMessageServiceCompletedStopReason}from"../runtime/message-service-stop-reason.js";import{emitNormalizedToolResultForTurn}from"./tool-result-emitter.js";import{buildToolCallPayload}from"./tool-call-payload-builder.js";import{BEEOS_UPLOAD_TOOL_NAME}from"../upload/tool.js";import{resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.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";import{runOutputTerminalFromFailure}from"../runtime/run-output-router.js";import{fromAcpRunOutputUpdate}from"./acp-run-output-mapper.js";import{openRunOutputRoute}from"./run-output-route.js";export function lifecycleEndFailureMessage(t){if(!t)return;const e=!0===t.isError||!0===t.is_error,s=t.error,n=s&&"object"==typeof s?s:void 0,o="string"==typeof s||void 0!==n||"string"==typeof t.message||"string"==typeof t.reason;if(e||o){if(!e){const e="string"==typeof t.stopReason?t.stopReason:"";if("error"!==e&&"timeout"!==e&&"aborted"!==e)return}return("string"==typeof t.message?t.message:void 0)??("string"==typeof s?s:void 0)??("string"==typeof n?.message?n.message:void 0)??("string"==typeof t.reason?t.reason:void 0)??(n?JSON.stringify(n):void 0)??"gateway lifecycle error"}}function asString(t){if("string"==typeof t){return t.trim()||void 0}}function isRecord(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}function normalizeText(t){if("string"==typeof t)return normalizeTransportText(t.replace(/\r\n/g,"\n").replace(/\r/g,"\n"))}const DEDUP_HASH_THRESHOLD=256;function dedupKey(t,...e){const s=`${t}:${e.join(":")}`;if(s.length<=256)return s;return`${t}:h:${createHash("sha256").update(s).digest("hex").slice(0,24)}`}function toISO(t){if(!(null==t||t<=0))try{return new Date(t).toISOString()}catch{return}}function resolveToolArgs(t){const e=t.arguments??t.args??t.input;if(isRecord(e))return e;if("string"==typeof e){const t=e.trim();if(t.startsWith("{")||t.startsWith("["))try{return JSON.parse(t)}catch{}}return{}}export function resolveTurnId(t){if(t.currentRunId)return t.currentRunId;const e=t.pendingPromptId;return void 0!==e?"string"==typeof e?`req:${e}`:`req:${String(e)}`:void 0}export function roleFromSessionUpdate(t){const e=t.sessionUpdate;if(e)return"user_message_chunk"===e?"user":"system_message_chunk"===e?"system":e.startsWith("agent_")||e.startsWith("tool_")?"assistant":"history_turn"===e?"user"===t.role?"user":"system"===t.role?"system":"assistant":void 0}export function recordTurnIdForRole(t,e,s){if(!s)return;const n=roleFromSessionUpdate(e);n&&(t.lastTurnIdByRole||(t.lastTurnIdByRole={}),t.lastTurnIdByRole[n]=s)}export function recordToolCallTurnForUpdate(t,e,s){s&&"tool_call"===e.sessionUpdate&&e.toolCallId&&(t.toolCallTurns??=new Map,t.toolCallTurns.set(e.toolCallId,{turnId:s,..."string"==typeof e.name&&e.name?{toolName:e.name}:{}}))}export class AcpGatewayEvents{state;transport;config;logger;writeObs;runOutputRouter;adoptBackgroundMutation;abortGatewayRun;cloud;getCronConversationDelivery;gatewayRequest;getCanvasState;cronChannelCache;constructor(t){this.state=t.state,this.transport=t.transport,this.config=t.config,this.logger=t.logger,this.writeObs=t.writeObs,this.runOutputRouter=t.runOutputRouter,this.adoptBackgroundMutation=t.adoptBackgroundMutation,this.abortGatewayRun=t.abortGatewayRun,this.cloud=t.cloud,this.getCronConversationDelivery=t.getCronConversationDelivery,this.gatewayRequest=t.gatewayRequest,this.getCanvasState=t.getCanvasState,this.cronChannelCache=t.cronChannelCache}observeLifecycle(t,e,s){observeLifecyclePromise(t,this.logger,{component:"gateway-events",action:e,sessionId:s.sessionId,gatewaySessionKey:s.gatewaySessionKey})}touchStreamActivity(t){t.lastStreamActivityTs=Date.now(),this.resetStreamIdleTimer(t)}resetStreamIdleTimer(t){this.state.clearStreamIdleTimer(t);const e=this.config.streamIdleTimeoutMs??DEFAULT_STREAM_IDLE_TIMEOUT_MS,s=this.config.streamStaleTimeoutMs??DEFAULT_STREAM_STALE_TIMEOUT_MS;t.streamIdleTimer=setTimeout(()=>{if(!t.lastStreamActivityTs)return;const e=Date.now()-t.lastStreamActivityTs;e>s&&(this.logger.warn?.(`[gateway-events] Stream stale timeout sessionId=${t.sessionId} stale=${e}ms`),this.observeLifecycle(this.terminalRunOutput(t,this.failedRunOutput(classifyAgentRuntimeFailure({raw:`Stream stale timeout after ${e}ms`,source:"live_gateway",runtimeKind:"openclaw"}))),"stream_stale_timeout",t))},e),"object"==typeof t.streamIdleTimer&&"unref"in t.streamIdleTimer&&t.streamIdleTimer.unref()}sendTurnUpdate(t,e){const s=resolveTurnId(t);if(!s)return!1;s&&void 0===e._turnId&&(e._turnId=s),recordTurnIdForRole(t,e,s),recordToolCallTurnForUpdate(t,e,s);const n=fromAcpRunOutputUpdate(e);return!!n&&(this.runOutputRouter.project(s,n),!0)}trackRunOutputWork(t,e){const s=resolveTurnId(t);s&&this.runOutputRouter.track(s,e)}beginRunOutputUpload(t,e,s){if(s!==BEEOS_UPLOAD_TOOL_NAME)return;const n=resolveTurnId(t);n&&this.runOutputRouter.beginUpload(n,e)}terminalRunOutput(t,e){const s=resolveTurnId(t);return s?this.runOutputRouter.terminal(s,e):Promise.reject(new Error(`run output terminal has no run id: ${t.sessionId}`))}failedRunOutput(t){return runOutputTerminalFromFailure(t,Errors.AgentError.rpcCode)}sendToolCallSpacer(t,e){const s={sessionUpdate:"agent_message_chunk",content:{type:"text",text:"\n"},...e?{timestamp:e}:{}};this.sendTurnUpdate(t,s),t.assistantMq?.push(s)}resolveToolCallId(t,e,s){const n=asString(e.toolCallId)??asString(e.tool_call_id)??asString(e.callId)??asString(e.id);if(n){const e=this.state.toolCallIdMap.get(n);return e?(t.sessionId&&this.state.toolCallToSessionMap.set(e,t.sessionId),e):(this.state.toolCallIdMap.set(n,n),t.sessionId&&this.state.toolCallToSessionMap.set(n,t.sessionId),n)}const o=`tc_${t.currentRunId??"unknown"}_${this.state.nextToolCallSeq++}`;return t.sessionId&&this.state.toolCallToSessionMap.set(o,t.sessionId),o}handleChatEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??this.state.findSessionByKey(t.sessionKey);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}e.sessionId;const s=t.message;if(s){this.touchStreamActivity(e);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(e.hasAgentThinkingStream)continue;const t=normalizeText(o.thinking)??normalizeText(o.text);if(!t)continue;const s=dedupKey("chat:thinking",t);if(e.chatReplayDedupKeys.has(s))continue;e.chatReplayDedupKeys.add(s);const n={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:t},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,n)&&e.assistantMq?.push(n);continue}if("toolCall"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolStartStream)continue;const t=this.resolveToolCallId(e,o,"start"),s=asString(o.name)??asString(o.toolName)??asString(o.tool_name)??"tool",n=resolveToolArgs(o),a=dedupKey("chat:tool_start",t,JSON.stringify(n));if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=buildToolCallPayload({toolCallId:t,toolName:s,args:n,timestamp:r});this.beginRunOutputUpload(e,t,s),this.sendTurnUpdate(e,i)&&e.assistantMq?.push(i);continue}if("toolResult"===a){if(this.sendToolCallSpacer(e,r),e.hasAgentToolResultStream)continue;const t=this.resolveToolCallId(e,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",t,a);if(e.chatReplayDedupKeys.has(i))continue;e.chatReplayDedupKeys.add(i),this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:t,toolName:s,rawText:a,rawResult:n,timestamp:r}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}if("text"===a&&o.text){if(e.hasAgentAssistantStream||e.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:e.sessionId,runId:t.runId,cumulative:s});continue}const a=dedupKey("chat:text",s);if(e.chatReplayDedupKeys.has(a))continue;e.chatReplayDedupKeys.add(a);const i=e.chatAssistantTextSoFar??"";let l;if(e.chatAssistantTextSoFar=s,s.startsWith(i))l=s.slice(i.length);else{if(i.startsWith(s))continue;l=s}if(!l)continue;this.logger.debug?.(`[gateway-events] chat.text delta seq=${t.seq} runId=${t.runId} len=${l.length} prevLen=${i.length}`);const u={sessionUpdate:"agent_message_chunk",content:{type:"text",text:l},...r?{timestamp:r}:{}};this.sendTurnUpdate(e,u),e.assistantMq?.push(u)}}}}else"delta"===t.state&&this.touchStreamActivity(e);if(!e.promptDone)if("final"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t.stopReason)}),"chat_final",e);else if("aborted"===t.state||"cancelled"===t.state||"cancel"===t.state)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"chat_cancelled",e);else if("error"===t.state){const s=t.errorMessage||Errors.AgentError.message,n=classifyAgentRuntimeFailure({raw:s,source:"chat_state",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(n)),"interrupted"===n.kind?"chat_interrupt":"chat_failure",e)}}handleAgentEvent(t){const e=(t.runId?this.state.promptsByRunId.get(t.runId):void 0)??(t.sessionKey?this.state.findSessionByKey(t.sessionKey):void 0);if(!e)return;if(!this.state.isCurrentStreamOwner(e,t.runId)){const s=!e.currentRunId&&void 0===e.pendingPromptId;if(!t.runId||!s||this.state.isRunCompleted(t.runId))return;if(!this.attachGatewayRun(e,t.runId))return}t.runId&&e.cronJobId&&!this.state.runIdToCronJobId.has(t.runId)&&this.state.setRunCronJobId(t.runId,e.cronJobId);e.sessionId;this.touchStreamActivity(e);const s=toISO(t.ts),n=t=>{this.sendTurnUpdate(e,t),e.assistantMq?.push(t)};if("assistant"===t.stream){e.hasAgentAssistantStream=!0,this.scheduleStructuredAssistantMedia(e,t.data);const o=this.state.extractAgentAssistantDelta(e,t.data??{});if(o){const a=normalizeTransportText(o);if(a){const o=e.agentAssistantTextSoFar??a,r=classifyHeartbeatFilter(o);r.filtered?this.logHeartbeatSkip({source:"assistant-delta",reason:r.reason,sessionId:e.sessionId,runId:t.runId,cumulative:o,delta:a}):(this.logger.debug?.(`[gateway-events] agent.assistant delta seq=${t.seq} runId=${t.runId} len=${a.length} source=${t.data?.delta?"delta":"cumulative"}`),n({sessionUpdate:"agent_message_chunk",content:{type:"text",text:a},...s?{timestamp:s}:{}}))}}const a=[],r=t.data;r&&(Array.isArray(r.content)?a.push(...r.content):isRecord(r.content)?a.push(r.content):"string"==typeof r.type&&a.push(r));for(const t of a){if(!isRecord(t))continue;const o=asString(t.type);if("thinking"===o){e.hasAgentThinkingStream=!0;const n=normalizeText(t.thinking)??normalizeText(t.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}continue}if("toolCall"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const o=this.resolveToolCallId(e,t,"start"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=resolveToolArgs(t);this.beginRunOutputUpload(e,o,a),n(buildToolCallPayload({toolCallId:o,toolName:a,args:r,timestamp:s}));continue}if("toolResult"===o){this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0;const o=this.resolveToolCallId(e,t,"result"),a=asString(t.name)??asString(t.toolName)??asString(t.tool_name)??"tool",r=t.result,i=normalizeText(t.text)??normalizeText(t.result)??(isRecord(r)||Array.isArray(r)?JSON.stringify(r,null,2):void 0);i&&this.trackRunOutputWork(e,this.emitNormalizedToolResult(e,{toolCallId:o,toolName:a,rawText:i,rawResult:r,timestamp:s,pushUpdate:n}).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result normalization failed: ${t instanceof Error?t.message:t}`)}));continue}}}else if("thinking"===t.stream){e.hasAgentThinkingStream=!0;const n=normalizeText(t.data?.delta)??normalizeText(t.data?.text);if(n){const t={sessionUpdate:"agent_thought_chunk",content:{type:"text",text:n},...s?{timestamp:s}:{}};this.sendTurnUpdate(e,t)&&e.assistantMq?.push(t)}}else if("tool"===t.stream){const o=t.data,a="string"==typeof o?.phase?o.phase:void 0;if("start"===a){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const t=this.resolveToolCallId(e,o,"start"),a=asString(o?.name)??asString(o?.toolName)??asString(o?.tool_name)??"tool",r=resolveToolArgs(o);this.beginRunOutputUpload(e,t,a),n(buildToolCallPayload({toolCallId:t,toolName:a,args:r,timestamp:s}))}else if("result"===a)this.sendToolCallSpacer(e,s),e.hasAgentToolResultStream=!0,this.trackRunOutputWork(e,this.handleToolResult(e,t,s).catch(t=>{this.logger.warn?.(`[gateway-events] Tool result handling failed: ${t instanceof Error?t.message:t}`)}));else{const t=o?.toolCall;if(t){this.sendToolCallSpacer(e,s),e.hasAgentToolStartStream=!0;const a=asString(t?.name)??asString(o?.name)??asString(o?.toolName)??"tool",r={...o,...t},i=this.resolveToolCallId(e,r,"start"),l=resolveToolArgs(r);n(buildToolCallPayload({toolCallId:i,toolName:a,args:l,timestamp:s}))}}}else if("lifecycle"===t.stream){const s=t.data,n="string"==typeof s?.phase?s.phase:void 0,o=(n,o)=>{this.logger.warn?.(`[gateway-events] lifecycle error runId=${t.runId} sessionKey=${t.sessionKey} message=${n} data=${JSON.stringify(s)}`);const a=classifyAgentRuntimeFailure({raw:n,source:"live_gateway",runtimeKind:"openclaw"});this.observeLifecycle(this.terminalRunOutput(e,this.failedRunOutput(a)),"interrupted"===a.kind?"agent_interrupt":o,e)};if("end"===n){const t=lifecycleEndFailureMessage(s);if(t)o(t,"agent_failure");else{const t="string"==typeof s?.stopReason?s.stopReason:void 0;this.observeLifecycle(this.terminalRunOutput(e,{kind:"complete",stopReason:toMessageServiceCompletedStopReason(t)}),"agent_final",e)}}else if("cancelled"===n||"cancel"===n)this.observeLifecycle(this.terminalRunOutput(e,{kind:"cancelled",stopReason:"cancelled"}),"agent_cancelled",e);else if("error"===n){const t=s?.error,e=t&&"object"==typeof t?t:void 0;o(("string"==typeof s?.message?s.message:void 0)??("string"==typeof t?t:void 0)??("string"==typeof e?.message?e.message:void 0)??("string"==typeof s?.reason?s.reason:void 0)??(e?JSON.stringify(e):void 0)??"gateway lifecycle error","agent_failure")}}}scheduleStructuredAssistantMedia(t,e){const s=resolveTurnId(t);if(!s||!this.runOutputRouter.routesTo(s,"message_service"))return;const n=isRecord(e)?e:void 0,o=[...new Set([..."string"==typeof n?.mediaUrl?[n.mediaUrl]:[],...Array.isArray(n?.mediaUrls)?n.mediaUrls.filter(t=>"string"==typeof t):[]].map(t=>t.trim()).filter(Boolean))];if(0===o.length)return;const a=resolveToolArtifactLinks({toolCallId:`final:${s}`,toolName:"final_reply_media",payloadMediaUrls:o}).then(e=>{for(const s of e){const e={sessionUpdate:"agent_message_chunk",content:{type:"resource_link",uri:s.uri,name:s.name,...s.mimeType?{mimeType:s.mimeType}:{}},timestamp:(new Date).toISOString()};this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}}).catch(t=>{this.logger.warn?.(`[gateway-events] structured assistant media enrichment failed runId=${s}: ${t instanceof Error?t.message:String(t)}`)});this.trackRunOutputWork(t,a)}async handleToolResult(t,e,s){const n=e.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_${e.runId}_${e.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,u=a??r,d=n?.result;await this.emitNormalizedToolResult(t,{toolCallId:r,normalizeToolCallId:u,toolName:i,rawText:l,rawResult:d,timestamp:s})}async emitNormalizedToolResult(t,e){const s=e.pushUpdate??(e=>{this.sendTurnUpdate(t,e)&&t.assistantMq?.push(e)}),n=resolveTurnId(t);try{await emitNormalizedToolResultForTurn({session:t,envelope:{toolCallId:e.toolCallId,toolName:e.toolName,rawText:e.rawText,rawResult:e.rawResult,timestamp:e.timestamp},normalizeToolCallId:e.normalizeToolCallId,turnId:n,config:this.config,logger:this.logger,writeObs:this.writeObs,getCanvasState:this.getCanvasState,sendUpdate:t=>(s(t),!0)})}finally{n&&e.toolName===BEEOS_UPLOAD_TOOL_NAME&&this.runOutputRouter.completeUpload(n,e.toolCallId)}}attachGatewayRun(t,e){this.logger.debug?.(`[gateway-events] Attaching Gateway run ${e} on session ${t.sessionId} (prev=${t.currentRunId})`);const s=this.adoptBackgroundMutation(t,e);if(!s.ok)return this.observeLifecycle(this.abortGatewayRun(t.gatewaySessionKey,e),"background_run_conflict_abort",t),!1;t.currentRunId=e,t.promptDone=!1,this.state.promptsByRunId.set(e,t);const n=this.state.runIdToCronJobId.get(e);n&&(t.cronJobId=n),t.assistantStreamStartedAt=Date.now(),t.lastStreamActivityTs=Date.now(),t.hasAgentAssistantStream=void 0,t.hasAgentThinkingStream=void 0,t.hasAgentToolStartStream=void 0,t.hasAgentToolResultStream=void 0,t.chatAssistantTextSoFar=void 0,t.agentAssistantTextSoFar=void 0,t.chatReplayDedupKeys?.clear(),t.presentedFileArtifactKeys=new Set,t.emittedToolResultKeys=new Set;const o=["acp"];return"owner"===s.ownership&&t.channelId?.trim()&&!t.cronJobId&&o.push("message_service"),openRunOutputRoute(this.runOutputRouter,t,e,o,{awaitPersistenceReconciliation:o.includes("message_service")}),t.assistantMq&&!t.assistantMq.closed||(t.assistantMq=new MessageQueue),!0}handleCronEvent(t){this.transport.sendNotification("_beeos/cron_event",{payload:t});const e=t??{};"string"==typeof e.runId&&"string"==typeof e.jobId&&this.state.setRunCronJobId(e.runId,e.jobId),"started"===e.action&&this.cloud&&this.reportCronEventToAgentGateway(e),"added"===e.action&&this.maybeBackfillBeeosChannel(e),"finished"===e.action&&"string"==typeof e.summary&&e.summary&&this.publishCronResult(e)}async reportCronEventToAgentGateway(t){if(!this.cloud)return;const e="string"==typeof t.jobId&&t.jobId.trim()?t.jobId.trim():void 0,s="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!e||void 0===s)return void this.logger.warn?.("[gateway-events] cron start missing stable execution identity; skipping notification");const n="string"==typeof t.runId&&t.runId.trim()?t.runId.trim():`cron:${e}:${s}`,o={...t,jobId:e,runAtMs:s,runId:n},a="string"==typeof t.sessionKey?t.sessionKey:void 0;a&&null==o.sessionKey&&(o.sessionKey=a);const r=a?this.state.findSessionByKey(a)?.sessionId:void 0;if(r&&(o.sessionId=r),("string"!=typeof o.name||!o.name)&&e&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),s=t?.jobs;if(Array.isArray(s))for(const t of s)if(isRecord(t)&&t.id===e&&"string"==typeof t.name&&t.name){o.name=t.name;break}}catch(t){this.logger.debug?.(`[gateway-events] cron.list lookup failed: ${t instanceof Error?t.message:t}`)}try{await this.cloud.events.publish({type:"cron",data:o})}catch(t){this.logger.warn?.(`[gateway-events] event report failed: ${t instanceof Error?t.message:t}`)}}async maybeBackfillBeeosChannel(t){const e="string"==typeof t.jobId?t.jobId:void 0;if(!e)return;if(!this.gatewayRequest)return;if(this.cronChannelCache?.get(e))return;let s;try{const t=await this.gatewayRequest("cron.list",{}),n=t?.jobs;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t;if(n.id===e){s=n;break}}}catch(t){return void this.logger.debug?.(`[gateway-events] maybeBackfillBeeosChannel cron.list failed: ${t instanceof Error?t.message:t}`)}if(!s)return;const n="string"==typeof s.description?s.description:"";if(BEEOS_CHANNEL_TAG_RE.test(n)){const t=decodeBeeosChannelId(n);return void(t&&this.setCronChannelCacheLocal(e,t))}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=${e}`);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=${e}`);const u=encodeBeeosChannelTag(n,l);try{await this.gatewayRequest("cron.update",{id:e,patch:{description:u}}),this.setCronChannelCacheLocal(e,l),this.logger.info?.(`[gateway-events] backfilled beeos:channel for LLM-created cron job=${e} channel=${l}`)}catch(t){this.logger.warn?.(`[gateway-events] maybeBackfillBeeosChannel cron.update failed: ${t instanceof Error?t.message:t}`)}}setCronChannelCacheLocal(t,e){this.cronChannelCache&&this.cronChannelCache.set(t,e)}async publishCronResult(t){const e=this.getCronConversationDelivery?.();if(!e)return;const s="string"==typeof t.jobId?t.jobId:void 0;if(!s)return;const n=t.summary,o="string"==typeof t.runId&&t.runId?t.runId:void 0,a="number"==typeof t.runAtMs&&Number.isSafeInteger(t.runAtMs)&&t.runAtMs>0?t.runAtMs:void 0;if(!o&&void 0===a)return void this.logger.warn?.(`[gateway-events] cron result missing stable execution identity; skipping conversation delivery job=${s}`);let r=this.cronChannelCache?.get(s);if(!r&&this.gatewayRequest)try{const t=await this.gatewayRequest("cron.list",{}),e=t?.jobs;if(Array.isArray(e))for(const t of e){if(!t||"object"!=typeof t)continue;const e=t;if(e.id===s){r=decodeBeeosChannelId(e.description);break}}}catch(t){this.logger.debug?.(`[gateway-events] cron.list fallback failed: ${t instanceof Error?t.message:t}`)}if(r)try{const i=await e.deliver({conversationId:r,jobId:s,...o?{runId:o}:{},...void 0!==a?{runAtMs:a}:{},..."string"==typeof t.sessionKey&&t.sessionKey?{sessionKey:t.sessionKey}:{},body:n,..."string"==typeof t.status&&t.status?{status:t.status}:{}});this.logger.info?.(`[gateway-events] delivered cron result to channel=${r} job=${s} message=${i.messageId} disposition=${i.disposition}`)}catch(t){const e=t instanceof Error?t.message:String(t);if(isChannelGoneError(t,e))return this.logger.info?.(`[gateway-events] origin channel gone (session likely deleted), skipping publish: channel=${r} job=${s} err=${e}`),void this.cronChannelCache?.delete(s);this.logger.warn?.(`[gateway-events] publishCronResult failed: ${e}`)}}logHeartbeatSkip(t){const e=previewSanitizedText(t.cumulative),s=void 0!==t.delta?previewSanitizedText(t.delta):void 0;this.logger.info?.(`[gateway-events] SKIP heartbeat ${t.source} reason=${t.reason} sessionId=${t.sessionId} runId=${t.runId??"n/a"} cumLen=${t.cumulative.length}`+(void 0!==t.delta?` deltaLen=${t.delta.length}`:"")+` cumulative="${e}"`+(void 0!==s?` delta="${s}"`:"")),this.writeObs({component:"gateway-events",domain:"lifecycle",name:"heartbeat_filter_skip",severity:"info",summary:`heartbeat ${t.source} drop (${t.reason})`,payload:{source:t.source,reason:t.reason,sessionId:t.sessionId,runId:t.runId,cumulativeLen:t.cumulative.length,cumulativePreview:e,...void 0!==t.delta?{deltaLen:t.delta.length,deltaPreview:s}:{}}})}}export function isChannelGoneError(t,e){const s=e.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(t)){const e=t.status??t.statusCode??t.code;if(404===e||410===e)return!0}return!1}
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{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{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{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.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;mcpProjection;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;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),openRunOutputRoute(this.runOutputRouter,r,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}resolveNativePromptSessionKey(e,t){return this.core.resolveNativePromptSessionKey(e,t)}prepareNativePromptModel(e,t,i){return this.core.prepareNativePromptModel(e,t,i)}ensureImInboundSession(e,t,i,n){const r=this.core.ensureSessionForKey(e);r.channelId=t,r.inboundMessageId=i,r.activeMentions=n.length>0?n:void 0}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,u={}){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=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.gateway=u.gateway??null,this.agentProjection=u.agentProjection,this.markAgentProjectionDirty=u.markAgentProjectionDirty,this.templateOriginReporter=u.templateOriginReporter,this.agentMappings=u.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=u.agentCapabilities??{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 m=(u.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=m;const p=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:p,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,...u.runExclusive?{runExclusive:u.runExclusive}:{},mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:m,agentMappings:this.agentMappings}),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,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),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,runOutputRouter:m,adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},agentFetch:this.agentFetchFn,getCronConversationDelivery:()=>this.cronConversationDelivery??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)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const n=i.currentConsumerBinding();if(!n)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const r=e.runtimeConsumer;let s;r&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===r&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(r).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:n,getBinding:()=>i.currentConsumerBinding()??n,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}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};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}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");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))return;const c="terminal"!==this.config.bridge.mode;if(c&&a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}else c?(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] terminal-only bridge mode; ACP phone-home not started"),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.markAgentProjectionDirty?this.markAgentProjectionDirty():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("terminal"===this.config.bridge.mode)return void this.logger.info?.("[acp-server] terminal-only bridge mode; ACP phone-home suppressed");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;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}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const n=i instanceof Error?i.message:String(i),r=n.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${n}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:r?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:n,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] connection transport=openclaw_gateway_ws state=connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.core.prepareWorkspaceCatalog(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] connection transport=openclaw_gateway_ws state=disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] connection transport=openclaw_gateway_ws state=reconnecting attempt=${e.attempt} delay_ms=${Math.round(e.delayMs)}`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{const t="connect_fail"===e.event&&!0===e.retryable,i=`[acp-server] connection transport=openclaw_gateway_ws event=${e.event} duration_ms=${e.durationMs??"?"}`;t?this.logger.debug?.(i):this.logger.info?.(i),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:t?"debug":"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}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.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}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.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();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),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.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&o.push(a("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&o.push(a("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),g&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&o.push(a("websocket-server",()=>{u.removeAllListeners(),u.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{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{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{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.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{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{cloud;instanceId;runtimeAuthority;mcpProjection;mcpCredentialReferences;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;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 r=this.config.gateway.agentId||this.core.getDefaultAgentId();let n,s;i?(n=this.state.findSessionByKey(i),s=i,n?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${n.sessionId}`):(n=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${n.sessionId}`))):(s=`agent:${r}:ephemeral:${randomUUID()}`,n=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const o=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let a=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(n&&!n.promptDone&&(void 0!==n.pendingPromptId||void 0!==n.currentRunId||void 0!==n.gatewayRequestId))return(n.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",n.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(o);a=!0;const c=await i.agentSend(s,e,r,t);return c?.runId&&n?(this.state.resetForNewRun(n,n.sessionId,this.transport),n.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(o,c.runId),this.core.observeActiveRun(n),n.assistantStreamStartedAt=Date.now(),n.writeGateToken=o,this.state.promptsByRunId.set(c.runId,n),openRunOutputRoute(this.runOutputRouter,n,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(o),c}catch(e){throw a?this.sessionMutationCoordinator.requireRecovery(o,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(o),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}resolveNativePromptSessionKey(e,t){return this.core.resolveNativePromptSessionKey(e,t)}prepareNativePromptModel(e,t,i){return this.core.prepareNativePromptModel(e,t,i)}ensureImInboundSession(e,t,i,r){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i,n.activeMentions=r.length>0?r:void 0}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,r,n,s,o,a,c,l,h,g,d={}){this.cloud=n,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=h,this.mcpCredentialReferences=g,this.config=e,this.logger=t,this.writeObs=r??(()=>{}),this.thirdPartyChannels=o??new Set,this.sessionMutationCoordinator=i,this.gateway=d.gateway??null,this.agentProjection=d.agentProjection,this.markAgentProjectionDirty=d.markAgentProjectionDirty,this.templateOriginReporter=d.templateOriginReporter,this.agentMappings=d.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=d.agentCapabilities??{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=(d.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=u;const m=this.cloud?new BeeosFileResolver({cloud:this.cloud,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:m,cloud:this.cloud,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,...d.runExclusive?{runExclusive:d.runExclusive}:{},mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:u,agentMappings:this.agentMappings}),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,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),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,runOutputRouter:u,adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},cloud:this.cloud,getCronConversationDelivery:()=>this.cronConversationDelivery??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=a??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)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const r=i.currentConsumerBinding();if(!r)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const n=e.runtimeConsumer;let s;n&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===n&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(n).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:r,getBinding:()=>i.currentConsumerBinding()??r,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}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};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,r=e.factory;let n=!1;if(i&&r){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=r.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}),o=await this.waitForGeneration(s,e);if("completed"!==o.status)return void("cancelled"===o.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const a=o.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(a,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(a.receiptHistory),e.runtimeConsumer=a,this.runtimeRpcConsumer=a,a.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),n=!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&&n){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const o=this.config.bridge.url,a=this.config.keyFile;if(o&&!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&&o&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:o,authority:i,keys:i.agentKeys}),r=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 r.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=r,this.terminalOutbound=r;try{const t=await this.waitForGeneration(r.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");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))return;const c="terminal"!==this.config.bridge.mode;if(c&&o&&a&&BridgeClientCtor){Date.now();await this.startBridgeMode(o,a,e)}else c?(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] terminal-only bridge mode; ACP phone-home not started"),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.markAgentProjectionDirty?this.markAgentProjectionDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const r=t.abortController.signal;return r.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,n)=>{let s,o=!1;const a=e=>{o||(o=!0,r.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>a({status:"cancelled"});r.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>a({status:"timed_out"}),i),s.unref?.()),e.then(e=>a({status:"completed",value:e}),e=>{o||(o=!0,r.removeEventListener("abort",c),s&&clearTimeout(s),n(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const r=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,r),r}async startBridgeMode(e,t,i){if("terminal"===this.config.bridge.mode)return void this.logger.info?.("[acp-server] terminal-only bridge mode; ACP phone-home suppressed");if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const r=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(r,!1).catch(()=>{});i.bridge=r,this.bridgeInstance=r;const n=()=>this.bridgeInstance===r;r.on("connected",()=>{n()&&(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())}),r.on("disconnected",(...e)=>{if(!n())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),o=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${o})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.cloud){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;this.cloud.bridge.config().then(async s=>{if(this.isGenerationActive(i)&&n()&&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(r,!1),!this.isGenerationActive(i)||this.bridgeInstance!==r)return;i.bridge===r&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),r.on("message",(...e)=>{if(!n())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")}}),r.on("error",(...e)=>{if(!n())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{r.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(r).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(r,!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"===r.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const o=Date.now();try{const e=await this.waitForGeneration(r.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()-o}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-o}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 r=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,r),r}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.cloud)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return;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],r=3e5,n=()=>{const n=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(n)},r))}const s=i[Math.min(e,i.length-1)],o=Math.round(s/1e3),a=`[acp-server] agent sync retry #${n} in ${o}s`;n>0&&n%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${n} attempts — retrying every ${o}s`):n<=i.length?this.logger.info?.(a):this.logger.debug?.(a),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(n)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,cloud:this.cloud,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,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")):n()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),n()}finally{this.agentSyncInFlight=!1}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const r=i instanceof Error?i.message:String(i),n=r.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${r}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:n?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:r,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] connection transport=openclaw_gateway_ws state=connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.core.prepareWorkspaceCatalog(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] connection transport=openclaw_gateway_ws state=disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] connection transport=openclaw_gateway_ws state=reconnecting attempt=${e.attempt} delay_ms=${Math.round(e.delayMs)}`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{const t="connect_fail"===e.event&&!0===e.retryable,i=`[acp-server] connection transport=openclaw_gateway_ws event=${e.event} duration_ms=${e.durationMs??"?"}`;t?this.logger.debug?.(i):this.logger.info?.(i),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:t?"debug":"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}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 r="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(r);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,r)=>this.core.dispatchLegacyTerminalCapability(e,t,i,r)});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 r=e.split("_");if(r.length>=3&&"tc"===r[0]){const e=r[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.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}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.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();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:r})=>{try{const n=`${t}/skill.json`,s=await r(n,"utf-8"),o=JSON.parse(s);if(o.formSchemas?.length){for(const e of o.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${o.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,r;const n=new Promise((e,t)=>{i=e,r=t});t.stopPromise=n,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){r(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),n}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let r,n=!1;const s=new Promise(e=>{r=setTimeout(()=>{n=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),r.unref?.()}),o=async(r,n)=>{let o;const a=new Promise(e=>{o=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:o},()=>n(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),a.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: ${r}`);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:r,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},a=[];this.setTerminalServiceReady(e,!1),a.push(o("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),a.push(o("session-state",()=>this.state.destroy())),a.push(o("canvas-orchestrator",()=>this.canvasOrch.destroy())),a.push(o("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&a.push(o("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&a.push(o("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&a.push(o("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,a.push(o("runtime-authority",()=>e.authority?.stop()))),g&&a.push(o("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&a.push(o("websocket-server",()=>{u.removeAllListeners(),u.close()})),await Promise.all(a),await o("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await o("terminal-handler",()=>this.terminalHandler.destroy()),await o("terminal-manager",()=>this.terminalMgr.destroy()),r&&clearTimeout(r),n||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 +1 @@
1
- const UUID_AGENT_ID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;export function resolveSyncAgentName(t){const e=t.id.trim(),n=t.name?.trim()??"";return UUID_AGENT_ID_RE.test(e)&&n?n:e||n}export function isAgentSyncWaitForGateway(t){return!(!t||t instanceof Map||"wait_gateway"!==t.status)}export async function syncAgentsToPlatform(t){const{gateway:e,agentGatewayUrl:n,capabilities:a,deliveryMode:s,boundAgentFetch:i,logger:o}=t;if(!i)return o?.debug?.("[agent-sync] no boundAgentFetch — skipping sync (no key pair)"),null;if(!n)return o?.debug?.("[agent-sync] no agentGatewayUrl — skipping sync"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return o?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const c=r.map(t=>({agent:t,syncName:resolveSyncAgentName(t)})).filter(t=>t.syncName),l=c.some(t=>!UUID_AGENT_ID_RE.test(t.syncName)),d=[];for(const{agent:t,syncName:n}of c){if(l&&UUID_AGENT_ID_RE.test(n)){o?.debug?.(`[agent-sync] skipping platform-id shadow agent ${n}`);continue}let i=[];try{const n=await e.request("skills.status",{agentId:t.id});i=(n?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{},...t.skillKey?{skillKey:t.skillKey}:{}}))}catch{o?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const r={};if(a)for(const[t,e]of Object.entries(a))e&&(r[t]=!0);const c=t.identity?.name||t.name||n;d.push({name:n,displayName:c,description:t.name||t.identity?.name||n,version:"0.1.0",capabilities:r,skills:i,avatarUrl:t.identity?.avatarUrl||"",homepage:"",...s?{deliveryMode:s}:{}})}const y=new URL("/api/v1/agents/sync",n),g=await i(y,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({agents:d})});if(g.ok){const t=await g.json();o?.info?.(`[agent-sync] synced ${d.length} agent(s) — created=${t.created??0} updated=${t.updated??0} deactivated=${t.deactivated??0}`);const e=new Map,n=Array.isArray(t.data)?t.data:[];for(const t of n)t&&"string"==typeof t.name&&"string"==typeof t.id&&e.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return e.size>0?e:null}{const t=await g.text().catch(()=>"");return o?.warn?.(`[agent-sync] sync failed: HTTP ${g.status} — ${t.slice(0,200)}`),null}}catch(t){const e=t instanceof Error?t.message:String(t);return o?.warn?.(`[agent-sync] sync error: ${e}`),"Not connected to Gateway"===e?{status:"wait_gateway",reason:e}:null}}
1
+ import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";const UUID_AGENT_ID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;export function resolveSyncAgentName(t){const e=t.id.trim(),a=t.name?.trim()??"";return UUID_AGENT_ID_RE.test(e)&&a?a:e||a}export function isAgentSyncWaitForGateway(t){return!(!t||t instanceof Map||"wait_gateway"!==t.status)}export async function syncAgentsToPlatform(t){const{gateway:e,cloud:a,capabilities:n,deliveryMode:s,logger:i}=t;if(!a)return i?.debug?.("[agent-sync] no Cloud gateway client — skipping sync (no key pair or agentGatewayUrl)"),null;try{const t=await e.request("agents.list",{}),r=t?.agents??[];if(0===r.length)return i?.debug?.("[agent-sync] no agents from Gateway — skipping sync"),null;const o=r.map(t=>({agent:t,syncName:resolveSyncAgentName(t)})).filter(t=>t.syncName),c=o.some(t=>!UUID_AGENT_ID_RE.test(t.syncName)),l=[];for(const{agent:t,syncName:a}of o){if(c&&UUID_AGENT_ID_RE.test(a)){i?.debug?.(`[agent-sync] skipping platform-id shadow agent ${a}`);continue}let r=[];try{const a=await e.request("skills.status",{agentId:t.id});r=(a?.skills??[]).filter(t=>t.eligible&&!t.disabled).map(t=>({name:t.name,description:t.description,...t.emoji?{emoji:t.emoji}:{},...t.skillKey?{skillKey:t.skillKey}:{}}))}catch{i?.debug?.(`[agent-sync] skills.status failed for agent ${t.id} — continuing without skills`)}const o={};if(n)for(const[t,e]of Object.entries(n))e&&(o[t]=!0);const d=t.identity?.name||t.name||a;l.push({name:a,displayName:d,description:t.name||t.identity?.name||a,version:"0.1.0",capabilities:o,skills:r,avatarUrl:t.identity?.avatarUrl||"",homepage:"",...s?{deliveryMode:s}:{}})}let d;try{d=await a.agents.sync({agents:l})}catch(t){if(!(t instanceof CloudHttpError))throw t;return i?.warn?.(`[agent-sync] sync failed: HTTP ${t.status} — ${t.body.slice(0,200)}`),null}i?.info?.(`[agent-sync] synced ${l.length} agent(s) — created=${d.created??0} updated=${d.updated??0} deactivated=${d.deactivated??0}`);const y=new Map,g=Array.isArray(d.data)?d.data:[];for(const t of g)t&&"string"==typeof t.name&&"string"==typeof t.id&&y.set(t.name,{id:t.id,instanceId:t.instanceId??"",name:t.name,displayName:t.displayName??t.name,description:t.description??"",version:t.version??"",avatarUrl:t.avatarUrl??"",status:t.status??"active"});return y.size>0?y:null}catch(t){const e=t instanceof Error?t.message:String(t);return i?.warn?.(`[agent-sync] sync error: ${e}`),"Not connected to Gateway"===e?{status:"wait_gateway",reason:e}:null}}
@@ -0,0 +1 @@
1
+ import{randomUUID}from"node:crypto";import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";const UUID_RE=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function asObject(e){return e&&"object"==typeof e&&!Array.isArray(e)?e:{}}function stringValue(e){return"string"==typeof e&&e.trim()?e.trim():void 0}function toolResult(e){return{content:JSON.stringify(e,null,2)}}function isUuid(e){return Boolean(e&&UUID_RE.test(e))}function resolveTarget(e){const t=e.getDefaultAgent?.(),r=stringValue(t?.instanceId),o=stringValue(t?.id);if(r&&o)return{instanceId:r,agentId:o}}function resolveConversationId(e,t){const r=stringValue(e.getCurrentConversationId?.(t));return isUuid(r)?r:void 0}async function request(e){try{const t=await e()??{},r=asObject(t);return{ok:!0,data:Object.hasOwn(r,"data")?r.data:t}}catch(e){if(e instanceof CloudHttpError){let t=e.body;try{t=e.body?JSON.parse(e.body):{}}catch{}return{ok:!1,status:e.status,error:`Automation request failed: HTTP ${e.status}`,details:t}}return{ok:!1,status:0,error:e instanceof Error?e.message:String(e),details:void 0}}}function listQuery(e){const t={};for(const r of["status","cursor"]){const o=stringValue(e[r]);o&&(t[r]=o)}return"number"==typeof e.limit&&Number.isFinite(e.limit)&&(t.limit=String(Math.trunc(e.limit))),t}function buildSchedule(e){const t=stringValue(e.kind)??"every",r={kind:t,tz:stringValue(e.tz)??"UTC"};switch(t){case"every":{r.intervalSeconds="number"==typeof e.intervalSeconds?String(Math.trunc(e.intervalSeconds)):stringValue(e.intervalSeconds)??"3600";const t=stringValue(e.anchorAt);t&&(r.anchorAt=t);break}case"at":{const t=stringValue(e.at);t&&(r.at=t);break}case"cron":{const t=stringValue(e.expression);t&&(r.expression=t);break}default:return{error:"schedule.kind must be one of at, every, or cron"}}return r}function buildSession(e,t,r){const o=stringValue(asObject(e.session).mode)??"new";if("new"!==o&&"bound"!==o)return{error:"session.mode must be new or bound"};const n={mode:o},a=resolveConversationId(t,r);if("bound"===o){if(!a)return{error:"bound Automation requires the current conversation context"};n.boundConversationId=a}return n}function createInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=resolveTarget(t);if(!n||!a||!s)return{error:"name, prompt, and a resolvable target agent are required"};const i=buildSchedule(asObject(o.schedule));if("error"in i)return i;const u=buildSession(o,t,r);if("error"in u)return u;const c={name:n,prompt:a,target:s,schedule:i,session:u},l=stringValue(o.validFrom),d=stringValue(o.validUntil);l&&(c.validFrom=l),d&&(c.validUntil=d);const m=resolveConversationId(t,r);return m&&(c.createdInConversationId=m),c}function createWebhookInput(e,t,r){const o=asObject(e),n=stringValue(o.name),a=stringValue(o.prompt),s=(stringValue(o.source)??"").toLowerCase(),i=resolveTarget(t);if(!n||!a||!i)return{error:"name, prompt, and a resolvable target agent are required"};if("github"!==s&&"gmail"!==s)return{error:"source must be github or gmail"};const u=o.events,c=Array.isArray(u)?u.filter(e=>"string"==typeof e&&Boolean(e.trim())).map(e=>e.trim()):[],l=stringValue(o.repoOwner),d=stringValue(o.repoName),m=stringValue(o.mailbox);if(!("github"!==s||0!==c.length&&l&&d))return{error:"github webhook requires events, repoOwner, and repoName"};if("gmail"===s&&!m)return{error:"gmail webhook requires mailbox"};const p=buildSession(o,t,r);if("error"in p)return p;const g={name:n,prompt:a,target:i,session:p,source:s};"github"===s?(g.events=c,g.repoOwner=l,g.repoName=d):g.mailbox=m;const y=stringValue(o.eventFilter);y&&(g.eventFilter=y);const f=stringValue(o.validFrom),b=stringValue(o.validUntil);f&&(g.validFrom=f),b&&(g.validUntil=b);const h=resolveConversationId(t,r);return h&&(g.createdInConversationId=h),g}function unwrapConnectorMcp(e){let t=e;if("string"==typeof t)try{t=JSON.parse(t)}catch{return}if(!t||"object"!=typeof t||Array.isArray(t))return;const r=t;if(null!=r.error)return;let o=r.result??r;if(!o||"object"!=typeof o||Array.isArray(o))return;const n=o;if(n.isError)return;const a=n.structuredContent;if(a&&"object"==typeof a&&!Array.isArray(a))return a;const s=n.content;if(Array.isArray(s))for(const e of s){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if("text"===t.type&&"string"==typeof t.text)try{const e=JSON.parse(t.text);if(e&&"object"==typeof e&&!Array.isArray(e))return e}catch{continue}}return n}async function runPreflight(e,t,r){const o=asObject(t),n=(stringValue(o.source)??"").toLowerCase(),a=resolveConversationId(e,r),s=resolveTarget(e),i={source:n,operation:"webhook",ready:!1,platform:{reachable:!1,authorized:!1},connector:{id:n,installed:!1,credentialStatus:"unknown"},operationSupported:"github"===n||"gmail"===n,target:{resolved:Boolean(s)},session:{canBindCurrentConversation:Boolean(a)},nextAction:"retry_or_check_connection"};if("github"!==n&&"gmail"!==n)return i.blockingReason="unsupported_source",i.nextAction="retry_or_check_connection",i;if("gmail"===n&&!stringValue(o.mailbox))return i.blockingReason="mailbox_required",i.nextAction="provide_mailbox",i;if("github"===n){const e=o.events,t=Array.isArray(e)&&e.some(e=>"string"==typeof e&&e.trim());if(!stringValue(o.repoOwner)||!stringValue(o.repoName)||!t)return i.blockingReason="github_webhook_input_required",i.nextAction="provide_repository_and_events",i}const u=await request(()=>e.gateway.automations.list({limit:1}));if(!u.ok)return i.blockingReason="automation_gateway_unavailable",i.platform={reachable:!1,authorized:!1,status:u.status,error:u.error},i.nextAction="retry_or_check_connection",i;i.platform={reachable:!0,authorized:!0};const c={jsonrpc:"2.0",id:randomUUID(),method:"tools/call",params:{name:"list_connectors",arguments:{}}},l=await request(()=>e.gateway.connectors.callJson(c));if(!l.ok)return i.blockingReason="mcp_unavailable",i.connector={id:n,installed:!1,credentialStatus:"unknown",error:l.error,status:l.status},i.nextAction="retry_or_check_connection",i;const d=unwrapConnectorMcp(l.data),m=d&&Array.isArray(d.connectors)?d.connectors:void 0;let p;if(Array.isArray(m))for(const e of m){if(!e||"object"!=typeof e||Array.isArray(e))continue;const t=e;if((stringValue(t.connectorId)??stringValue(t.connector_id)??"").toLowerCase()===n){p=t;break}}if(!p)return i.blockingReason="mcp_not_connected",i.nextAction="install_connector",i;const g=(stringValue(p.credentialStatus)??stringValue(p.credential_status)??"unknown").toLowerCase();return i.connector={id:n,installed:!0,credentialStatus:g},"active"!==g?(i.blockingReason="credential_missing",i.nextAction="authorize_connector",i):s?(i.ready=!0,i.nextAction="create_automation",i.sideEffects="gmail"===n?["create_rule","ensure_shared_gmail_watch"]:["create_rule","issue_hook","register_connector_webhook"],i):(i.blockingReason="target_not_resolved",i.nextAction="sync_openclaw_agent",i)}async function runCapabilities(e,t){const r=resolveTarget(e),o=resolveConversationId(e,t),n={platform:{reachable:!1,authorized:!1},capabilities:{createSchedule:Boolean(r),createWebhook:Boolean(r),run:!0},target:{resolved:Boolean(r)},session:{currentConversationAvailable:Boolean(o),canBindCurrentConversation:Boolean(o)}},a=await request(()=>e.gateway.automations.list({limit:1}));return a.ok?n.platform={reachable:!0,authorized:!0}:n.platform={reachable:!1,authorized:!1,status:a.status,error:a.error},n}function requestErrorResult(e){return toolResult({error:e.error,status:e.status,details:e.details})}export function registerAutomationTools(e,t){e.registerTool({name:"beeos_automation_create",description:"Create a BeeOS platform Automation owned by the current user. The target is the current Agent and is resolved automatically. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string",description:"Human-readable automation name"},prompt:{type:"string",description:"Prompt sent to the target agent when the automation fires"},schedule:{type:"object",properties:{kind:{type:"string",enum:["at","every","cron"]},tz:{type:"string"},at:{type:"string"},intervalSeconds:{type:"integer",minimum:1},anchorAt:{type:"string"},expression:{type:"string"}}},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every run returns here. Use new if the user requests a separate conversation per run or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt"]},async execute(e,r,o){const n=createInput(r,t,e);if("error"in n)return toolResult(n);try{const e=await request(()=>t.gateway.automations.create(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return t.logger?.warn?.(`[beeos_automation_create] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_create_webhook",description:"Create a BeeOS platform Webhook Automation. GitHub uses source/events/repoOwner/repoName; Gmail uses source/mailbox only (never hookId/hookUrl/repo*). Runs preflight first. Prefer session.mode=bound for a task created in this chat.",parameters:{type:"object",properties:{name:{type:"string"},prompt:{type:"string"},source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"},minItems:1},repoOwner:{type:"string"},repoName:{type:"string"},eventFilter:{type:"string"},mailbox:{type:"string"},session:{type:"object",description:"Always set session.mode. Prefer bound for tasks created in the current BeeOS chat so every event returns here. Use new if the user requests a separate conversation per event or the current chat cannot bind. The plugin fills the bound conversation ID; do not supply it.",properties:{mode:{type:"string",enum:["new","bound"]}}},validFrom:{type:"string"},validUntil:{type:"string"}},required:["name","prompt","source"]},async execute(e,r,o){const n=createWebhookInput(r,t,e);if("error"in n)return toolResult(n);try{const a=await runPreflight(t,r,e);if(!a.ready)return toolResult({error:"automation_preflight_failed",preflight:a});const s=await request(()=>t.gateway.automations.createWebhook(n,{signal:o}));return s.ok?toolResult(s.data):requestErrorResult(s)}catch(e){return t.logger?.warn?.(`[beeos_automation_create_webhook] ${e instanceof Error?e.message:String(e)}`),toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_capabilities",description:"Check whether the BeeOS Automation platform is reachable/authorized and whether the current chat can bind as an Automation session. Read-only.",parameters:{type:"object",properties:{}},async execute(e){try{return toolResult(await runCapabilities(t,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_preflight",description:"Read-only readiness check for creating a BeeOS Webhook Automation. Returns nextAction such as authorize_connector, install_connector, provide_mailbox, provide_repository_and_events, sync_openclaw_agent, retry_or_check_connection, or create_automation.",parameters:{type:"object",properties:{source:{type:"string",enum:["github","gmail"]},events:{type:"array",items:{type:"string"}},repoOwner:{type:"string"},repoName:{type:"string"},mailbox:{type:"string"},session:{type:"object",properties:{mode:{type:"string",enum:["new","bound"]}}}},required:["source"]},async execute(e,r){try{return toolResult(await runPreflight(t,r,e))}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_list",description:"List BeeOS platform Automations owned by the current user.",parameters:{type:"object",properties:{status:{type:"string",enum:["enabled","paused","expired"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}}},async execute(e,r,o){const n=listQuery(asObject(r));try{const e=await request(()=>t.gateway.automations.list(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get",description:"Get one BeeOS platform Automation by its automation ID.",parameters:{type:"object",properties:{automationId:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=stringValue(asObject(r).automationId);if(!n)return toolResult({error:"automationId is required"});try{const e=await request(()=>t.gateway.automations.get(n,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_run",description:"Trigger an existing BeeOS platform Automation immediately and return its Run record.",parameters:{type:"object",properties:{automationId:{type:"string"},requestKey:{type:"string",description:"Optional idempotency key; generated when omitted"},sourceRunId:{type:"string"},rerunSessionMode:{type:"string",enum:["original","new"]}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=stringValue(n.requestKey)??randomUUID(),i=stringValue(n.sourceRunId),u=stringValue(n.rerunSessionMode),c={};i&&(c.sourceRunId=i),u&&(c.rerunSessionMode=u);try{const e=await request(()=>t.gateway.automations.run(a,c,s,{signal:o}));return e.ok?toolResult({requestKey:s,data:e.data}):toolResult({error:e.error,status:e.status,details:e.details,requestKey:s})}catch(e){return toolResult({error:e instanceof Error?e.message:String(e),requestKey:s})}}}),e.registerTool({name:"beeos_automation_runs",description:"List Runs for one BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},status:{type:"string",enum:["scheduled","dispatched","failed","skipped"]},limit:{type:"integer",minimum:1,maximum:100},cursor:{type:"string"}},required:["automationId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId);if(!a)return toolResult({error:"automationId is required"});const s=listQuery(n);try{const e=await request(()=>t.gateway.automations.runs(a,s,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}}),e.registerTool({name:"beeos_automation_get_run",description:"Get one Run record for a BeeOS platform Automation.",parameters:{type:"object",properties:{automationId:{type:"string"},runId:{type:"string"}},required:["automationId","runId"]},async execute(e,r,o){const n=asObject(r),a=stringValue(n.automationId),s=stringValue(n.runId);if(!a||!s)return toolResult({error:"automationId and runId are required"});try{const e=await request(()=>t.gateway.automations.runDetail(a,s,{signal:o}));return e.ok?toolResult(e.data):requestErrorResult(e)}catch(e){return toolResult({error:e instanceof Error?e.message:String(e)})}}})}
@@ -0,0 +1 @@
1
+ import{AsyncLocalStorage}from"node:async_hooks";const forbidden=new Set(["beeos_automation_create","beeos_automation_create_webhook","beeos_automation_run"]);export const AUTOMATION_TOOL_BLOCK_REASON="Automation Wake cannot create or trigger another Automation.";export class AutomationTurnPolicy{ready=!1;turns=new Map;activeContext=new AsyncLocalStorage;enable(){this.ready=!0}disable(){this.ready=!1}isReady(){return this.ready}enter(e,t,o){if(!this.ready||!e||!t||this.turns.has(t))throw new Error("automation_turn_policy_unavailable");const n={sessionKey:e,context:o};return this.turns.set(t,n),{run:e=>this.activeContext.run(o,e),close:()=>{this.turns.get(t)===n&&this.turns.delete(t)}}}check(e,t){if(!forbidden.has(e))return;return this.activeContext.getStore()||t.runId&&this.turns.has(t.runId)||t.sessionKey&&[...this.turns.values()].some(e=>e.sessionKey===t.sessionKey)?{block:!0,blockReason:AUTOMATION_TOOL_BLOCK_REASON}:void 0}}
@@ -0,0 +1 @@
1
+ const uuid=e=>"string"==typeof e&&/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(e)&&"00000000-0000-0000-0000-000000000000"!==e,ownerID=e=>uuid(e)||"string"==typeof e&&/^[0-9a-f]{32}$/.test(e)&&"00000000000000000000000000000000"!==e;export function decodeWake(e){const o=e.content,n=o?.metadata;if(!n||!["beeos.kind","beeos.automation_id","beeos.run_id","beeos.session_mode"].some(e=>Object.hasOwn(n,e)))return{kind:"ordinary"};return"chat_message"===e.type&&"automation.run"===n["beeos.kind"]&&uuid(n["beeos.automation_id"])&&uuid(n["beeos.run_id"])&&e.id===n["beeos.run_id"]&&uuid(e.conversationId)&&!e.replyTo&&o?.channel_id===e.conversationId&&o.context_id===e.conversationId&&"string"==typeof o.message&&o.message.trim()&&e.body===o.message&&!Object.hasOwn(o,"session_key")&&("new"===n["beeos.session_mode"]||"bound"===n["beeos.session_mode"])&&uuid(n.target_agent_id)&&"string"==typeof n.delivery_principal&&n.delivery_principal&&"string"==typeof n.runtime_epoch&&/^[1-9][0-9]*$/.test(n.runtime_epoch)&&["beeos-conversation","openapi-conversation"].includes(String(n.protocol))&&ownerID(n.caller_owner_id)&&e.senderId===`user:${n.caller_owner_id}`?{kind:"wake",context:Object.freeze({automationId:n["beeos.automation_id"],runId:n["beeos.run_id"],sessionMode:n["beeos.session_mode"]})}:{kind:"invalid",reason:"invalid_automation_wake"}}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;controlHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relay_url||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):(this.hasConnectedOnce=!0,this.opts.onConnected?.())}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}onCanvasControl(e){this.controlHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;case"canvas.toggle":case"canvas.dimensions":case"canvas.reference":case"canvas.clear":this.opts.logger?.info?.(`[canvas-relay] Received control frame type=${n} canvasId=${t.canvasId} sessionId=${t.sessionId??""}`),this.controlHandler?this.controlHandler(t):this.opts.logger?.warn?.(`[canvas-relay] Control frame ${n} received but no handler registered`);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch(e){this.opts.logger?.warn?.(`[canvas-relay] Failed to parse control message: ${e instanceof Error?e.message:String(e)}`)}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
1
+ import{randomUUID}from"node:crypto";import WebSocket from"ws";import{agentAuthHeaders}from"@beeos-ai/cloud-harness-sdk";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_DATA=500;export class CanvasRelayClient{ws=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;hasConnectedOnce=!1;canvasHandlers=new Map;joinedCanvases=new Set;pendingJoins=new Map;pendingData=[];actionHandler=null;controlHandler=null;pendingRequests=new Map;constructor(e){this.opts=e}connect(){this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){this.closed||this.doConnectAsync().catch(e=>{this.opts.logger?.warn?.(`[canvas-relay] Connect failed: ${e}`),this.scheduleReconnect()})}async doConnectAsync(){if(this.closed)return;const e="/ws/agent";let t,n={};if(this.opts.tokenProvider){const s=await this.opts.tokenProvider();t=`${(s.relayUrl||this.opts.canvasRelayUrl).replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),t+=`${t.includes("?")?"&":"?"}token=${encodeURIComponent(s.token)}`,n=agentAuthHeaders("GET",e,this.opts.agentKeys)}else t=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`,this.opts.agentId&&(t+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),n=agentAuthHeaders("GET",e,this.opts.agentKeys);this.opts.logger?.info?.(`[canvas-relay] Connecting to ${t}`);const s=new WebSocket(t,{headers:n});s.binaryType="arraybuffer",s.on("open",()=>{this.opts.logger?.info?.("[canvas-relay] Connected (multiplexed)"),this.reconnectAttempts=0,this.ws=s,(this.opts.displayName||this.opts.avatarUrl)&&this.sendControlMessage({type:"profile",displayName:this.opts.displayName,avatarUrl:this.opts.avatarUrl});for(const e of this.joinedCanvases)this.sendJoinCommand(e);for(const e of this.pendingData)this.sendTagged(e.canvasId,e.data);this.pendingData=[],this.hasConnectedOnce?(this.opts.logger?.info?.("[canvas-relay] Reconnected — notifying upper layer"),this.opts.onReconnect?.()):(this.hasConnectedOnce=!0,this.opts.onConnected?.())}),s.on("message",(e,t)=>{if(!t&&"string"!=typeof e){const t=e instanceof Buffer?e.toString("utf8"):(new TextDecoder).decode(e);return void this.handleTextMessage(t)}if("string"==typeof e)return void this.handleTextMessage(e);const n=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));this.handleBinaryMessage(n)}),s.on("close",()=>{this.opts.logger?.info?.("[canvas-relay] Disconnected"),this.ws=null;for(const[,e]of this.pendingJoins)e.reject(new Error("WebSocket disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("WebSocket disconnected"));this.pendingRequests.clear(),this.scheduleReconnect()}),s.on("error",e=>{this.opts.logger?.warn?.("[canvas-relay] Error:",e.message),s.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-relay] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>this.doConnect(),e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}async joinCanvas(e,t){if(this.canvasHandlers.set(e,t),!this.joinedCanvases.has(e)&&(this.joinedCanvases.add(e),this.connected))return new Promise((t,n)=>{this.pendingJoins.set(e,{resolve:t,reject:n}),this.sendJoinCommand(e);const s=setTimeout(()=>{const t=this.pendingJoins.get(e);t&&(this.pendingJoins.delete(e),this.opts.logger?.warn?.(`[canvas-relay] joinCanvas timed out for canvas=${e}, resolving optimistically`),t.resolve())},5e3),o=this.pendingJoins.get(e);this.pendingJoins.set(e,{resolve:()=>{clearTimeout(s),o.resolve()},reject:e=>{clearTimeout(s),o.reject(e)}})})}leaveCanvas(e){this.canvasHandlers.delete(e),this.joinedCanvases.delete(e),this.connected&&this.sendControlMessage({type:"leave",canvasId:e})}sendUpdate(e,t){this.sendTagged(e,t)}sendAwareness(e,t){this.sendTagged(e,t)}async ensureCanvas(e,t){return(await this.sendRequest({type:"ensureCanvas",sessionId:e,agentId:t?.agentId,title:t?.title,linkSessionId:t?.linkSessionId})).canvasId}async deleteCanvas(e){await this.sendRequest({type:"deleteCanvas",canvasId:e})}async createSnapshot(e,t){return(await this.sendRequest({type:"createSnapshot",canvasId:e,description:t})).snapshotId??null}onAction(e){this.actionHandler=e}onCanvasControl(e){this.controlHandler=e}sendActionResult(e){this.sendControlMessage({type:"action:result",...e})}sendActionError(e){this.sendControlMessage({type:"action:error",...e})}sendRequest(e){if(!this.connected)return Promise.reject(new Error(`WebSocket not connected (request: ${e.type})`));const t=randomUUID();return e.reqId=t,new Promise((n,s)=>{const o=setTimeout(()=>{this.pendingRequests.delete(t),s(new Error(`WSS request ${e.type} timed out`))},1e4);this.pendingRequests.set(t,{resolve:n,reject:s,timer:o}),this.sendControlMessage(e)})}sendTagged(e,t){if(!this.connected)return this.pendingData.length>=500&&(this.opts.logger?.warn?.("[canvas-relay] Pending data buffer full (500), dropping oldest entry"),this.pendingData.shift()),void this.pendingData.push({canvasId:e,data:t});const n=(new TextEncoder).encode(e),s=new Uint8Array(1+n.length+t.length);s[0]=n.length,s.set(n,1),s.set(t,1+n.length),this.ws.send(s)}handleTextMessage(e){try{const t=JSON.parse(e),n=t.type;if(n?.endsWith(":result")&&t.reqId){const e=this.pendingRequests.get(t.reqId);return void(e&&(this.pendingRequests.delete(t.reqId),clearTimeout(e.timer),t.error?e.reject(new Error(t.error)):e.resolve(t)))}switch(n){case"joined":{const e=this.pendingJoins.get(t.canvasId??"");e&&(this.pendingJoins.delete(t.canvasId),e.resolve()),this.opts.logger?.info?.(`[canvas-relay] Joined canvas ${t.canvasId}`);break}case"left":this.opts.logger?.info?.(`[canvas-relay] Left canvas ${t.canvasId}`);break;case"error":this.opts.logger?.warn?.(`[canvas-relay] Error for canvas ${t.canvasId}: ${t.message}`);break;case"action":this.opts.logger?.info?.(`[canvas-relay] Received action: canvasId=${t.canvasId} surfaceId=${t.surfaceId} requestId=${t.requestId} senderClientId=${t.senderClientId}`),this.actionHandler?this.actionHandler(t):this.opts.logger?.warn?.("[canvas-relay] Action received but no handler registered");break;case"canvas.toggle":case"canvas.dimensions":case"canvas.reference":case"canvas.clear":this.opts.logger?.info?.(`[canvas-relay] Received control frame type=${n} canvasId=${t.canvasId} sessionId=${t.sessionId??""}`),this.controlHandler?this.controlHandler(t):this.opts.logger?.warn?.(`[canvas-relay] Control frame ${n} received but no handler registered`);break;default:this.opts.logger?.debug?.(`[canvas-relay] Unknown control: ${n}`)}}catch(e){this.opts.logger?.warn?.(`[canvas-relay] Failed to parse control message: ${e instanceof Error?e.message:String(e)}`)}}handleBinaryMessage(e){if(e.length<2)return;const t=e[0];if(e.length<1+t+1)return;const n=(new TextDecoder).decode(e.slice(1,1+t)),s=e.slice(1+t),o=this.canvasHandlers.get(n);if(!o)return;const a=s[0];a===MSG_SYNC&&o.onUpdate?o.onUpdate(s):a===MSG_AWARENESS&&o.onAwareness&&o.onAwareness(s)}sendJoinCommand(e){this.sendControlMessage({type:"join",canvasId:e})}sendControlMessage(e){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(e))}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.pendingData=[];for(const[,e]of this.pendingJoins)e.reject(new Error("Client disconnected"));this.pendingJoins.clear();for(const[,e]of this.pendingRequests)clearTimeout(e.timer),e.reject(new Error("Client disconnected"));this.pendingRequests.clear()}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}}
@@ -1 +1 @@
1
- import WebSocket from"ws";import{agentAuthHeaders}from"../agent-auth.js";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_MESSAGES=500;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(e){this.opts=e}connect(e,s){this.canvasId=e,this.onUpdate=s.onUpdate,this.onAwareness=s.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const e=`/ws/agent/${this.canvasId}`;let s=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`;this.opts.agentId&&(s+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-ws] Connecting to ${s}`);const t=agentAuthHeaders("GET",e,this.opts.agentKeys),n=new WebSocket(s,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const e of this.pendingMessages)n.send(e);this.pendingMessages=[]}),n.on("message",e=>{const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));if(0===s.length)return;const t=s[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(s):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(s)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-ws] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}sendUpdate(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}sendAwareness(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}enqueuePending(e){this.pendingMessages.length>=500&&(this.pendingMessages.shift(),this.opts.logger?.warn?.("[canvas-ws] pendingMessages exceeded 500, dropping oldest")),this.pendingMessages.push(e)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
1
+ import WebSocket from"ws";import{agentAuthHeaders}from"@beeos-ai/cloud-harness-sdk";import{MSG_SYNC,MSG_AWARENESS}from"./constants.js";const RECONNECT_BASE_MS=1e3,RECONNECT_MAX_MS=3e4,MAX_PENDING_MESSAGES=500;export class CanvasWSProvider{ws=null;canvasId=null;opts;reconnectAttempts=0;reconnectTimer=null;closed=!1;onUpdate=null;onAwareness=null;pendingMessages=[];constructor(e){this.opts=e}connect(e,s){this.canvasId=e,this.onUpdate=s.onUpdate,this.onAwareness=s.onAwareness??null,this.closed=!1,this.reconnectAttempts=0,this.doConnect()}doConnect(){if(this.closed||!this.canvasId)return;const e=`/ws/agent/${this.canvasId}`;let s=`${this.opts.canvasRelayUrl.replace(/^http/,"ws")}${e}`;this.opts.agentId&&(s+=`?agentId=${encodeURIComponent(this.opts.agentId)}`),this.opts.logger?.info?.(`[canvas-ws] Connecting to ${s}`);const t=agentAuthHeaders("GET",e,this.opts.agentKeys),n=new WebSocket(s,{headers:t});n.binaryType="arraybuffer",n.on("open",()=>{this.opts.logger?.info?.(`[canvas-ws] Connected to canvas ${this.canvasId}`),this.reconnectAttempts=0,this.ws=n;for(const e of this.pendingMessages)n.send(e);this.pendingMessages=[]}),n.on("message",e=>{const s=new Uint8Array(e instanceof ArrayBuffer?e:e.buffer.slice(e.byteOffset,e.byteOffset+e.byteLength));if(0===s.length)return;const t=s[0];t===MSG_AWARENESS&&this.onAwareness?this.onAwareness(s):t===MSG_SYNC&&this.onUpdate&&this.onUpdate(s)}),n.on("close",()=>{this.opts.logger?.info?.(`[canvas-ws] Disconnected from canvas ${this.canvasId}`),this.ws=null,this.scheduleReconnect()}),n.on("error",e=>{this.opts.logger?.warn?.("[canvas-ws] Error:",e.message),n.close()})}scheduleReconnect(){if(this.closed)return;const e=Math.min(1e3*Math.pow(2,this.reconnectAttempts),3e4);this.reconnectAttempts++,this.opts.logger?.debug?.(`[canvas-ws] Reconnecting in ${e}ms (attempt ${this.reconnectAttempts})`),this.reconnectTimer=setTimeout(()=>{this.doConnect()},e),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}sendUpdate(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}sendAwareness(e){this.ws&&this.ws.readyState===WebSocket.OPEN?this.ws.send(e):this.enqueuePending(e)}enqueuePending(e){this.pendingMessages.length>=500&&(this.pendingMessages.shift(),this.opts.logger?.warn?.("[canvas-ws] pendingMessages exceeded 500, dropping oldest")),this.pendingMessages.push(e)}disconnect(){this.closed=!0,this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.ws&&(this.ws.close(),this.ws=null),this.canvasId=null,this.pendingMessages=[]}get connected(){return null!==this.ws&&this.ws.readyState===WebSocket.OPEN}get currentCanvasId(){return this.canvasId}}
@@ -1 +1 @@
1
- import{mkdir,writeFile}from"node:fs/promises";import{basename,dirname,isAbsolute,resolve as resolvePathFs}from"node:path";import{agentFetch}from"../agent-auth.js";import{isBeeosFileUri,parseBeeosFileUri}from"./beeos-file-uri.js";export const BEEOS_FETCH_FILE_TOOL_NAME="beeos_fetch_file";export const BEEOS_FETCH_FILE_TOOL_DESCRIPTION="Download a file from the user's BeeOS account to the local workspace by its raw fileId (discover fileIds with `beeos_list_files`). Use only when local byte analysis is required; for display, keep the beeos-file:// URI in chat or Canvas. Returns the local path where the file was saved so you can then read or process it. Any file type is supported.";export const BEEOS_FETCH_FILE_TOOL_SCHEMA={title:"BeeOS fetch account file",type:"object",description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,additionalProperties:!1,required:["fileId"],properties:{fileId:{type:"string",minLength:1,description:"The raw fileId of the account file to download, not a beeos-file:// URI."},destPath:{type:"string",description:"Optional destination path (absolute or workspace-relative). Defaults to the original file name in the current working directory."}}};const REQUEST_TIMEOUT_MS=12e4;export async function executeFetchFile(e,t,r,o){const s=parseParams(e);if(!s.ok)return errorResult(s.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — fetching account files requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign fetch requests");const i=AbortSignal.timeout(12e4),a=o?.signal?AbortSignal.any([o.signal,i]):i,n=`${t.agentGatewayUrl}/api/v1/agent/files/${encodeURIComponent(s.fileId)}/resolve`;let l;try{l=await agentFetch(n,r,{method:"GET",signal:a})}catch(e){return errorResult(`resolve request failed: ${e instanceof Error?e.message:String(e)}`)}if(!l.ok){const e=await l.text().catch(()=>l.statusText);return errorResult(`resolve ${l.status}: ${e}`)}const f=await l.json();if(!f.downloadUrl)return errorResult("resolve returned no download URL");let d;try{d=await fetch(f.downloadUrl,{method:"GET",signal:a})}catch(e){return errorResult(`download failed: ${e instanceof Error?e.message:String(e)}`)}if(!d.ok)return errorResult(`download failed: ${d.status}`);const c=Buffer.from(await d.arrayBuffer()),u=resolveDestPath(s.destPath,f.fileName||s.fileId,o?.resolvePath);try{await mkdir(dirname(u),{recursive:!0}),await writeFile(u,c)}catch(e){return errorResult(`write failed: ${e instanceof Error?e.message:String(e)}`)}const h={ok:!0,path:u,fileName:f.fileName,mimeType:f.mimeType,size:c.byteLength};return{...h,content:[{type:"text",text:`Downloaded ${f.fileName} to ${u} (${c.byteLength}B)`}],output:h,result:h,details:h,isError:!1}}function resolveDestPath(e,t,r){if(e)return isAbsolute(e)?e:r?r(e):resolvePathFs(process.cwd(),e);const o=basename(t)||"download";return r?r(o):resolvePathFs(process.cwd(),o)}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e,r=new Set(["fileId","destPath"]),o=Object.keys(t).filter(e=>!r.has(e));if(o.length>0)return{ok:!1,error:`unexpected properties: ${o.join(", ")}`};if("string"!=typeof t.fileId||""===t.fileId.trim())return{ok:!1,error:"fileId must be a non-empty string"};const s=t.fileId.trim();if(isBeeosFileUri(s)){const e=parseBeeosFileUri(s);return{ok:!1,error:`fileId must be a raw BeeOS fileId, not a beeos-file:// URI.${e.fileId?` Use raw fileId "${e.fileId}" only when local byte analysis is required.`:" Use a raw fileId only when local byte analysis is required."}`}}let i;if(void 0!==t.destPath){if("string"!=typeof t.destPath||""===t.destPath.trim())return{ok:!1,error:"destPath must be a non-empty string"};i=t.destPath.trim()}return{ok:!0,fileId:s,destPath:i}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
1
+ import{mkdir,writeFile}from"node:fs/promises";import{basename,dirname,isAbsolute,resolve as resolvePathFs}from"node:path";import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";import{isBeeosFileUri,parseBeeosFileUri}from"./beeos-file-uri.js";export const BEEOS_FETCH_FILE_TOOL_NAME="beeos_fetch_file";export const BEEOS_FETCH_FILE_TOOL_DESCRIPTION="Download a file from the user's BeeOS account to the local workspace by its raw fileId (discover fileIds with `beeos_list_files`). Use only when local byte analysis is required; for display, keep the beeos-file:// URI in chat or Canvas. Returns the local path where the file was saved so you can then read or process it. Any file type is supported.";export const BEEOS_FETCH_FILE_TOOL_SCHEMA={title:"BeeOS fetch account file",type:"object",description:BEEOS_FETCH_FILE_TOOL_DESCRIPTION,additionalProperties:!1,required:["fileId"],properties:{fileId:{type:"string",minLength:1,description:"The raw fileId of the account file to download, not a beeos-file:// URI."},destPath:{type:"string",description:"Optional destination path (absolute or workspace-relative). Defaults to the original file name in the current working directory."}}};const REQUEST_TIMEOUT_MS=12e4;export async function executeFetchFile(e,t,r){const o=parseParams(e);if(!o.ok)return errorResult(o.error);if(!t)return errorResult("Agent Gateway client unavailable — fetching account files requires agentGatewayUrl and a key pair");const i=AbortSignal.timeout(12e4),s=r?.signal?AbortSignal.any([r.signal,i]):i;let a,n;try{a=await t.files.resolve(o.fileId,{signal:s})}catch(e){return errorResult(e instanceof CloudHttpError?`resolve ${e.status}: ${e.body}`:`resolve request failed: ${e instanceof Error?e.message:String(e)}`)}if(!a.downloadUrl)return errorResult("resolve returned no download URL");try{n=Buffer.from(await t.files.download(a.downloadUrl,s))}catch(e){return errorResult(e instanceof CloudHttpError?`download failed: ${e.status}`:`download failed: ${e instanceof Error?e.message:String(e)}`)}const l=resolveDestPath(o.destPath,a.fileName||o.fileId,r?.resolvePath);try{await mkdir(dirname(l),{recursive:!0}),await writeFile(l,n)}catch(e){return errorResult(`write failed: ${e instanceof Error?e.message:String(e)}`)}const d={ok:!0,path:l,fileName:a.fileName,mimeType:a.mimeType,size:n.byteLength};return{...d,content:[{type:"text",text:`Downloaded ${a.fileName} to ${l} (${n.byteLength}B)`}],output:d,result:d,details:d,isError:!1}}function resolveDestPath(e,t,r){if(e)return isAbsolute(e)?e:r?r(e):resolvePathFs(process.cwd(),e);const o=basename(t)||"download";return r?r(o):resolvePathFs(process.cwd(),o)}function parseParams(e){if(!e||"object"!=typeof e)return{ok:!1,error:"params must be an object"};const t=e,r=new Set(["fileId","destPath"]),o=Object.keys(t).filter(e=>!r.has(e));if(o.length>0)return{ok:!1,error:`unexpected properties: ${o.join(", ")}`};if("string"!=typeof t.fileId||""===t.fileId.trim())return{ok:!1,error:"fileId must be a non-empty string"};const i=t.fileId.trim();if(isBeeosFileUri(i)){const e=parseBeeosFileUri(i);return{ok:!1,error:`fileId must be a raw BeeOS fileId, not a beeos-file:// URI.${e.fileId?` Use raw fileId "${e.fileId}" only when local byte analysis is required.`:" Use a raw fileId only when local byte analysis is required."}`}}let s;if(void 0!==t.destPath){if("string"!=typeof t.destPath||""===t.destPath.trim())return{ok:!1,error:"destPath must be a non-empty string"};s=t.destPath.trim()}return{ok:!0,fileId:i,destPath:s}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
@@ -1 +1 @@
1
- import{agentFetch}from"../agent-auth.js";export const BEEOS_LIST_FILES_TOOL_NAME="beeos_list_files";export const BEEOS_LIST_FILES_TOOL_DESCRIPTION="List files stored in the user's BeeOS account, across all their agents/instances. Use this to discover files the user (or another agent) uploaded or generated earlier, then pass a returned fileId to `beeos_fetch_file` to download it locally. Returns fileId, name, mimeType, size, source and originating instance for each file.";export const BEEOS_LIST_FILES_TOOL_SCHEMA={title:"BeeOS list account files",type:"object",description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,additionalProperties:!1,properties:{query:{type:"string",description:"Optional case-insensitive substring to match against file names."},source:{type:"string",enum:["chat_upload","agent_generated","manual"],description:"Optional filter by file origin."},limit:{type:"integer",minimum:1,maximum:200,description:"Maximum number of files to return (default 50)."}}};const DEFAULT_LIMIT=50,MAX_LIMIT=200,REQUEST_TIMEOUT_MS=3e4;export async function executeListFiles(e,t,r,i){const n=parseParams(e);if(!n.ok)return errorResult(n.error);if(!t.agentGatewayUrl)return errorResult("agentGatewayUrl not configured — listing account files requires Agent Gateway access");if(!r)return errorResult("agent key pair not available — cannot sign list requests");const o=AbortSignal.timeout(3e4),s=i?AbortSignal.any([i,o]):o,a=`${t.agentGatewayUrl}/api/v1/agent/files`;let u;try{u=await agentFetch(a,r,{method:"GET",signal:s})}catch(e){return errorResult(`list request failed: ${e instanceof Error?e.message:String(e)}`)}if(!u.ok){const e=await u.text().catch(()=>u.statusText);return errorResult(`list ${u.status}: ${e}`)}let l=(await u.json()).files??[];if(n.source&&(l=l.filter(e=>e.source===n.source)),n.query){const e=n.query.toLowerCase();l=l.filter(t=>t.fileName.toLowerCase().includes(e))}l=l.slice(0,n.limit);const c=0===l.length?"No matching files found in the account.":`Found ${l.length} file(s):\n`+l.map(e=>`- ${e.fileName} (${e.mimeType}, ${e.size}B, ${e.source}${e.instanceName?`, from ${e.instanceName}`:""}) → fileId=${e.fileId}`).join("\n"),f={ok:!0,files:l};return{...f,content:[{type:"text",text:c}],output:f,result:f,details:f,isError:!1}}function parseParams(e){const t=e&&"object"==typeof e?e:{},r=new Set(["query","source","limit"]),i=Object.keys(t).filter(e=>!r.has(e));if(i.length>0)return{ok:!1,error:`unexpected properties: ${i.join(", ")}`};let n,o;if(void 0!==t.query){if("string"!=typeof t.query)return{ok:!1,error:"query must be a string"};n=t.query.trim()||void 0}if(void 0!==t.source){if("string"!=typeof t.source)return{ok:!1,error:"source must be a string"};const e=["chat_upload","agent_generated","manual"];if(!e.includes(t.source))return{ok:!1,error:`source must be one of ${e.join(", ")}`};o=t.source}let s=50;if(void 0!==t.limit){if("number"!=typeof t.limit||!Number.isInteger(t.limit)||t.limit<1)return{ok:!1,error:"limit must be a positive integer"};s=Math.min(t.limit,200)}return{ok:!0,query:n,source:o,limit:s}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
1
+ import{CloudHttpError}from"@beeos-ai/cloud-harness-sdk";export const BEEOS_LIST_FILES_TOOL_NAME="beeos_list_files";export const BEEOS_LIST_FILES_TOOL_DESCRIPTION="List files stored in the user's BeeOS account, across all their agents/instances. Use this to discover files the user (or another agent) uploaded or generated earlier, then pass a returned fileId to `beeos_fetch_file` to download it locally. Returns fileId, name, mimeType, size, source and originating instance for each file.";export const BEEOS_LIST_FILES_TOOL_SCHEMA={title:"BeeOS list account files",type:"object",description:BEEOS_LIST_FILES_TOOL_DESCRIPTION,additionalProperties:!1,properties:{query:{type:"string",description:"Optional case-insensitive substring to match against file names."},source:{type:"string",enum:["chat_upload","agent_generated","manual"],description:"Optional filter by file origin."},limit:{type:"integer",minimum:1,maximum:200,description:"Maximum number of files to return (default 50)."}}};const DEFAULT_LIMIT=50,MAX_LIMIT=200,REQUEST_TIMEOUT_MS=3e4;export async function executeListFiles(e,t,r){const i=parseParams(e);if(!i.ok)return errorResult(i.error);if(!t)return errorResult("Agent Gateway client unavailable — listing account files requires agentGatewayUrl and a key pair");const o=AbortSignal.timeout(3e4),s=r?AbortSignal.any([r,o]):o;let n;try{n=await t.files.list({signal:s})}catch(e){return errorResult(e instanceof CloudHttpError?`list ${e.status}: ${e.body}`:`list request failed: ${e instanceof Error?e.message:String(e)}`)}let a=n.files??[];if(i.source&&(a=a.filter(e=>e.source===i.source)),i.query){const e=i.query.toLowerCase();a=a.filter(t=>t.fileName.toLowerCase().includes(e))}a=a.slice(0,i.limit);const l=0===a.length?"No matching files found in the account.":`Found ${a.length} file(s):\n`+a.map(e=>`- ${e.fileName} (${e.mimeType}, ${e.size}B, ${e.source}${e.instanceName?`, from ${e.instanceName}`:""}) → fileId=${e.fileId}`).join("\n"),u={ok:!0,files:a};return{...u,content:[{type:"text",text:l}],output:u,result:u,details:u,isError:!1}}function parseParams(e){const t=e&&"object"==typeof e?e:{},r=new Set(["query","source","limit"]),i=Object.keys(t).filter(e=>!r.has(e));if(i.length>0)return{ok:!1,error:`unexpected properties: ${i.join(", ")}`};let o,s;if(void 0!==t.query){if("string"!=typeof t.query)return{ok:!1,error:"query must be a string"};o=t.query.trim()||void 0}if(void 0!==t.source){if("string"!=typeof t.source)return{ok:!1,error:"source must be a string"};const e=["chat_upload","agent_generated","manual"];if(!e.includes(t.source))return{ok:!1,error:`source must be one of ${e.join(", ")}`};s=t.source}let n=50;if(void 0!==t.limit){if("number"!=typeof t.limit||!Number.isInteger(t.limit)||t.limit<1)return{ok:!1,error:"limit must be a positive integer"};n=Math.min(t.limit,200)}return{ok:!0,query:o,source:s,limit:n}}function errorResult(e){const t={ok:!1,error:e};return{...t,content:[{type:"text",text:e}],output:t,result:t,details:t,isError:!0}}
@@ -1 +1 @@
1
- import{extractChatPrompt}from"./chat-envelope.js";import{buildMentionInstruction,parseMentionsParam}from"./delegation/mention-resolver.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>"string"==typeof e.conversationId?e.conversationId:"";export const createImInboundHandler=e=>{const{runtime:t,pluginConfig:n,client:s,localServer:o,promptRunStore:a,waitIfExclusive:r,assistantMessageCommitRouter:i,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,logger:m,writeObs:p,principalIdOverride:g,dispatch:u=dispatchImInboundPrompt,completeA2ATask:I,activeRunAuthority:h}=e;return{onChatMessage:async e=>{const _=e.content??{},v=_.metadata??void 0,f="string"==typeof v?.target_agent_id?v.target_agent_id:"",y=f?o.resolveLocalAgentId?.(f):void 0;if(!f||!y)return void m.warn?.(`[beeos-claw] chat_message target_agent_id=${f||"missing"} not resolvable to a local agent — drop`);const b=extractChatPrompt(e,{source:"chat_message",localAgentId:y});if(!b)return;if(!b.channelId||!b.messageId)return void m.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${b.sessionKey}) — drop`);const $=g||o.getDefaultPlatformAgent()?.instanceId||"";if(!$)return void m.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${b.sessionKey})`);const w="string"==typeof v?.delivery_principal?v.delivery_principal:"";if(w&&w!==$)return void m.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${w}, selfPrincipalId=${$}, message_id=${b.messageId})`);const R=parseMentionsParam(_.mentions);o.ensureImInboundSession(b.sessionKey,b.channelId,b.messageId,R);const C=buildMentionInstruction(R),M=C?`${C}\n${b.message}`:b.message,K=resolveOrigin(v?.protocol),O=b.messageId,A="a2a"===K&&I?(e,t)=>I(O,e,t):void 0;m.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${b.sessionKey}, channelId=${b.channelId}, origin=${K})`);const P=e=>{p({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await u({runtime:t,pluginConfig:n,prompt:{channelId:b.channelId,inboundMessageId:b.messageId,requestId:e.requestId,selfPrincipalId:$,dispatchInput:{message:M,attachments:b.files},sessionKey:b.sessionKey},client:s,logger:m,promptRunStore:a,...r?{waitIfExclusive:r}:{},assistantMessageCommitRouter:i,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,abortTransport:o,origin:K,onLifecycleEvent:P,...A?{onTerminalResult:A}:{},activeRunAuthority:h})}catch(e){m.error?.(`[beeos-claw] chat_message dispatch failed channelId=${b.channelId} in_reply_to=${b.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async e=>{const t=e.content??{},n="string"==typeof t.target_message_id?t.target_message_id:"",s="string"==typeof t.reason?t.reason:"unknown";if(!n)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${s})`);const o=conversationIdFromMessage(e);if(!o)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${n}, reason=${s})`);const r=`chat_cancel:${s}`,i=await a.abort(promptRunKey(o,n),r);i.ok?m.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${n}, requestId=${i.requestId}, channelId=${i.channelId}, reason=${s})`):m.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${n}, reason=${s}, why=${i.reason})`)}}};
1
+ import{extractChatPrompt}from"./chat-envelope.js";import{buildMentionInstruction,parseMentionsParam}from"./delegation/mention-resolver.js";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{decodeWake}from"./automation/wake.js";const MAX_PROTOCOL_MARKER_LEN=64,DEFAULT_ORIGIN="im",resolveOrigin=e=>"string"!=typeof e||0===e.length||e.length>64?"im":e,conversationIdFromMessage=e=>"string"==typeof e.conversationId?e.conversationId:"";export const createImInboundHandler=e=>{const{runtime:n,pluginConfig:t,client:o,localServer:a,promptRunStore:s,waitIfExclusive:i,assistantMessageCommitRouter:r,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,logger:m,writeObs:p,principalIdOverride:g,dispatch:u=dispatchImInboundPrompt,completeA2ATask:I,activeRunAuthority:h}=e;return{onChatMessage:async _=>{const v=decodeWake(_);if("invalid"===v.kind||"wake"===v.kind&&!e.automationTurnPolicy?.isReady())return void m.warn?.("[beeos-claw] Automation Wake rejected: invalid contract or unavailable turn policy");const y=_.content??{},f=y.metadata??void 0,b="string"==typeof f?.target_agent_id?f.target_agent_id:"",w=b?a.resolveLocalAgentId?.(b):void 0;if(!b||!w)return void m.warn?.(`[beeos-claw] chat_message target_agent_id=${b||"missing"} not resolvable to a local agent — drop`);const $=extractChatPrompt(_,{source:"chat_message",localAgentId:w});if(!$)return;if(!$.channelId||!$.messageId)return void m.warn?.(`[beeos-claw] chat_message missing channelId/messageId (sessionKey=${$.sessionKey}) — drop`);const R=g||a.getDefaultPlatformAgent()?.instanceId||"";if(!R)return void m.warn?.(`[beeos-claw] chat_message dropped (no principalId resolved, sessionKey=${$.sessionKey})`);const C="string"==typeof f?.delivery_principal?f.delivery_principal:"";if(C&&C!==R)return void m.info?.(`[beeos-claw] chat_message skipped (delivery_principal=${C}, selfPrincipalId=${R}, message_id=${$.messageId})`);const M=parseMentionsParam(y.mentions);a.ensureImInboundSession($.sessionKey,$.channelId,$.messageId,M);const k=buildMentionInstruction(M),K=k?`${k}\n${$.message}`:$.message,O=resolveOrigin(f?.protocol),P=$.messageId,A="a2a"===O&&I?(e,n)=>I(P,e,n):void 0;m.debug?.(`[beeos-claw] chat_message dispatch (sessionKey=${$.sessionKey}, channelId=${$.channelId}, origin=${O})`);const T=e=>{p({component:"plugin",domain:"im_dispatch",name:`im.dispatch.${e.type}`,severity:"dispatch_error"===e.type?"warn":"info",payload:e})};try{await u({runtime:n,pluginConfig:t,prompt:{channelId:$.channelId,inboundMessageId:$.messageId,requestId:_.requestId,selfPrincipalId:R,dispatchInput:{message:K,attachments:$.files},sessionKey:$.sessionKey},client:o,logger:m,promptRunStore:s,...i?{waitIfExclusive:i}:{},assistantMessageCommitRouter:r,toolResultObservationRouter:c,modelCallObservationRouter:d,sessionMutationCoordinator:l,abortTransport:a,origin:O,onLifecycleEvent:T,...A?{onTerminalResult:A}:{},activeRunAuthority:h,..."wake"===v.kind?{enterTurn:(n,t)=>e.automationTurnPolicy.enter(n,t,v.context)}:{}})}catch(e){m.error?.(`[beeos-claw] chat_message dispatch failed channelId=${$.channelId} in_reply_to=${$.messageId} error_code=runtime_dispatch_failed`)}},onChatCancel:async e=>{const n=e.content??{},t="string"==typeof n.target_message_id?n.target_message_id:"",o="string"==typeof n.reason?n.reason:"unknown";if(!t)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no target_message_id, reason=${o})`);const a=conversationIdFromMessage(e);if(!a)return void m.warn?.(`[beeos-claw] chat_cancel dropped (no conversation identity, target_message_id=${t}, reason=${o})`);const i=`chat_cancel:${o}`,r=await s.abort(promptRunKey(a,t),i);r.ok?m.info?.(`[beeos-claw] chat_cancel accepted (target_message_id=${t}, requestId=${r.requestId}, channelId=${r.channelId}, reason=${o})`):m.info?.(`[beeos-claw] chat_cancel no-op (target_message_id=${t}, reason=${o}, why=${r.reason})`)}}};