beeos-claw 0.6.63-cloud.2 → 0.6.63-cloud.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-server.js +1 -1
- package/dist/agent-auth.js +1 -1
- package/dist/canvas-orchestrator.js +1 -1
- package/dist/chat-envelope.js +1 -0
- package/dist/config.js +1 -1
- package/dist/im-inbound-handler.js +1 -1
- package/dist/im-runtime-dispatch.js +1 -1
- package/dist/index.js +1 -1
- package/dist/message-client-composition.js +1 -1
- package/dist/message-projection-inbound-consumer.js +1 -1
- package/dist/runtime/callback-queue.js +1 -1
- package/dist/runtime/chat-handler-port.js +1 -0
- package/dist/runtime/conversation-title.js +1 -1
- package/dist/runtime/core-execution-attempt-context.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/dist/runtime/message-service-conversation-clear-http-authority.js +1 -1
- package/dist/runtime/message-service-conversation-run-boundary.js +1 -1
- package/dist/runtime/message-service-runtime-message-channel.js +1 -1
- package/dist/runtime/ms-runtime-message-channel-adapter.js +1 -1
- package/dist/runtime/ms-runtime-message-channel-consumer-factory.js +1 -1
- package/dist/runtime/ms-runtime-rpc-adapter.js +1 -1
- package/dist/runtime/native-cloud-chat-composition.js +1 -0
- package/dist/runtime/native-cloud-prompt-bridge.js +1 -0
- package/dist/runtime/native-input-files-http.js +1 -0
- package/dist/runtime/native-input-files.js +1 -0
- package/dist/runtime/native-output-file-attempt.js +1 -0
- package/dist/runtime/native-prompt-authority.js +1 -0
- package/dist/runtime/native-prompt-preflight.js +1 -0
- package/dist/runtime/native-session-new-bridge.js +1 -0
- package/dist/runtime/reply-router.js +1 -1
- package/dist/runtime/runtime-error-part.js +1 -0
- package/dist/runtime/runtime-local-parity-matrix.js +1 -1
- package/dist/runtime/runtime-operation-resource-resolver.js +1 -1
- package/dist/runtime/runtime-parity-manifest-digest.js +1 -1
- package/dist/runtime/runtime-parity-manifest.js +1 -1
- package/dist/runtime/runtime-production-authority.js +1 -1
- package/dist/runtime/runtime-registration.js +1 -0
- package/dist/runtime/runtime-service-contract-compatibility.js +1 -1
- package/dist/runtime/runtime-service-production-candidate.js +1 -1
- package/dist/runtime/runtime-session-control-service.js +1 -1
- package/dist/runtime/runtime-skills-service.js +1 -1
- package/dist/runtime/runtime-transport-failure-policy.js +1 -0
- package/dist/runtime/service-runtime-attempt-ports-resolver.js +1 -1
- package/dist/runtime/session-new-runtime-delivery-bridge.js +1 -1
- package/dist/runtime/write-runtime-failure.js +1 -1
- package/dist/upload/tool.js +1 -1
- package/dist/upload/upload-file.js +1 -1
- package/openclaw.plugin.json +1 -15
- package/package.json +5 -5
- package/runtime-service-wire-identity.v1.json +5 -5
- package/dist/automation/tools.js +0 -1
- package/dist/automation/turn-policy.js +0 -1
- package/dist/automation/wake.js +0 -1
package/dist/acp-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{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()}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{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]}}
|
package/dist/agent-auth.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as crypto from"node:crypto";import*as fs from"node:fs";import*as path from"node:path";
|
|
1
|
+
import*as crypto from"node:crypto";import*as fs from"node:fs";import*as path from"node:path";export const EMPTY_BODY_SHA256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";const ED25519_PKCS8_PREFIX=Buffer.from("302e020100300506032b657004220420","hex");function wrapPrivateKey(e){return 32===e.length?Buffer.concat([ED25519_PKCS8_PREFIX,Buffer.from(e)]):Buffer.from(e)}const ED25519_SPKI_PREFIX=Buffer.from("302a300506032b6570032100","hex");function derivePublicKey(e){const r=crypto.createPrivateKey({key:wrapPrivateKey(e),format:"der",type:"pkcs8"}),t=crypto.createPublicKey(r).export({type:"spki",format:"der"});return 44===t.length?new Uint8Array(t.subarray(12)):new Uint8Array(t)}export function loadAgentKeyPair(e){if(!fs.existsSync(e))return generateAgentKeyPair(e);const r=fs.readFileSync(e,"utf-8").trim();try{const e=JSON.parse(r);if(e.publicKey&&e.privateKey)return{publicKey:new Uint8Array(Buffer.from(e.publicKey,"base64")),privateKey:new Uint8Array(Buffer.from(e.privateKey,"base64"))}}catch{}const t=new Uint8Array(Buffer.from(r,"base64"));if(32===t.length)return{publicKey:derivePublicKey(t),privateKey:t};throw new Error(`Invalid key file format at ${e}`)}function generateAgentKeyPair(e){const r=crypto.generateKeyPairSync("ed25519",{publicKeyEncoding:{type:"spki",format:"der"},privateKeyEncoding:{type:"pkcs8",format:"der"}}),t=r.publicKey,n=r.privateKey,o=new Uint8Array(44===t.length?t.subarray(12):t),a=new Uint8Array(48===n.length?n.subarray(16):n),i=path.dirname(e);return fs.existsSync(i)||fs.mkdirSync(i,{recursive:!0}),fs.writeFileSync(e,JSON.stringify({publicKey:Buffer.from(o).toString("base64"),privateKey:Buffer.from(a).toString("base64")},null,2),{mode:384}),{publicKey:o,privateKey:a}}export function signAgentMessage(e,r){const t=crypto.createPrivateKey({key:wrapPrivateKey(r),format:"der",type:"pkcs8"});return new Uint8Array(crypto.sign(null,Buffer.from(e),t))}export function bodyHashHex(e){const r=null==e?Buffer.alloc(0):"string"==typeof e?Buffer.from(e,"utf8"):Buffer.from(e);return crypto.createHash("sha256").update(r).digest("hex")}export function agentAuthHeaders(e,r,t,n){const o=Math.floor(Date.now()/1e3).toString(),a=crypto.randomUUID(),i=bodyHashHex(n),f=signAgentMessage(`${e.toUpperCase()}|${r}||${i}|${o}|${a}`,t.privateKey);return{"X-Agent-Public-Key":Buffer.from(t.publicKey).toString("base64"),"X-Agent-Signature":Buffer.from(f).toString("base64"),"X-Agent-Timestamp":o,"X-Agent-Nonce":a}}export function agentBoundAuthHeaders(e,r,t,n){return agentAuthHeaders(e,r,n,t)}export async function agentFetch(e,r,t){const n="string"==typeof e?new URL(e):e,o=t?.method?.toUpperCase()??"GET",a=decodeURIComponent(n.pathname);let i;if(void 0!==t?.body&&null!==t.body){if(!("string"==typeof t.body||t.body instanceof Uint8Array))throw new Error("agent-authenticated request body must be string or Uint8Array for exact signing");i=t.body}const f=agentAuthHeaders(o,a,r,i),s=new Headers(t?.headers);for(const[e,r]of Object.entries(f))s.set(e,r);s.delete("X-Agent-Body-SHA256");const c=await fetch(n,{...t,method:o,headers:s,redirect:"error"});return assertFixedOrigin(n,c),c}export async function agentBoundFetch(e,r,t){if(""!==("string"==typeof e?new URL(e):e).search)throw new Error("body-bound agent authority routes forbid query parameters");return agentFetch(e,r,t)}function assertFixedOrigin(e,r){if(r.url&&new URL(r.url).origin!==e.origin)throw new Error("agent-authenticated response origin changed")}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,i=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=i,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:i,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s);n=await e.ensureCanvas(s,{...a?{linkSessionId:a}:{}}),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const i=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,o=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:i,linkSessionId:o}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0);n.ensureCanvas(s,{linkSessionId:a}).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:i}=e,o=this.canvasState.resolveSessionForCanvas(s);if(!o)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,i,"no_session");const r=this.deps.state.findSessionByKey(o);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${o}`),void this.sendRelayActionError(s,t,i,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,i,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,i,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
|
|
1
|
+
import{CanvasActionRouter,formatActionForAgent}from"./canvas/action-router.js";import{FormSchemaRegistry}from"./surface/form-schema-registry.js";import{FormSurfaceBuilder}from"./surface/form-surface-builder.js";import{ResourceSurfaceRenderer}from"./surface/resource-surface-renderer.js";import{CANVAS_TERM_MAPPING}from"./canvas/tools.js";import{extractBeeosConversationIdFromSessionKey}from"./runtime/openclaw-session-key-resolution.js";let _oclSeq=0;function nextOclId(e){return`${e}-${Date.now()}-${++_oclSeq}`}const STRING_PROPS=new Set(["text","url","label","name","title","description","placeholder","icon","src","alt","value","hint","caption"]);function toPascalCase(e){return e?e[0]>="A"&&e[0]<="Z"?e:e.charAt(0).toUpperCase()+e.slice(1):"Text"}function sanitizeA2UIComponents(e){return Array.isArray(e)?e.map(e=>{if("object"!=typeof e||null===e)return e;const s=e;if("component"in s&&"object"==typeof s.component&&null!==s.component){const e=s.component;for(const[s,n]of Object.entries(e)){if("object"==typeof n&&null!==n){const a=n;for(const[e,s]of Object.entries(a))"string"==typeof s&&STRING_PROPS.has(e)&&(a[e]={literalString:s});e[s]=a}if(s[0]>="a"&&s[0]<="z"){e[toPascalCase(s)]=e[s],delete e[s]}}s.component=e}return s}):e}export class CanvasOrchestrator{canvasState=null;resourceRenderer=null;descriptorRegistry=null;actionRouter=null;formBuilder=null;schemaRegistry=null;pendingCanvasLinks=[];canvasPanelOpen=!1;deps;constructor(e){this.deps=e}updateDeps(e){Object.assign(this.deps,e)}setCanvasState(e,s){this.canvasState=e;const n=async(e,s)=>{if(!this.deps.gateway?.isConnected)throw new Error("Gateway not connected");return this.deps.gateway.request(e,s)},a=()=>{const s=this.deps.getSessionIdForToolCall();if(!s)return;const n=this.deps.state.sessions.get(s),a=n?.gatewaySessionKey;return a?e.resolveCanvasForSession(a):void 0},t=s?.schemaRegistry??new FormSchemaRegistry,o=new FormSurfaceBuilder({canvasState:e,gatewayRequest:n,resolveCanvasId:a,logger:this.deps.logger});this.formBuilder=o,this.schemaRegistry=t,this.descriptorRegistry=s?.descriptorRegistry??null,this.resourceRenderer=new ResourceSurfaceRenderer({canvasState:e,gatewayRequest:n,schemaRegistry:t,formBuilder:o,resolveCanvasId:a,logger:this.deps.logger}),this.actionRouter=new CanvasActionRouter({stateManager:e,logger:this.deps.logger,writeObs:e=>this.deps.writeObs(e),forwardToAgent:e=>this.forwardActionToAgent(e),emitActionAsUserMessage:e=>{const s=this.deps.state.resolveSessionFromParams(e);s?this.emitCanvasActionAsUserMessage(s.sessionId,e):this.deps.logger.warn?.("[canvas-orch] emitActionAsUserMessage: session not found, pill skipped",{surfaceId:e.surfaceId,sessionId:e.sessionId})}})}wireRelayActionHandler(){const e=this.canvasState?.getRelayClient();e?(e.onAction(e=>{this.handleRelayAction(e)}),e.onCanvasControl(e=>{this.handleRelayCanvasControl(e)}),this.deps.logger.debug?.("[canvas-orch] Canvas Relay action+control handlers wired")):this.deps.logger.warn?.("[canvas-orch] wireRelayActionHandler called but no relay client available")}handleRelayCanvasControl(e){if("canvas.toggle"===e.type){const s=!0===e.visible||!0===e.enabled;this.canvasPanelOpen=s;const n={enabled:s,canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}};if(e.sessionId?this.deps.state.resolveSessionFromParams(n):void 0)return void this.handleCanvasNotification("canvas/toggle",n);let a=0;for(const e of this.deps.state.sessionsByGatewayKey.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;for(const e of this.deps.state.sessions.values())e.canvasActive=s,s&&(e.canvasPromptInjected=!1),a++;if(s&&e.sessionId){const s=e.sessionId;for(const e of this.deps.state.sessionsByGatewayKey.values()){if(extractBeeosConversationIdFromSessionKey(e.gatewaySessionKey)===s||e.sessionId===s){this.ensureCanvasForSession(s,e.gatewaySessionKey);break}}}return void this.deps.logger.info?.(`[canvas-orch] Relay canvas.toggle enabled=${s} fan-out sessions≈${a} canvasId=${e.canvasId}`)}"canvas.dimensions"!==e.type?"canvas.reference"!==e.type?"canvas.clear"===e.type&&this.handleCanvasNotification("canvas/clear",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{}}):this.handleCanvasNotification("canvas/reference",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},surfaceId:e.surfaceId??null}):this.handleCanvasNotification("canvas/dimensions",{canvasId:e.canvasId,...e.sessionId?{sessionId:e.sessionId}:{},width:e.width,height:e.height,...void 0===e.devicePixelRatio?{}:{devicePixelRatio:e.devicePixelRatio}})}isCanvasActive(e){if(this.canvasPanelOpen)return!0;if(!e)return!1;const s=this.deps.state.sessions.get(e);return!0===s?.canvasActive}isCanvasActiveForGatewayKey(e){if(this.canvasPanelOpen)return!0;const s=this.deps.state.sessionsByGatewayKey.get(e);return!0===s?.canvasActive}onSessionLoaded(e,s,n){if(this.canvasPanelOpen){const n=this.deps.state.sessionsByGatewayKey.get(s)??this.deps.state.sessions.get(e);n&&(n.canvasActive=!0)}if(!this.deps.config.canvasEnabled)return;const a=this.canvasState?.getRelayClient();if(!a)return this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s}),void this.deps.logger.info?.(`[canvas-orch] Relay not ready, queued canvas link: acpSid=${e} gwKey=${s}`);this.ensureCanvasForSession(e,s)}onSessionDeleted(e){this.canvasState?.removeSession(e)}onSessionCleared(e,s){const n=this.canvasState?.resolveCanvasForSession(s);n&&(this.canvasState.clear(n),this.actionRouter?.reset())}onPromptStart(){this.actionRouter?.clearPending()}async onPromptComplete(e){try{await(this.actionRouter?.drainPending())}catch(e){this.deps.logger.warn?.(`[canvas-orch] Drain pending canvas actions failed: ${e instanceof Error?e.message:e}`)}}onPromptFailed(){this.actionRouter?.clearPending()}flushPendingCanvasLinks(){const e=this.pendingCanvasLinks.splice(0);if(0===e.length)return;const s=new Set,n=e.filter(({gatewaySessionKey:e})=>!s.has(e)&&(s.add(e),!this.canvasState?.resolveCanvasForSession(e)));if(0!==n.length){this.deps.logger.info?.(`[canvas-orch] Flushing ${n.length} pending canvas link(s)`);for(const{acpSessionId:e,gatewaySessionKey:s}of n)this.ensureCanvasForSession(e,s)}}onRelayReconnected(){this.deps.logger.info?.("[canvas-orch] Relay reconnected — retrying pending canvas links"),this.flushPendingCanvasLinks()}async handleSlashCommand(e){const s=this.descriptorRegistry?.getBySlashCommand(e);return!(!s||!this.resourceRenderer)&&(await this.resourceRenderer.render(s,"inline"),!0)}async enrichMessageWithCanvasContext(e,s,n){if(!this.canvasState)return e;if(n.has(s.split(":").slice(0,2).join(":"))||n.has(s))return e;try{let n=this.canvasState.resolveCanvasForSession(s);if(!n){const e=this.canvasState.getRelayClient();if(e)try{const a=extractBeeosConversationIdFromSessionKey(s),t=this.resolveCanvasAgentId(s);n=await e.ensureCanvas(s,{...t?{agentId:t}:{},...a?{linkSessionId:a}:{}}),this.canvasState.registerSession(s,n)}catch(e){this.deps.logger.warn?.(`[canvas-orch] ensureCanvas for prompt inject failed (session may not be linked yet): ${e instanceof Error?e.message:String(e)}`)}}if(n){let s=this.canvasState.buildContextForCanvas(n);s||(await this.canvasState.lazyLoadCanvas(n),s=this.canvasState.buildContextForCanvas(n));return`${CANVAS_TERM_MAPPING}\n\n${s??"[Canvas] Canvas panel is available. Use render_ui for structured/visual content."}\n\n${e}`}}catch(e){this.deps.logger.warn?.(`[canvas-orch] Canvas context injection failed: ${e instanceof Error?e.message:String(e)}`)}return e}handleCanvasNotification(e,s){if(this.canvasState){if("canvas/toggle"===e){const e=s,n=!1!==e.enabled,a=this.deps.state.resolveSessionFromParams(e);if(a&&(a.canvasActive=n,this.canvasPanelOpen=n,n&&(a.canvasPromptInjected=!1,a.sessionId))){const e=a.sessionId,s=a.gatewaySessionKey,n=a.agentId,t=this.canvasState?.getRelayClient();if(t){const o=this.deps.resolvePlatformAgentId(n??"")??a.platformAgentId,i=s&&s!==e?e:void 0;t.ensureCanvas(s??e,{agentId:o,linkSessionId:i}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Failed to pre-create canvas on toggle: ${e}`)})}}this.deps.logger.info?.(`[canvas-orch] Canvas toggled: ${n}`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.toggle",severity:"info",payload:{enabled:n}})}else if("canvas/clear"===e){const e=s,n=this.deps.state.resolveSessionFromParams(e),a=n?this.canvasState.resolveCanvasForSession(n.gatewaySessionKey):void 0,t=a?this.canvasState.getAllActive(a).length:0;a&&this.canvasState.clear(a),this.actionRouter?.reset(),this.deps.logger.info?.(`[canvas-orch] Canvas cleared (${t} surfaces removed)`),this.deps.writeObs({component:"canvas",domain:"canvas",name:"canvas.cleared",severity:"info"})}}else this.deps.logger.warn?.(`[canvas-orch] Canvas notification ${e} ignored — canvasState not initialized`)}handleOpenClawCanvasEvent(e){if(!this.canvasState||!e)return;const s=e,n=s.action;if("a2ui_push"===n||"surface_update"===n){const e=String(s.surfaceId??nextOclId("openclaw")),n=s.components;if(n&&Array.isArray(n)){const a=sanitizeA2UIComponents(n),t=s.canvasId?String(s.canvasId):void 0;this.canvasState.has(e,t)?this.canvasState.update(e,a,t):this.canvasState.create(t??nextOclId("_openclaw"),e,"inline",a,{origin:{kind:"openclaw"}}),this.deps.logger.debug?.(`[canvas-orch] OpenClaw canvas → state + relay (surfaceId=${e})`)}}else if("navigate"===n||"url_open"===n){const e=String(s.url??"");if(e){const n=nextOclId("openclaw-nav"),a=String(s.canvasId??nextOclId("_openclaw_nav")),t=[{id:`${n}-embed`,component:{WebEmbed:{url:{literalString:e},title:{literalString:String(s.title??e)}}}}];this.canvasState.create(a,n,"inline",t,{origin:{kind:"openclaw"}})}}else this.deps.logger.debug?.(`[canvas-orch] Unhandled OpenClaw canvas action: ${n}`)}resolveCanvasAgentId(e,s){const n=this.deps.state.sessionsByGatewayKey.get(e)??(s?this.deps.state.sessions.get(s):void 0);if(n)return this.deps.resolvePlatformAgentId(n.agentId??"")??n.platformAgentId}ensureCanvasForSession(e,s){const n=this.canvasState?.getRelayClient();if(!n)return;const a=extractBeeosConversationIdFromSessionKey(s)??(e!==s?e:void 0),t=this.resolveCanvasAgentId(s,e);n.ensureCanvas(s,{agentId:t,linkSessionId:a}).then(async n=>{this.canvasState?.registerSession(s,n),await(this.canvasState?.joinCanvas(n,s)),this.deps.logger.info?.(`[canvas-orch] Canvas ensured: canvas=${n} acpSid=${e} gwKey=${s} linkSessionId=${a??"none"}`)}).catch(n=>{this.deps.logger.warn?.(`[canvas-orch] Failed to ensure canvas: acpSid=${e} gwKey=${s} linkSessionId=${a??"none"} err=${n}`),this.pendingCanvasLinks.push({acpSessionId:e,gatewaySessionKey:s})})}async forwardActionToAgent(e){const s=this.deps.state.resolveSessionFromParams(e);if(!s)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent dropped — no session resolved");if(this.emitCanvasActionAsUserMessage(s.sessionId,e),!this.deps.gateway?.isConnected)return void this.deps.logger.warn?.("[canvas-orch] forwardActionToAgent: gateway not connected, pill sent but agent forward skipped");if(s.currentRunId&&!s.promptDone)return this.actionRouter?.enqueue(e),void this.deps.logger.info?.(`[canvas-orch] Agent busy, canvas action queued (runId=${s.currentRunId})`);const n=this.canvasState?.summarize(e.surfaceId??""),a=formatActionForAgent(e,n??void 0);try{await this.deps.sendAgentMessage(a,s.gatewaySessionKey)}catch(n){this.deps.logger.warn?.(`[canvas-orch] Agent forward failed: ${n instanceof Error?n.message:n}`),this.deps.transport.sendSessionUpdate(s.sessionId,{sessionUpdate:"agent_message_chunk",content:{type:"text",text:`> _User interacted with "${e.userAction?.name??"unknown"}" on canvas._`}})}}emitCanvasActionAsUserMessage(e,s){const n=s.userAction?.name??"unknown",a=`Clicked "${n}"`;this.deps.transport.sendSessionUpdate(e,{sessionUpdate:"user_message_chunk",content:{type:"text",text:a},metadata:{type:"canvas_action",surfaceId:s.surfaceId,actionName:n,sourceComponentId:s.userAction?.sourceComponentId,actionPayload:s.userAction?.context,actionTimestamp:s.userAction?.timestamp}}),this.deps.logger.info?.(`[canvas-orch] Emitted canvas action pill: session=${e} action=${n} surface=${s.surfaceId}`)}handleRelayAction(e){if(!this.canvasState||!this.actionRouter)return void this.deps.logger.warn?.("[canvas-orch] Relay action ignored — canvas not initialized");const{canvasId:s,surfaceId:n,userAction:a,requestId:t,senderClientId:o}=e,i=this.canvasState.resolveSessionForCanvas(s);if(!i)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — no session for canvas=${s}`),void this.sendRelayActionError(s,t,o,"no_session");const r=this.deps.state.findSessionByKey(i);if(!r)return this.deps.logger.warn?.(`[canvas-orch] Relay action ignored — session not found for gatewayKey=${i}`),void this.sendRelayActionError(s,t,o,"no_session");const c={surfaceId:n,userAction:{name:a.name,sourceComponentId:a.sourceComponentId,timestamp:a.timestamp,context:a.context},sessionId:r.sessionId};this.actionRouter.route(c,s).then(()=>{this.deps.logger.info?.(`[canvas-orch] Relay action routed: surface=${n} request=${t}`),this.sendRelayActionResult(s,t,o,"ok")}).catch(e=>{this.deps.logger.warn?.(`[canvas-orch] Relay action routing failed: ${e instanceof Error?e.message:e}`),this.sendRelayActionError(s,t,o,"processing_failed")})}sendRelayActionResult(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionResult({canvasId:e,requestId:s,targetClientId:n,status:a})}sendRelayActionError(e,s,n,a){this.canvasState?.getRelayClient()?.sendActionError({canvasId:e,requestId:s,targetClientId:n,error:a})}destroy(){this.canvasState?.destroy(),this.actionRouter?.reset(),this.pendingCanvasLinks.length=0,this.canvasState=null,this.actionRouter=null,this.resourceRenderer=null,this.descriptorRegistry=null,this.formBuilder=null,this.schemaRegistry=null}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const A2A_PROTOCOL_FIELDS=Object.freeze(["task_id","skills","artifacts","protocol_version","caller_protocol","correlation_id","callback_url","trace_parent","trace_state","openapi_invocation_id","mcp_request_id","agent_card"]);export function extractChatPrompt(t,e={}){const n=t.content??{},o=n.metadata??{},r=extractMessageText(n);if(!r)return null;const c=pickString(t.conversationId,n.channel_id),i=pickString(n.context_id,o.context_id),s=extractFiles(n),a=pickString(t.id)||void 0,f={message:r,files:s,sessionKey:computeSessionKey({content:n,source:e.source??("agent_request"===t.type?"agent_request":"chat_message"),localAgentId:e.localAgentId??"",contextId:i,channelId:c,generateSuffix:e.generateSuffix??defaultSuffix})};return c&&(f.channelId=c),i&&(f.contextId=i),a&&(f.messageId=a),f}function extractMessageText(t){const e=pickString(t.message);if(e)return e;const n=t.parts;if(Array.isArray(n)){const t=[];for(const e of n)if(e&&"object"==typeof e&&"text"===e.type){const n=e.text;"string"==typeof n&&t.push(n)}if(t.length>0)return t.join("\n")}return""}function extractFiles(t){const e=[],n=t.parts;if(Array.isArray(n))for(const t of n){if(!t||"object"!=typeof t)continue;const n=t.file_url;"string"==typeof n&&n.length>0&&e.push(n)}const o=t.files;if(Array.isArray(o))for(const t of o)if("string"==typeof t&&t.length>0)e.push(t);else if(t&&"object"==typeof t){const n=pickString(t.url,t.uri);n&&e.push(n)}const r=t.attachments;if(Array.isArray(r))for(const t of r){if(!t||"object"!=typeof t)continue;const n=t,o=pickString(n.download_url,n.url,n.uri);o&&e.push(o)}return e}function computeSessionKey(t){const e=pickString(t.content.session_key);if(e)return e;const n=pickString((t.content.metadata&&"object"==typeof t.content.metadata?t.content.metadata:{}).protocol),o=n||("agent_request"===t.source?"a2a":"invoke"),r=`agent:${t.localAgentId}:${o}`;if("chat_message"===t.source||n){if(t.contextId)return`${r}:ctx-${t.contextId}`;if(t.channelId)return`${r}:ch-${t.channelId}`}return`${r}:${t.generateSuffix()}`}function defaultSuffix(){return Math.random().toString(36).slice(2,14)}function pickString(...t){for(const e of t)if("string"==typeof e&&e.length>0)return e;return""}
|
package/dist/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const CURRENT_CONFIG_PROTOCOL=1;function shouldTrustOperationalConfig(e){const r=e._generator;if(!r)return!1;return("number"==typeof r.configProtocol?r.configProtocol:0)>=1}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},a=e.openclawConfig??{},o=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=shouldTrustOperationalConfig(r),l=pick([{source:"pluginConfig",value:trimStr(r.agentGatewayUrl)},{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.AGENT_GATEWAY_URL)}],{source:"default",value:""}),n=r.gateway??{},s=(a.gateway??{}).auth??{},i=pick([...u?[{source:"pluginConfig",value:trimStr(n.url)}]:[],{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),t=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(n.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),c=pick([...u?[{source:"pluginConfig",value:readInt(n.protocol)}]:[]],{source:"default",value:3}),v=pick([...u?[{source:"pluginConfig",value:trimStr(n.clientId)}]:[]],{source:"default",value:`beeos-claw-${process.pid}`}),p=pick([...u?[{source:"pluginConfig",value:trimStr(n.agentId)}]:[]],{source:"default",value:""}),d=r.bridge??{},g=pick([{source:"pluginConfig",value:trimStr(r.keyFile)},{source:"pluginConfig",value:trimStr(d.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),E=pick([{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),f=pick([...u?[{source:"pluginConfig",value:trimStr(d.service)}]:[],{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),m=
|
|
1
|
+
import{mkdirSync,writeFileSync}from"node:fs";import{homedir}from"node:os";import{dirname,join}from"node:path";function trimStr(e){if("string"==typeof e){const r=e.trim();return r.length>0?r:void 0}}function readInt(e){if("number"==typeof e&&Number.isFinite(e))return Math.trunc(e);if("string"==typeof e){const r=parseInt(e,10);return Number.isFinite(r)?r:void 0}}function readBool(e){return"boolean"==typeof e?e:"true"===e||"false"!==e&&void 0}function pick(e,r){for(const r of e)if(void 0!==r.value)return{value:r.value,source:r.source};return r}const CURRENT_CONFIG_PROTOCOL=1;function shouldTrustOperationalConfig(e){const r=e._generator;if(!r)return!1;return("number"==typeof r.configProtocol?r.configProtocol:0)>=1}const DEFAULT_GATEWAY_URL="ws://127.0.0.1:18789",DEFAULT_LOG_DIR=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-logs"),DEFAULT_OPENCLAW_CONFIG_PATH=join(homedir(),".openclaw","openclaw.json");export function resolveConfigWithMeta(e){const r=e.pluginConfig??{},a=e.openclawConfig??{},o=e.openclawConfigPath??DEFAULT_OPENCLAW_CONFIG_PATH,u=shouldTrustOperationalConfig(r),l=pick([{source:"pluginConfig",value:trimStr(r.agentGatewayUrl)},{source:"pluginConfig",value:trimStr(r.platformUrl)},{source:"env",value:trimStr(process.env.AGENT_GATEWAY_URL)}],{source:"default",value:""}),n=r.gateway??{},s=(a.gateway??{}).auth??{},i=pick([...u?[{source:"pluginConfig",value:trimStr(n.url)}]:[],{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_URL)}],{source:"default",value:DEFAULT_GATEWAY_URL}),t=pick([{source:"openclawConfig",value:trimStr(s.token)},{source:"pluginConfig",value:trimStr(n.token)},{source:"env",value:trimStr(process.env.BEEOS_GATEWAY_TOKEN)}],{source:"default",value:""}),c=pick([...u?[{source:"pluginConfig",value:readInt(n.protocol)}]:[]],{source:"default",value:3}),v=pick([...u?[{source:"pluginConfig",value:trimStr(n.clientId)}]:[]],{source:"default",value:`beeos-claw-${process.pid}`}),p=pick([...u?[{source:"pluginConfig",value:trimStr(n.agentId)}]:[]],{source:"default",value:""}),d=r.bridge??{},g=pick([{source:"pluginConfig",value:trimStr(r.keyFile)},{source:"pluginConfig",value:trimStr(d.keyFile)},{source:"env",value:trimStr(process.env.BRIDGE_KEY_FILE)}],{source:"default",value:""}),E=pick([{source:"env",value:trimStr(process.env.BRIDGE_URL)}],{source:"default",value:""}),f=pick([...u?[{source:"pluginConfig",value:trimStr(d.service)}]:[],{source:"env",value:trimStr(process.env.BRIDGE_SERVICE)}],{source:"default",value:"acp"}),m=d.shell??{},_=pick([...u?[{source:"pluginConfig",value:readBool(m.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_SHELL_ENABLED)}],{source:"default",value:!0}),S=r.retry??{},A=pick([...u?[{source:"pluginConfig",value:readInt(S.baseMs)}]:[]],{source:"default",value:1e3}),C=pick([...u?[{source:"pluginConfig",value:readInt(S.maxMs)}]:[]],{source:"default",value:6e5}),O=pick([...u?[{source:"pluginConfig",value:readInt(S.maxAttempts)}]:[]],{source:"default",value:0}),I=r.log??{},T=pick([...u?[{source:"pluginConfig",value:readBool(I.enabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LOG_ENABLED)}],{source:"default",value:!0}),R=pick([...u?[{source:"pluginConfig",value:readBool(I.verbose)}]:[]],{source:"default",value:!1}),B=pick([{source:"pluginConfig",value:trimStr(I.dir)}],{source:"default",value:DEFAULT_LOG_DIR}),y=pick([...u?[{source:"pluginConfig",value:readInt(r.terminalWsPort)}]:[],{source:"env",value:readInt(process.env.BEEOS_TERMINAL_WS_PORT)}],{source:"default",value:18801}),L=(u?trimStr(r.terminalWsAllowedOrigins):void 0)??trimStr(process.env.BEEOS_TERMINAL_WS_ALLOWED_ORIGINS),M=L?L.split(",").map(e=>e.trim()).filter(Boolean):void 0,w=pick([{source:L?"pluginConfig":"default",value:M?.length?M:void 0}],{source:"default",value:["localhost","127.0.0.1","::1"]}),b=!0===readBool(process.env.BEEOS_OPENCLAW_DESKTOP)||"1"===process.env.BEEOS_OPENCLAW_DESKTOP,k=pick([{source:"env",value:readBool(process.env.BEEOS_TERMINAL_SERVICE_ENABLED)},...u&&!0===r.terminalServiceEnabled?[{source:"pluginConfig",value:!0}]:[]],{source:"default",value:b}),N=pick([...u?[{source:"pluginConfig",value:readBool(r.legacyTerminalWsEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_LEGACY_TERMINAL_WS_ENABLED)}],{source:"default",value:!1}),h=pick([...u?[{source:"pluginConfig",value:readInt(r.streamIdleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_IDLE_TIMEOUT_MS)}],{source:"default",value:15e3}),G=pick([...u?[{source:"pluginConfig",value:readInt(r.streamStaleTimeoutMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_STREAM_STALE_TIMEOUT_MS)}],{source:"default",value:6e5}),W=pick([...u?[{source:"pluginConfig",value:readInt(r.cronSignalWindowMs)}]:[],{source:"env",value:readInt(process.env.BEEOS_CRON_SIGNAL_WINDOW_MS)}],{source:"default",value:6e4}),P=pick([{source:"env",value:trimStr(process.env.CANVAS_RELAY_URL)},{source:"pluginConfig",value:trimStr(r.canvasRelayUrl)}],{source:"default",value:"https://canvas-relay.beeos.ai"}),U=pick([...u?[{source:"pluginConfig",value:readBool(r.canvasEnabled)}]:[],{source:"env",value:readBool(process.env.BEEOS_CANVAS_ENABLED)}],{source:"default",value:!0}),D=pick([...u?[{source:"pluginConfig",value:readBool(r.openclawCanvasCompat)}]:[],{source:"env",value:readBool(process.env.BEEOS_OPENCLAW_CANVAS_COMPAT)}],{source:"default",value:!1}),F=r.messageBus??{},x=pick([{source:"pluginConfig",value:trimStr(F.serviceUrl)},{source:"env",value:trimStr(process.env.MESSAGE_SERVICE_URL)}],{source:"default",value:""}),H=pick([{source:"pluginConfig",value:trimStr(F.principalId)},{source:"env",value:trimStr(process.env.BEEOS_PRINCIPAL_ID)}],{source:"default",value:""}),Y=pick([{source:"pluginConfig",value:readBool(F.a2aEnabled)},{source:"env",value:readBool(process.env.BEEOS_A2A_ENABLED)}],{source:"default",value:!0}),j=pick([{source:"pluginConfig",value:readBool(F.enabled)},{source:"env",value:readBool(process.env.BEEOS_MESSAGE_BUS_ENABLED)}],{source:"default",value:!(!x.value&&!l.value)}),$={agentGatewayUrl:l.value.replace(/\/+$/,""),keyFile:g.value,bridge:{url:E.value,service:f.value,mode:trimStr(d.mode),shell:{enabled:_.value}},gateway:{url:i.value,token:t.value,protocol:c.value,clientId:v.value,agentId:p.value},retry:{baseMs:Math.max(0,A.value),maxMs:Math.max(0,C.value),maxAttempts:Math.max(0,O.value)},log:{enabled:T.value,verbose:R.value,dir:B.value},terminalWsPort:y.value,terminalWsAllowedOrigins:w.value,terminalServiceEnabled:k.value,legacyTerminalWsEnabled:N.value,streamIdleTimeoutMs:Math.max(0,h.value),streamStaleTimeoutMs:Math.max(0,G.value),cronSignalWindowMs:Math.max(0,W.value),canvasRelayUrl:P.value.replace(/\/+$/,""),canvasEnabled:U.value,openclawCanvasCompat:D.value,messageBus:{enabled:j.value,serviceUrl:x.value,principalId:H.value,a2aEnabled:Y.value}},V={agentGatewayUrl:l.source,keyFile:g.source,bridge:{url:E.source,service:f.source,shell:{enabled:_.source}},gateway:{url:i.source,token:t.source,protocol:c.source,clientId:v.source,agentId:p.source},retry:{baseMs:A.source,maxMs:C.source,maxAttempts:O.source},log:{enabled:T.source,verbose:R.source,dir:B.source},terminalWsPort:y.source,terminalWsAllowedOrigins:w.source,terminalServiceEnabled:k.source,legacyTerminalWsEnabled:N.source,streamIdleTimeoutMs:h.source,streamStaleTimeoutMs:G.source,cronSignalWindowMs:W.source,canvasRelayUrl:P.source,canvasEnabled:U.source,openclawCanvasCompat:D.source,messageBus:{enabled:j.source,serviceUrl:x.source,principalId:H.source,a2aEnabled:Y.source}},K=[];return $.agentGatewayUrl||K.push({code:"CONFIG_AGENT_GATEWAY_URL_MISSING",severity:"error",message:"agentGatewayUrl is required for file uploads and Agent Gateway callbacks",nextSteps:[`Set plugins.entries.beeos-claw.config.agentGatewayUrl in ${o}`,"Or set the AGENT_GATEWAY_URL environment variable"]}),$.gateway.token||K.push({code:"CONFIG_GATEWAY_TOKEN_MISSING",severity:"warn",message:"gateway.token is empty; Gateway auth may fail if authentication is enabled",nextSteps:[`Set gateway.auth.token in ${o}`,`Or set plugins.entries.beeos-claw.config.gateway.token in ${o}`]}),{config:$,sources:V,validation:K,openclawConfigPath:o}}export function resolveConfig(e){const{config:r,validation:a}=resolveConfigWithMeta({pluginConfig:e}),o=a.find(e=>"error"===e.severity);if(o)throw new Error(`[beeos-claw] ${o.message}`);return r}const MIRROR_PATH=join(process.env.OPENCLAW_HOME||join(homedir(),".openclaw"),"beeos-claw","openclaw.json");export function mirrorOpenClawConfig(e){if(!e||"object"!=typeof e||Array.isArray(e))return{copied:!1,destinationPath:MIRROR_PATH,reason:"config_invalid"};try{return mkdirSync(dirname(MIRROR_PATH),{recursive:!0}),writeFileSync(MIRROR_PATH,JSON.stringify(e,null,2)+"\n",{encoding:"utf8",mode:384}),{copied:!0,destinationPath:MIRROR_PATH}}catch(e){return{copied:!1,destinationPath:MIRROR_PATH,reason:e instanceof Error?e.message:"write_failed"}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{extractChatPrompt}from"
|
|
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 +1 @@
|
|
|
1
|
-
import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{SESSION_RECOVERY_REQUIRED_CODE,SESSION_RECOVERY_REQUIRED_MESSAGE}from"./runtime/session-mutation-coordinator.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";import{writeRuntimeFailure}from"./runtime/write-runtime-failure.js";import{maybeRenameImConversationTitle}from"./runtime/conversation-title.js";import{logChatLatency}from"./chat-latency.js";const normalizePositiveTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},ABORT_TERMINAL_GRACE_MS=1e4,PRE_RUN_LOOP_PROBE_MS=100,PRE_RUN_LOOP_LAG_LOG_MS=500,formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},failMessageStream=async(e,n)=>{void 0!==n.body&&e.setBody(n.body),await e.fail(void 0===n.stopReason?void 0:{stopReason:n.stopReason})};export const deterministicReplyMessageId=(e,n,r,t)=>`${e}_${createHash("sha256").update(`${n}:${r}:${t}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:r,client:t,logger:a,promptRunStore:i,assistantMessageCommitRouter:s,toolResultObservationRouter:o,modelCallObservationRouter:c,sessionMutationCoordinator:d,abortTransport:l,activeRunAuthority:u}=e;if(!hasRuntimeReplyApi(n))return a.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${r.channelId} in_reply_to=${r.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:r.channelId,inboundMessageId:r.inboundMessageId}),!1;const m=r.requestId??r.inboundMessageId,p=randomUUID(),I=Date.now();logChatLatency(a,"claw.inbound.received",m,I,{origin:e.origin??"im",request_id_source:r.requestId?"realtime_correlation":"message_id_fallback"});const g=e.origin??"im",h={protocol:"im",requestId:m,sessionId:r.sessionKey??"agent:main",sourceMessageId:r.inboundMessageId,chatId:r.channelId,origin:g},y=async(n,t)=>{if(e.onTerminalResult)try{await e.onTerminalResult(n,t)}catch(e){a.warn?.(`[im] terminal business callback failed requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}},_=promptRunKey(r.channelId,r.inboundMessageId),R=r.sessionKey??"agent:main",f=new AbortController;let b,E,w;const M=new Promise((e,n)=>{w=n});let $;const S=()=>{void 0!==$&&(clearTimeout($),$=void 0)};if(e.waitIfExclusive)try{await e.waitIfExclusive("im_dispatch",f.signal)}catch(n){return a.warn?.(`[im] runtime dispatch aborted waiting for exclusive mutation requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:n instanceof Error?n.message:String(n),cause:"abort"}),!1}if(!i.register(_,{requestId:m,channelId:r.channelId,requestCancel:async e=>{if(E??=e,f.abort(e),!b)return{ok:!0};const n=await d.requestCancel(b,l);return n.ok?n.terminalObservation.then(e=>{e.ok||w(new Error(SESSION_RECOVERY_REQUIRED_CODE))}):w(new Error(SESSION_RECOVERY_REQUIRED_CODE)),{ok:n.ok}},startedAt:I,sessionKey:R}))return a.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${r.inboundMessageId} requestId=${m} channel=${r.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const v=deterministicReplyMessageId("asst",r.channelId,r.inboundMessageId,r.selfPrincipalId),C={conversationId:r.channelId,clientMessageId:v,idempotencyKey:v,replyTo:e.inReplyTo??r.inboundMessageId,requestId:m,type:"agent_reply",content:{}},q=t.messages.startStream(C);let k,O=!1;try{const e=await q.opened();O="duplicate"===e.outcome,logChatLatency(a,"claw.reply_stream.opened",m,I,{outcome:e.outcome})}catch(n){let s="unconfirmed";try{s=(await t.messages.getEnvelope(r.channelId,v)).state}catch{}return a.warn?.(`[im] runtime reply stream open outcome unknown requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} reply_id=${v} reconciled_state=${s}: ${formatErrMessage(n)}; runtime_not_invoked`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_open_outcome_unknown",channelId:r.channelId,inboundMessageId:r.inboundMessageId}),i.unregister(_),!1}if(O)return a.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:r.channelId,inboundMessageId:r.inboundMessageId}),i.unregister(_),!1;const L=new SerializedCallbackQueue,T=createImReplySink({stream:q,abortSignal:f.signal,callbackQueue:L,logger:a,requestId:m,startedAtMs:I,runId:p,channelId:r.channelId,inboundMessageId:r.inboundMessageId,origin:g,sessionKey:R,onAgentRunStart:e=>{d.bindRun(b,e),void 0!==k&&logChatLatency(a,"claw.runtime.pre_run_completed",m,k)}}),A=interceptAgentRuntimeFailures(T,"im_runtime",e=>a.warn?.(`[im] runtime failure reply suppressed requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} error_code=${e.errorCode}`)),D=A.sink;let F=!1;const U=async e=>{D.dispatcher.markComplete(e);try{await D.dispatcher.waitForIdle()}catch{}},K=()=>{S();const n=normalizePositiveTimeoutMs(e.abortTerminalGraceMs)??1e4;$=setTimeout(()=>{$=void 0,(async()=>{if(q.isTerminated)return;const e=f.signal.reason??E,n="string"==typeof e&&e.startsWith("chat_cancel");if(await U("failure"),!q.isTerminated)try{n?await q.cancel({stopReason:"user_stop"}):await failMessageStream(q,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}),a.warn?.(`[im] abort settlement watchdog terminalized reply requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} user_cancel=${n}`)}catch(e){a.warn?.(`[im] abort settlement watchdog terminal write failed requestId=${m} channel=${r.channelId}: ${formatErrMessage(e)}`)}})()},n)};f.signal.aborted?K():f.signal.addEventListener("abort",K,{once:!0});const N=()=>{if(!f.signal.aborted)return;const e=f.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))};let P,G,Q,j,Y,x,B;const V=async e=>{if(!b)throw new Error("session_mutation_not_acquired");const n=await d.converge(b,e);return n.ok?(P=n.permit,!0):(a.error?.(`[im] session mutation recovery required requestId=${m} sessionKey=${R} reason=${n.recovery.reason}`),G={code:n.recovery.code,reason:n.recovery.reason,sessionKey:n.recovery.sessionKey,operationId:n.recovery.operationId,surface:"message_service",...n.recovery.terminalKind?{terminalKind:n.recovery.terminalKind}:{}},!1)};try{N();const l=d.tryStartInteractive({sessionKey:R,label:"im_chat_message"},"message_service");if(!l.ok){try{await failMessageStream(q,{body:SESSION_BUSY_PUBLIC_MESSAGE,stopReason:"error"})}catch(e){a.warn?.(`[im] session busy terminal write failed channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}return a.warn?.(`[im] session write gate busy sessionKey=${R} channel=${r.channelId} in_reply_to=${r.inboundMessageId} active_operation=${l.activeOperationId??"unknown"}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:"session_busy",cause:"runtime_error"}),!1}if(b=l.token,logChatLatency(a,"claw.session_gate.acquired",m,I),d.bindRun(b,p),j=s.register(R,T),Y=o.register(R,T),x=c.register(p,{requestId:m,startedAtMs:I,logger:a}),u){const e=d.snapshot(R);Q={runtimeConversationUid:r.channelId,runtimeSessionKey:r.channelId,historyGeneration:u.historyGeneration(r.channelId),targetMessageId:r.inboundMessageId,runtimeRunId:p,activeRunRegistrationId:`im:${e?.operationId??p}`},u.activeRuns.register(Q,async()=>{const e=`chat_cancel:activeRun:${Q.activeRunRegistrationId}`,n=await i.abort(_,e);return n.ok||"already_aborted"===n.reason?"accepted":"terminal_committing"===n.reason||"not_found"===n.reason?"already_terminal":"recovery_required"})}N(),a.info?.(`[im] runtime dispatching requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} origin=${g}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId}),k=Date.now(),B=e.enterTurn?.(R,p),logChatLatency(a,"claw.runtime.invoke",m,I);const f=setTimeout(()=>{const e=Date.now()-k-100;e>=500&&logChatLatency(a,"claw.runtime.event_loop_lag",m,k,{lag_ms:e,phase:"pre_run"})},100);f.unref?.();const E=()=>dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:r.dispatchInput,replyRoute:h,replySinks:{im:D},onDispatchInvoked:()=>{F=!0,logChatLatency(a,"claw.runtime.dispatch_returned",m,k)}}),w=B?B.run(E):E(),$=await Promise.race([w,M]).finally(()=>{}),S=A.detectedFailure();if(S)throw new Error(S.diagnosticMessage);if(N(),await U("success"),N(),!i.claimTerminal(_,"completion")){if(N(),"cancelling"===i.phase(_))throw new Error("cancellation in progress");throw new Error("terminal outcome already claimed")}if(!await V("completed"))return await failMessageStream(q,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:"runtime_error",...G?{recovery:G}:{}}),!1;const v=D.dispatcher.getTerminalErrorText?.();if(void 0!==v){let n=!1;const t=classifyAgentRuntimeFailure({raw:v,source:"im_runtime",runtimeKind:"openclaw"});try{await writeRuntimeFailure(q,{body:t.publicMessage,stopReason:messageStopReasonForRuntimeFailure(t),businessErrorCode:t.businessErrorCode})}catch(e){n=!0,a.warn?.(`[im] runtime reply stream error-terminal write outcome unknown requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}return u&&Q&&u.activeRuns.markTerminal(Q,"failed"),a.warn?.(`[im] runtime dispatch ended with an error terminal requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} counts=${JSON.stringify($.counts)} terminalWriteFailed=${n} elapsed_ms=${Date.now()-I}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:"runtime_error_terminal_reply",cause:"runtime_error"}),n||await y("",v),!1}let C=!1;try{await q.finalize({stopReason:"end_turn"})}catch(e){C=!0,a.warn?.(`[im] runtime reply stream finalize outcome unknown requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId}: ${formatErrMessage(e)}`)}u&&Q&&u.activeRuns.markTerminal(Q,"completed");const O={...$.counts,source:T.getSourceCount()};a.info?.(`[im] runtime dispatch completed requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} counts=${JSON.stringify(O)} elapsed_ms=${Date.now()-I}`),logChatLatency(a,"claw.turn.terminal",m,I,{outcome:C?"outcome_unknown":"completed",tool_count:O.tool}),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,counts:O,...C?{terminalWriteFailed:!0}:{}});const L=D.dispatcher.getAccumulatedBody?.()??"";C||await y(L,"");const K="string"==typeof r.dispatchInput.message?r.dispatchInput.message:"";return maybeRenameImConversationTitle({client:t,conversationId:r.channelId,question:K,answer:L,logger:a}),!0}catch(n){await U("failure");const t=f.signal.aborted,s=t?f.signal.reason:void 0,o="cancelling"===i.phase(_),c=s??E,d="string"==typeof c&&c.startsWith("chat_cancel");let l="runtime_error";(t||o)&&(l="abort");const p=o||d||t?"cancellation":"completion";if(!i.claimTerminal(_,p)){if(a.warn?.(`[im] terminal claim raced requestId=${m} channel=${r.channelId}; best-effort envelope terminal if still streaming`),!q.isTerminated)try{d?await q.cancel({stopReason:"user_stop"}):await failMessageStream(q,{body:formatErrMessage(n),stopReason:"error"})}catch(e){a.warn?.(`[im] raced terminal write failed requestId=${m}: ${formatErrMessage(e)}`)}throw n}const g=d?"cancelled":"error";if(u&&Q&&u.activeRuns.markTerminal(Q,"cancelled"===g?"cancelled":"failed"),b&&!await V(g))throw d?await q.cancel({stopReason:"user_stop"}).catch(()=>{}):await failMessageStream(q,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:l,...G?{recovery:G}:{}}),n;const h=classifyAgentRuntimeFailure({raw:n,source:"im_runtime",runtimeKind:"openclaw",requestAborted:d}),R=d?h:A.detectedFailure()??h;let w=!1;try{d?await q.cancel({stopReason:"user_stop"}):await writeRuntimeFailure(q,{body:R.publicMessage,stopReason:messageStopReasonForRuntimeFailure(R),businessErrorCode:R.businessErrorCode})}catch(e){w=!0,a.warn?.(`[im] runtime stream terminal write failed requestId=${m} channel=${r.channelId}: ${formatErrMessage(e)}`)}throw a.warn?.(`[im] runtime dispatch failed requestId=${m} channel=${r.channelId} in_reply_to=${r.inboundMessageId} cause=${l} dispatchInvoked=${F} error_code=${R.errorCode}`),logChatLatency(a,"claw.turn.terminal",m,I,{outcome:l,error_code:R.errorCode}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:r.channelId,inboundMessageId:r.inboundMessageId,error:R.errorCode,cause:l}),d||w||await y("",R.publicMessage),n}finally{F&&!P||B?.close(),S(),x?.(),Y?.(),j?.(),i.unregister(_),F?P&&d.release(P):d.releaseBeforeRuntime(b)}};
|
|
1
|
+
import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";import{classifyAgentRuntimeFailure}from"./runtime/agent-runtime-failure-classifier.js";import{SESSION_RECOVERY_REQUIRED_CODE,SESSION_RECOVERY_REQUIRED_MESSAGE}from"./runtime/session-mutation-coordinator.js";import{interceptAgentRuntimeFailures,messageStopReasonForRuntimeFailure,SESSION_BUSY_PUBLIC_MESSAGE}from"./runtime/im-runtime-failure-policy.js";import{writeRuntimeFailure}from"./runtime/write-runtime-failure.js";import{maybeRenameImConversationTitle}from"./runtime/conversation-title.js";import{logChatLatency}from"./chat-latency.js";const normalizePositiveTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},ABORT_TERMINAL_GRACE_MS=1e4,PRE_RUN_LOOP_PROBE_MS=100,PRE_RUN_LOOP_LAG_LOG_MS=500,formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},failMessageStream=async(e,n)=>{void 0!==n.body&&e.setBody(n.body),await e.fail(void 0===n.stopReason?void 0:{stopReason:n.stopReason})};export const deterministicReplyMessageId=(e,n,t,r)=>`${e}_${createHash("sha256").update(`${n}:${t}:${r}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:t,client:r,logger:a,promptRunStore:i,assistantMessageCommitRouter:o,toolResultObservationRouter:s,modelCallObservationRouter:c,sessionMutationCoordinator:l,abortTransport:d,activeRunAuthority:u}=e;if(!hasRuntimeReplyApi(n))return a.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),!1;const m=t.requestId??t.inboundMessageId,p=e.runtimeRunId??randomUUID();if(!p)throw new Error("runtime run id required");const I=Date.now();logChatLatency(a,"claw.inbound.received",m,I,{origin:e.origin??"im",request_id_source:t.requestId?"realtime_correlation":"message_id_fallback"});const g=e.origin??"im",h={protocol:"im",requestId:m,sessionId:t.sessionKey??"agent:main",sourceMessageId:t.inboundMessageId,chatId:t.channelId,origin:g},y=async(n,r)=>{if(e.onTerminalResult)try{await e.onTerminalResult(n,r)}catch(e){a.warn?.(`[im] terminal business callback failed requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}},_=promptRunKey(t.channelId,t.inboundMessageId),R=t.sessionKey??"agent:main",w=new AbortController;let f,E,b,v,M,S,$,C;const q=new Promise((e,n)=>{C=n});e.onNativeRunCompletion&&q.catch(()=>{});const N=e.onNativeRunCompletion?{chatAbort:(e,n)=>(b=Promise.resolve().then(()=>d.chatAbort(e,n)),b)}:d;let k;const O=()=>{void 0!==k&&(clearTimeout(k),k=void 0)},L=async e=>{if($??=e,w.abort(e),!f)return{ok:!0};const n=await l.requestCancel(f,N);return n.ok?n.terminalObservation.then(e=>{e.ok||C(new Error(SESSION_RECOVERY_REQUIRED_CODE))}):C(new Error(SESSION_RECOVERY_REQUIRED_CODE)),{ok:n.ok}};if(e.waitIfExclusive)try{await e.waitIfExclusive("im_dispatch",w.signal)}catch(n){return a.warn?.(`[im] runtime dispatch aborted waiting for exclusive mutation requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:n instanceof Error?n.message:String(n),cause:"abort"}),!1}if(!i.register(_,{requestId:m,channelId:t.channelId,requestCancel:L,startedAt:I,sessionKey:R}))return a.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${t.inboundMessageId} requestId=${m} channel=${t.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const T=e.retainedStream?.id??deterministicReplyMessageId("asst",t.channelId,t.inboundMessageId,t.selfPrincipalId),A={conversationId:t.channelId,clientMessageId:T,idempotencyKey:T,replyTo:e.inReplyTo??t.inboundMessageId,requestId:m,type:"agent_reply",content:{}},D=e.retainedStream??r.messages.startStream(A);let F,U=!1;try{const e=await D.opened();U="duplicate"===e.outcome,logChatLatency(a,"claw.reply_stream.opened",m,I,{outcome:e.outcome})}catch(n){let o="unconfirmed";try{o=(await r.messages.getEnvelope(t.channelId,T)).state}catch{}return a.warn?.(`[im] runtime reply stream open outcome unknown requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} reply_id=${T} reconciled_state=${o}: ${formatErrMessage(n)}; runtime_not_invoked`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_open_outcome_unknown",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(_),!1}if(U)return a.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(_),!1;const K=()=>{L(e.abortSignal?.reason).catch(e=>{C(e instanceof Error?e:new Error(String(e)))})};e.abortSignal?.addEventListener("abort",K,{once:!0}),e.abortSignal?.aborted&&K();const P=new SerializedCallbackQueue,Q=createImReplySink({stream:D,persistObservedToolResults:void 0!==e.retainedStream,abortSignal:w.signal,callbackQueue:P,...e.onNativeRunCompletion?{onCallbackFailure:e=>P.fail(e)}:{},logger:a,requestId:m,startedAtMs:I,runId:p,channelId:t.channelId,inboundMessageId:t.inboundMessageId,origin:g,sessionKey:R,onAgentRunStart:n=>{l.bindRun(f,n),e.onNativeAgentRunStart?.(n),void 0!==F&&logChatLatency(a,"claw.runtime.pre_run_completed",m,F)}}),G=interceptAgentRuntimeFailures(Q,"im_runtime",e=>a.warn?.(`[im] runtime failure reply suppressed requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} error_code=${e.errorCode}`)),Y=G.sink;let j=!1;const x=async n=>{e.onNativeRunCompletion||Y.dispatcher.markComplete(n);try{e.onNativeRunCompletion&&Y.dispatcher.markComplete(n),await Y.dispatcher.waitForIdle()}catch(n){if(e.onNativeRunCompletion)throw P.fail(n),n}},V=()=>{if(e.onNativeRunCompletion)return;O();const n=normalizePositiveTimeoutMs(e.abortTerminalGraceMs)??1e4;k=setTimeout(()=>{k=void 0,(async()=>{if(D.isTerminated)return;const e=w.signal.reason??$,n="string"==typeof e&&e.startsWith("chat_cancel");if(await x("failure"),!D.isTerminated)try{n?await D.cancel({stopReason:"user_stop"}):await failMessageStream(D,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}),a.warn?.(`[im] abort settlement watchdog terminalized reply requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} user_cancel=${n}`)}catch(e){a.warn?.(`[im] abort settlement watchdog terminal write failed requestId=${m} channel=${t.channelId}: ${formatErrMessage(e)}`)}})()},n)};w.signal.aborted?V():w.signal.addEventListener("abort",V,{once:!0});const B=()=>{if(!w.signal.aborted)return;const e=w.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))};let z,W,J,H,X,Z;const ee=async e=>{if(!f)throw new Error("session_mutation_not_acquired");const n=await l.converge(f,e);return n.ok?(z=n.permit,!0):(a.error?.(`[im] session mutation recovery required requestId=${m} sessionKey=${R} reason=${n.recovery.reason}`),W={code:n.recovery.code,reason:n.recovery.reason,sessionKey:n.recovery.sessionKey,operationId:n.recovery.operationId,surface:"message_service",...n.recovery.terminalKind?{terminalKind:n.recovery.terminalKind}:{}},!1)};try{if(B(),e.onNativeRunCompletion&&!e.nativeActiveRunRegistration)throw new Error("native MS Start registration required");const d=l.tryStartInteractive({sessionKey:R,label:"im_chat_message"},"message_service");if(!d.ok){try{await failMessageStream(D,{body:SESSION_BUSY_PUBLIC_MESSAGE,stopReason:"error"})}catch(e){a.warn?.(`[im] session busy terminal write failed channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}return a.warn?.(`[im] session write gate busy sessionKey=${R} channel=${t.channelId} in_reply_to=${t.inboundMessageId} active_operation=${d.activeOperationId??"unknown"}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"session_busy",cause:"runtime_error"}),!1}if(f=d.token,logChatLatency(a,"claw.session_gate.acquired",m,I),l.bindRun(f,p),H=o.register(R,Q),X=s.register(R,Q),Z=c.register(p,{requestId:m,startedAtMs:I,logger:a}),u){const n=l.snapshot(R);J=e.onNativeRunCompletion?{...e.nativeActiveRunRegistration}:{runtimeConversationUid:t.channelId,runtimeSessionKey:t.channelId,historyGeneration:u.historyGeneration(t.channelId),targetMessageId:t.inboundMessageId,runtimeRunId:p,activeRunRegistrationId:`im:${n?.operationId??p}`},u.activeRuns.register(J,async()=>{const e=`chat_cancel:activeRun:${J.activeRunRegistrationId}`,n=await i.abort(_,e);return n.ok||"already_aborted"===n.reason?"accepted":"terminal_committing"===n.reason||"not_found"===n.reason?"already_terminal":"recovery_required"})}B(),a.info?.(`[im] runtime dispatching requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} origin=${g}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId}),e.onNativeRunCompletion&&e.assertNativeTarget&&(await e.assertNativeTarget(),B()),e.onNativeRunCompletion&&e.prepareNativeModel&&(await e.prepareNativeModel(f),B(),await(e.assertNativeTarget?.()),B()),F=Date.now(),logChatLatency(a,"claw.runtime.invoke",m,I);const w=setTimeout(()=>{const e=Date.now()-F-100;e>=500&&logChatLatency(a,"claw.runtime.event_loop_lag",m,F,{lag_ms:e,phase:"pre_run"})},100);w.unref?.();const S=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:t.dispatchInput,replyRoute:h,replySinks:{im:Y},onDispatchInvoked:n=>{if(j=!0,e.onNativeRunCompletion){E=n;const t=new Promise((e,n)=>{v=e,M=n});t.catch(()=>{}),e.onNativeRunCompletion({runtimeRunId:p,token:f,hostCompletion:n,completion:t})}logChatLatency(a,"claw.runtime.dispatch_returned",m,F)}}),$=e.onNativeRunCompletion?await S:await Promise.race([S,q]).finally(()=>{});e.onNativeRunCompletion&&(await E,await b);const C=G.detectedFailure();if(C)throw new Error(C.diagnosticMessage);if(B(),await x("success"),e.onNativeRunCompletion&&await b,B(),!i.claimTerminal(_,"completion")){if(B(),"cancelling"===i.phase(_))throw new Error("cancellation in progress");throw new Error("terminal outcome already claimed")}if(!await ee("completed"))return await failMessageStream(D,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:"runtime_error",...W?{recovery:W}:{}}),!1;const N=Y.dispatcher.getTerminalErrorText?.();if(void 0!==N){let n=!1;const r=classifyAgentRuntimeFailure({raw:N,source:"im_runtime",runtimeKind:"openclaw"});try{await writeRuntimeFailure(D,{body:r.publicMessage,stopReason:messageStopReasonForRuntimeFailure(r),businessErrorCode:r.businessErrorCode})}catch(e){n=!0,a.warn?.(`[im] runtime reply stream error-terminal write outcome unknown requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}return u&&J&&u.activeRuns.markTerminal(J,"failed"),a.warn?.(`[im] runtime dispatch ended with an error terminal requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} counts=${JSON.stringify($.counts)} terminalWriteFailed=${n} elapsed_ms=${Date.now()-I}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"runtime_error_terminal_reply",cause:"runtime_error"}),n||await y("",N),!1}let k=!1;try{await D.finalize({stopReason:"end_turn"})}catch(e){k=!0,a.warn?.(`[im] runtime reply stream finalize outcome unknown requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}u&&J&&u.activeRuns.markTerminal(J,"completed");const O={...$.counts,source:Q.getSourceCount()};a.info?.(`[im] runtime dispatch completed requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} counts=${JSON.stringify(O)} elapsed_ms=${Date.now()-I}`),logChatLatency(a,"claw.turn.terminal",m,I,{outcome:k?"outcome_unknown":"completed",tool_count:O.tool}),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,counts:O,...k?{terminalWriteFailed:!0}:{}});const L=Y.dispatcher.getAccumulatedBody?.()??"";k||await y(L,"");const T="string"==typeof t.dispatchInput.message?t.dispatchInput.message:"";return maybeRenameImConversationTitle({client:r,conversationId:t.channelId,question:T,answer:L,logger:a}),!0}catch(n){S=n,e.onNativeRunCompletion&&(await(E?.then(()=>{},()=>{})),await b),await x("failure"),e.onNativeRunCompletion&&await b;const r=w.signal.aborted,o=r?w.signal.reason:void 0,s="cancelling"===i.phase(_),c=o??$,l="string"==typeof c&&c.startsWith("chat_cancel");let d="runtime_error";(r||s)&&(d="abort");const p=s||l||r?"cancellation":"completion";if(!i.claimTerminal(_,p)){if(a.warn?.(`[im] terminal claim raced requestId=${m} channel=${t.channelId}; best-effort envelope terminal if still streaming`),!D.isTerminated)try{l?await D.cancel({stopReason:"user_stop"}):await failMessageStream(D,{body:formatErrMessage(n),stopReason:"error"})}catch(e){a.warn?.(`[im] raced terminal write failed requestId=${m}: ${formatErrMessage(e)}`)}throw n}const g=l?"cancelled":"error";if(u&&J&&!e.onNativeRunCompletion&&u.activeRuns.markTerminal(J,"cancelled"===g?"cancelled":"failed"),f&&!await ee(g))throw u&&J&&e.onNativeRunCompletion&&u.activeRuns.markTerminal(J,"cancelled"===g?"cancelled":"failed"),l?await D.cancel({stopReason:"user_stop"}).catch(()=>{}):await failMessageStream(D,{body:SESSION_RECOVERY_REQUIRED_MESSAGE,stopReason:"error"}).catch(()=>{}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:SESSION_RECOVERY_REQUIRED_CODE,cause:d,...W?{recovery:W}:{}}),n;u&&J&&e.onNativeRunCompletion&&u.activeRuns.markTerminal(J,"cancelled"===g?"cancelled":"failed");const h=classifyAgentRuntimeFailure({raw:n,source:"im_runtime",runtimeKind:"openclaw",requestAborted:l}),R=l?h:G.detectedFailure()??h;let v=!1;try{l?await D.cancel({stopReason:"user_stop"}):await writeRuntimeFailure(D,{body:R.publicMessage,stopReason:messageStopReasonForRuntimeFailure(R),businessErrorCode:R.businessErrorCode})}catch(e){v=!0,a.warn?.(`[im] runtime stream terminal write failed requestId=${m} channel=${t.channelId}: ${formatErrMessage(e)}`)}throw a.warn?.(`[im] runtime dispatch failed requestId=${m} channel=${t.channelId} in_reply_to=${t.inboundMessageId} cause=${d} dispatchInvoked=${j} error_code=${R.errorCode}`),logChatLatency(a,"claw.turn.terminal",m,I,{outcome:d,error_code:R.errorCode}),e.onLifecycleEvent?.({type:"dispatch_error",requestId:m,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:R.errorCode,cause:d}),l||v||await y("",R.publicMessage),n}finally{e.onNativeRunCompletion&&(z?v?.(z):(j&&f&&l.requireRecovery(f,"terminal_not_observed"),M?.(S??new Error(SESSION_RECOVERY_REQUIRED_CODE)))),e.abortSignal?.removeEventListener("abort",K),O(),Z?.(),X?.(),H?.(),i.unregister(_),j?z&&l.release(z):l.releaseBeforeRuntime(f)}};
|