beeos-claw 0.5.76 → 0.5.78
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-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{AgentGatewayHttpProjectionTransport,AgentProjectionProduction,OpenClawAgentProjectionSource}from"./runtime/agent-projection-production.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";import{MessageClient}from"@beeos-ai/message-sdk";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpToolBridge;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;messageClient;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r)):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setMessageClient(e,t){this.messageClient.setConfig(e),this.msgPrincipalId=t,this.logger.info?.(`[acp-server] MessageEnvelope v3 streamer ready (principal=${t})`)}getMessageClient(){return this.messageClient}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}ensureImInboundSession(e,t,i){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,n,r,s,a,o,c,l,h,g,d){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpToolBridge=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities={canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger),this.messageClient=new MessageClient({logger:this.logger});const m=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:m,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.runtimeAuthority?()=>this.agentProjection?.markDirty():void 0,getAgentProjection:this.runtimeAuthority?()=>this.agentProjection:void 0,mcpToolBridge:this.mcpToolBridge,mcpCredentialReferences:this.mcpCredentialReferences}),this.runtimeAuthority&&this.agentFetchFn&&this.config.agentGatewayUrl&&(this.agentProjection=new AgentProjectionProduction({authority:this.runtimeAuthority,stateFile:join(this.runtimeAuthority.stateDirectory,"runtime-agent-projection.json"),journal:this.runtimeAuthority.journal,identities:this.runtimeAuthority.identities,source:new OpenClawAgentProjectionSource(()=>this.gateway),transport:new AgentGatewayHttpProjectionTransport({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch,getRuntimeLeaseCredential:e=>{const t=this.runtimeAuthority?.authorityLease;if(!t||t.instanceId!==e.instanceId||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new Error("agent projection fenced by current runtime lease");return t.runtimeLeaseCredential},onAccepted:e=>{e.size>0&&this.core.setAgentMapping(e)}}),capabilities:this.runtimeAgentCapabilities,logger:this.logger})),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,i)=>this.core.failPrompt(e,t,i),interruptPrompt:(e,t)=>this.core.interruptPrompt(e,t),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient,getMsgPrincipalId:()=>this.msgPrincipalId??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1,projectionStarted:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getLease:()=>i.authorityLease,signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced");if(!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}this.agentProjection&&(e.projectionStarted=!0,this.agentProjection.start());const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(!this.shouldContinueStart(e))return;if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");const c=Date.now();if(await this.connectGateway(e),this.shouldContinueStart(e)){this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-c}ms`);try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){if(a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-s}ms`)}}}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",i),this.agentProjection?this.agentProjection.markDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentProjection)return void(this.runtimeAuthority?.authorityLease&&this.agentProjection.onGatewayReconnect());if(this.agentSyncInFlight)return;const t=[1e3,2e3,5e3,1e4],i=()=>{const i=e+1,n=t[Math.min(e,t.length-1)],r=Math.round(n/1e3),s=`[acp-server] agent sync retry #${i} in ${r}s`;i>0&&i%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${i} attempts — retrying every ${r}s`):i<=t.length?this.logger.info?.(s):this.logger.debug?.(s),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(i)},n)},n=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.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-n}ms)`)):i()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),i()}finally{this.agentSyncInFlight=!1}}async connectGateway(e){const t=this.config.gateway,i=new GatewayClient({url:t.url,token:t.token||"",clientId:t.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}});if(e&&!this.shouldContinueStart(e))return i.stop(),void i.removeAllListeners();e&&(e.gateway=i),this.gateway=i,i.on("chat",e=>this.events.handleChatEvent(e)),i.on("agent",e=>this.events.handleAgentEvent(e)),i.on("cron",e=>this.events.handleCronEvent(e)),i.on("sessionMessage",e=>this.core.handleSessionMessage(e)),i.on("response",e=>this.core.handleGatewayResponse(e));const n=i;this.canvasOrch.updateDeps({gateway:n?{get isConnected(){return n.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,i)=>this.gateway.agentSend(e,t,i)}:null}),this.config.openclawCanvasCompat&&(i.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),i.on("connected",()=>{this.agentProjection?.onGatewayReconnect(),this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),i.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),i.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),i.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),i.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),i.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),i.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),i.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{if(e){const t=await this.waitForGeneration(i.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Gateway connection timed out")}else await i.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}onAgentMappingUpdated(e){this.core.onAgentMappingUpdated(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.core.agentMapping;return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),e.projectionStarted&&(e.projectionStarted=!1,o.push(a("agent-projection",()=>this.agentProjection?.stop()))),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-desired-state",()=>this.core.stopMcp()));const c=e.gateway,l=e.bridge,h=e.bridgeSocket,g=e.terminalOutbound,d=e.runtimeConsumer,m=this.client&&this.client!==h?this.client:null,u=this.wss;if(e.gateway=void 0,e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.gateway===c&&(this.gateway=null),this.bridgeInstance===l&&(this.bridgeInstance=null),this.client!==m&&this.client!==h||(this.client=null),this.terminalOutbound===g&&(this.terminalOutbound=null),this.runtimeRpcConsumer===d&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,c&&o.push(a("gateway",async()=>{try{await c.stop()}finally{c.removeAllListeners()}})),m&&o.push(a("incoming-client",()=>{m.removeAllListeners(),m.close()})),l&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(l)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),g&&o.push(a("terminal-outbound",e=>g.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),d&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(d,e))),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{GatewayClient}from"./gateway-client.js";import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{AgentGatewayHttpProjectionTransport,AgentProjectionProduction,OpenClawAgentProjectionSource}from"./runtime/agent-projection-production.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";import{MessageClient}from"@beeos-ai/message-sdk";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpToolBridge;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway=null;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;messageClient;msgPrincipalId=null;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.("[acp-server] Deferred agentSend failed: %s",e)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r)):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setMessageClient(e,t){this.messageClient.setConfig(e),this.msgPrincipalId=t,this.logger.info?.(`[acp-server] MessageEnvelope v3 streamer ready (principal=${t})`)}getMessageClient(){return this.messageClient}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}ensureImInboundSession(e,t,i){const n=this.core.ensureSessionForKey(e);n.channelId=t,n.inboundMessageId=i}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,n,r,s,a,o,c,l,h,g,d){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpToolBridge=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities={canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger),this.messageClient=new MessageClient({logger:this.logger});const m=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:m,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.runtimeAuthority?()=>this.agentProjection?.markDirty():void 0,getAgentProjection:this.runtimeAuthority?()=>this.agentProjection:void 0,mcpToolBridge:this.mcpToolBridge,mcpCredentialReferences:this.mcpCredentialReferences}),this.runtimeAuthority&&this.agentFetchFn&&this.config.agentGatewayUrl&&(this.agentProjection=new AgentProjectionProduction({authority:this.runtimeAuthority,stateFile:join(this.runtimeAuthority.stateDirectory,"runtime-agent-projection.json"),journal:this.runtimeAuthority.journal,identities:this.runtimeAuthority.identities,source:new OpenClawAgentProjectionSource(()=>this.gateway),transport:new AgentGatewayHttpProjectionTransport({agentGatewayUrl:this.config.agentGatewayUrl,boundAgentFetch:this.runtimeAuthority.boundAgentFetch,getRuntimeLeaseCredential:e=>{const t=this.runtimeAuthority?.authorityLease;if(!t||t.instanceId!==e.instanceId||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new Error("agent projection fenced by current runtime lease");return t.runtimeLeaseCredential},onAccepted:e=>{e.size>0&&this.core.setAgentMapping(e)}}),capabilities:this.runtimeAgentCapabilities,logger:this.logger})),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,getDefaultAgentId:()=>this.core.getDefaultAgentId(),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,completePrompt:(e,t)=>this.core.completePrompt(e,t),failPrompt:(e,t,i)=>this.core.failPrompt(e,t,i),interruptPrompt:(e,t)=>this.core.interruptPrompt(e,t),schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},maybeCompletePromptFromChatState:(e,t)=>this.core.maybeCompletePromptFromChatState(e,t),agentFetch:this.agentFetchFn,getMessageClient:()=>this.messageClient,getMsgPrincipalId:()=>this.msgPrincipalId??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)})}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1,projectionStarted:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getLease:()=>i.authorityLease,signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}this.agentProjection&&(e.projectionStarted=!0,this.agentProjection.start());const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(!this.shouldContinueStart(e))return;if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");const c=Date.now();if(await this.connectGateway(e),this.shouldContinueStart(e)){this.logger.info?.(`[acp-server] connectGateway() took ${Date.now()-c}ms`);try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){if(a&&o&&BridgeClientCtor){Date.now();await this.startBridgeMode(a,o,e)}else this.logger.warn?.("[acp-server] Bridge not configured; running in standby (no ACP listener). Use handleUpgrade() for noServer mode."),this.core.markAgentSyncDone();this.logger.info?.(`[acp-server] start() total ${Date.now()-s}ms`)}}}shouldContinueStart(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&"starting"===this.lifecyclePhase}isGenerationActive(e){return!e.abortController.signal.aborted&&this.activeGeneration===e&&("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)}setTerminalServiceReady(e,t){if(this.activeGeneration!==e)return;const i=!0===this.config.terminalServiceEnabled&&t;this.terminalServiceReady!==i&&(this.terminalServiceReady=i,this.runtimeAgentCapabilities.terminal=i,this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",i),this.agentProjection?this.agentProjection.markDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return void this.agentProjection.onGatewayReconnect();if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}if(this.agentSyncInFlight)return;const t=[1e3,2e3,5e3,1e4],i=()=>{const i=e+1,n=t[Math.min(e,t.length-1)],r=Math.round(n/1e3),s=`[acp-server] agent sync retry #${i} in ${r}s`;i>0&&i%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${i} attempts — retrying every ${r}s`):i<=t.length?this.logger.info?.(s):this.logger.debug?.(s),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(i)},n)},n=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.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync took ${Date.now()-n}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")):i()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),i()}finally{this.agentSyncInFlight=!1}}async connectGateway(e){const t=this.config.gateway,i=new GatewayClient({url:t.url,token:t.token||"",clientId:t.clientId,openclawCanvasCompat:!!this.config.openclawCanvasCompat,retry:{baseMs:this.config.retry.baseMs,maxMs:this.config.retry.maxMs,maxAttempts:this.config.retry.maxAttempts}});if(e&&!this.shouldContinueStart(e))return i.stop(),void i.removeAllListeners();e&&(e.gateway=i),this.gateway=i,i.on("chat",e=>this.events.handleChatEvent(e)),i.on("agent",e=>this.events.handleAgentEvent(e)),i.on("cron",e=>this.events.handleCronEvent(e)),i.on("sessionMessage",e=>this.core.handleSessionMessage(e)),i.on("response",e=>this.core.handleGatewayResponse(e));const n=i;this.canvasOrch.updateDeps({gateway:n?{get isConnected(){return n.isConnected},request:(e,t)=>this.gateway.request(e,t),agentSend:(e,t,i)=>this.gateway.agentSend(e,t,i)}:null}),this.config.openclawCanvasCompat&&(i.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),i.on("connected",()=>{this.agentProjection?.onGatewayReconnect(),this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),i.on("disconnected",()=>{this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),i.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),i.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),i.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),i.on("transport",e=>{this.logger.info?.(`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),i.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),i.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")});try{if(e){const t=await this.waitForGeneration(i.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Gateway connection timed out")}else await i.start()}catch(e){this.logger.error?.("[acp-server] Initial Gateway connect failed:",e)}}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.core.waitForAgentMapping()}onceAgentMappingReady(e){this.core.onceAgentMappingReady(e)}onAgentMappingUpdated(e){this.core.onAgentMappingUpdated(e)}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.core.agentMapping;if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.core.agentMapping;return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),e.projectionStarted&&(e.projectionStarted=!1,o.push(a("agent-projection",()=>this.agentProjection?.stop()))),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-desired-state",()=>this.core.stopMcp()));const c=e.gateway,l=e.bridge,h=e.bridgeSocket,g=e.terminalOutbound,d=e.runtimeConsumer,m=this.client&&this.client!==h?this.client:null,u=this.wss;if(e.gateway=void 0,e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.gateway===c&&(this.gateway=null),this.bridgeInstance===l&&(this.bridgeInstance=null),this.client!==m&&this.client!==h||(this.client=null),this.terminalOutbound===g&&(this.terminalOutbound=null),this.runtimeRpcConsumer===d&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,c&&o.push(a("gateway",async()=>{try{await c.stop()}finally{c.removeAllListeners()}})),m&&o.push(a("incoming-client",()=>{m.removeAllListeners(),m.close()})),l&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(l)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),g&&o.push(a("terminal-outbound",e=>g.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),d&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(d,e))),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 +1 @@
|
|
|
1
|
-
import{randomUUID}from"node:crypto";import{verifyAndCreateServiceCoreExecutionAttempt}from"./core-execution-attempt-context.js";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{RuntimeClaimAuthorityProtocolError}from"./message-service-runtime-claim-authority.js";import{sessionSetModelMappingMatches}from"./session-set-model-mapping.js";export class MsRuntimeRpcAdapter{dispatcher;authority;authorizationVerifier;attemptPorts;authorizationJournal;getLease;options;controller;running;constructor(e,t,r,a,n,o,i){this.dispatcher=e,this.authority=t,this.authorizationVerifier=r,this.attemptPorts=a,this.authorizationJournal=n,this.getLease=o,this.options=i}start(){if(this.running)return;this.controller=new AbortController;const e=this.loop(this.controller.signal).finally(()=>{this.running===e&&(this.running=void 0)});this.running=e}async stop(e){const t=this.running;if(this.controller?.abort(),!t)return;await waitForAbort(t,e)===aborted&&this.running===t&&(this.running=void 0)}async loop(e){let t=randomUUID(),r="",a="";for(;!e.aborted;){const n=this.getLease();if(!n){await delay(this.options.pollDelayMs??1e3,e);continue}const o=`${n.instanceId}\n${n.runtimeEpoch}\n${this.options.handlerIdentity}\n${this.options.journalStoreId}\n${this.options.journalGeneration}`;if(r&&r!==o&&(t=randomUUID()),r=o,a!==o)try{const t=await this.authorizationJournal.listRecoverable(n.runtimeEpoch);for(const r of t){if(e.aborted||this.getLease()?.leaseId!==n.leaseId)break;if(!await this.recoverExact({operationId:r.operationId,requestMessageId:r.requestMessageId,dispatchEpoch:r.runtimeEpoch},n,e))throw new Error("durable runtime invocation is still owned by another claim")}a=o}catch{await delay(this.options.pollDelayMs??1e3,e);continue}let i;try{const r=this.authority.claimNext({claimRequestId:t,instanceId:n.instanceId,handlerIdentity:this.options.handlerIdentity,runtimeEpoch:n.runtimeEpoch,journalStoreId:this.options.journalStoreId,journalGeneration:this.options.journalGeneration,runtimeLeaseCredential:n.runtimeLeaseCredential}),a=await waitForAbort(r,e);if(a===aborted)return;i=a}catch(t){if(e.aborted)return;if(t instanceof RuntimeClaimAuthorityProtocolError)return;await delay(this.options.pollDelayMs??1e3,e);continue}if(e.aborted)return;t=randomUUID(),"empty"!==i.status?"fenced"!==i.status?await this.handleClaimed(i,n,e,"start").catch(()=>{}):await delay(this.options.pollDelayMs??1e3,e):await delay(Math.max(i.retryAfterMs,this.options.pollDelayMs??0),e)}}async recoverExact(e,t,r){if(e.dispatchEpoch!==t.runtimeEpoch)throw new Error("recovery candidate runtime epoch mismatch");const a=await this.authority.claimExact({operationId:e.operationId,requestMessageId:e.requestMessageId,handlerIdentity:this.options.handlerIdentity,dispatchEpoch:e.dispatchEpoch,runtimeEpoch:t.runtimeEpoch,journalStoreId:this.options.journalStoreId,journalGeneration:this.options.journalGeneration,runtimeLeaseCredential:t.runtimeLeaseCredential});return"terminal"===a.status?(await this.authorizationJournal.markTerminal(e.operationId).catch(()=>{}),!0):"claimed"===a.status&&(await this.handleClaimed(a,t,r,"recovery"),!0)}async handleClaimed(e,t,r,a="start"){const n={operationId:e.request.operationId,requestMessageId:e.request.messageId,dispatchEpoch:e.request.payload.command.context.dispatchEpoch},o=async()=>{await this.authorizationJournal.markTerminal(n.operationId).catch(()=>{})},i=async e=>{await this.authority.commitFailure(e),await o()},s=()=>{const r=this.getLease(),a=r?.leaseId===t.leaseId&&r.runtimeEpoch===t.runtimeEpoch?r.runtimeLeaseCredential:t.runtimeLeaseCredential;return{operationId:n.operationId,requestMessageId:n.requestMessageId,...e.fence,runtimeLeaseCredential:a}},c=async e=>{try{await i(e)}finally{await this.authority.releaseClaim({...s(),reason:"claim_rejected"}).catch(()=>{})}};if(e.cancelRequested)return await i({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"cancelled",code:"OPERATION_CANCELLED"}),void await this.authority.releaseClaim({...s(),reason:"cancelled"});let d;try{d=await this.authorizationJournal.prepare({operationId:n.operationId,requestMessageId:n.requestMessageId,method:e.request.payload.request.method,commandHash:e.request.payload.commandHash,runtimeEpoch:t.runtimeEpoch,journalStoreId:e.fence.journalStoreId,journalGeneration:e.fence.journalGeneration})}catch{return void await c({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"failed",code:"UNAUTHORIZED_TARGET"}).catch(()=>{})}const u=await this.authority.authorize({request:e.request,fence:e.fence,lease:t,journal:d});if("authorized"!==u.status)return"terminal"===u.status?(await o(),void await this.authority.releaseClaim({...s(),reason:"completed"}).catch(()=>{})):void await c({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"expired"===u.status?"expired":"cancelled"===u.status?"cancelled":"failed",code:"expired"===u.status?"COMMAND_EXPIRED":"cancelled"===u.status?"OPERATION_CANCELLED":"fenced"===u.status?"INSTANCE_EPOCH_MISMATCH":"UNAUTHORIZED_TARGET"});const m={operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:u.attempt},l={...m,attempt:prestartAttempt(e.request)};let h;const p=new AbortController;let E;try{assertCommandCorrelation(e.request,u.attempt.command,t.runtimeEpoch),h=await verifyAndCreateServiceCoreExecutionAttempt({operationId:n.operationId,runtimeEpoch:t.runtimeEpoch,fence:e.fence,authorization:u.attempt.authorization,derivationContext:{...u.attempt.derivationContext,localJournalRevision:d.journalRevisionBeforeAttempt,localPreparedEntryHash:d.preparedEntryHash},verifier:this.authorizationVerifier,abortSignal:p.signal,recordVerifiedAttempt:async t=>{const r=t.request.binding;await this.authorizationJournal.recordBoundary({operationId:n.operationId,expectedRevision:d.revision,preparedEntryHash:d.preparedEntryHash,boundary:{executionBoundaryId:r.executionBoundaryId,boundaryAssertionHash:r.boundaryAssertionHash,executionMode:r.executionMode,fence:e.fence,...r.handoffId?{handoffId:r.handoffId}:{}}})},resolvePorts:async()=>E??=await this.attemptPorts.resolve(m,{getRuntimeLeaseCredential:()=>this.currentRuntimeLeaseCredential(t)})})}catch{return p.abort(),void await c({...l,outcome:"failed",code:"UNAUTHORIZED_TARGET"})}if(!h.ok)return p.abort(),void await c({...l,outcome:"failed",code:"UNAUTHORIZED_TARGET"});let I,C,f,y,w=!1,M=!1,v=!1,N=!1,A=!1,R=!1,O=!1,T=!1,_=!1;const b=new Promise(e=>{y=e}),g=e=>{_||(_=!0,y(e))};let L;const x=()=>{O=!0,p.abort(),f&&!L&&(L=f.cancel().catch(()=>({status:"not_cancellable"})))},U=()=>{w=!0,g("fenced"),x()},q=()=>U();r.addEventListener("abort",q,{once:!0});const S=()=>{const e=this.getLease();e&&e.leaseId===t.leaseId&&e.runtimeEpoch===t.runtimeEpoch||U()},P=setInterval(S,100);P.unref?.();const H=setInterval(()=>{this.authority.renewClaim(s()).then(e=>{"renewed"===e.status&&e.cancelRequested?(M=!0,g("cancel_requested"),x()):"renewed"!==e.status&&U()}).catch(()=>{U()})},this.options.claimRenewIntervalMs??2e4);H.unref?.();const D=Date.parse(u.attempt.command.context.executionDeadline)-Date.now(),j=setTimeout(()=>{T=!0,g("deadline"),x()},Math.max(0,Math.min(D,2147483647)));j.unref?.();const G=async e=>{if(!I)return;const r=this.currentConversationRunBoundaryLease(t);await this.finishConversationRunBoundary(m,r,I,e,"recovery"===a?"recovery":"runtime"),I=void 0};try{if(S(),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(await this.authority.appendStarted(m),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(T)return void await i({...m,outcome:"expired",code:"COMMAND_EXPIRED"});if(M)return void await i({...m,outcome:"cancelled",code:"OPERATION_CANCELLED"});const e=await this.startConversationRunBoundary(m,t,E);if(e?.rejected)return void await i({...m,outcome:"failed",code:e.code});if(I=e,w||r.aborted)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime authority fenced after the conversation run boundary started");if("recovery"===a&&I)throw new RuntimeCommandError("OUTCOME_UNKNOWN","session.prompt crossed its durable run boundary before recovery");if(T)throw new RuntimeCommandError("COMMAND_EXPIRED","session.prompt expired after the conversation run boundary started");if(M)throw new RuntimeCommandError("OPERATION_CANCELLED","session.prompt was cancelled after the conversation run boundary started");let n;f=this.dispatcher.dispatch(u.attempt.command,h.context),N=!0,O&&x();const s=this.forwardProgress(f.progress,m,()=>w||r.aborted||M||T).catch(e=>{n=e}),c=f.completion.then(e=>(clearTimeout(j),{status:"completed",result:e}),e=>(clearTimeout(j),{status:"failed",error:e}));let d=await Promise.race([c,b.then(e=>({status:"interrupted",reason:e}))]);if("interrupted"===d.status){const e=d.reason;x();if("cancel_requested"===(L?await Promise.race([L,delay(this.options.cancellationGraceMs??1e3).then(()=>({status:"not_cancellable"}))]):{status:"not_cancellable"}).status&&(d=await Promise.race([c,delay(this.options.cancellationGraceMs??1e3).then(()=>({status:"interrupted",reason:e}))])),"interrupted"===d.status)throw new RuntimeCommandError("fenced"===d.reason?"INSTANCE_EPOCH_MISMATCH":"OUTCOME_UNKNOWN","session.prompt execution did not converge after interruption")}if(A=!0,"failed"===d.status)throw d.error;const l=d.result;v=!0;const p="session.prompt"===u.attempt.command.name&&!0===l.requestAborted;if(C=p?"cancelled":"completed",await s,n)throw n;if(await G(C),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(T)return void await i({...m,outcome:"outcome_unknown",code:"OUTCOME_UNKNOWN"});if(M&&!p)throw new RuntimeCommandError("OUTCOME_UNKNOWN","cancel request raced a locally completed runtime effect");await this.authority.commitResult({...m,command:u.attempt.command,result:sanitizeServiceResult(u.attempt.command.name,l)}),await o()}catch(e){let t=e;if(I&&(!N||A))try{await G(C??(M||T?"cancelled":"failed"))}catch(r){t=new RuntimeCommandError("OUTCOME_UNKNOWN",`conversation run terminal outcome is unknown: ${errorText(r)}`,void 0,{cause:e})}if(v&&!isUncertainRuntimeError(t)&&(t=new RuntimeCommandError("OUTCOME_UNKNOWN",`runtime effect completed before its terminal receipt: ${errorText(t)}`,void 0,{cause:e})),I)return void(R=!0);const r=serviceRuntimeFailureCode(t),a="OUTCOME_UNKNOWN"===r||"INSTANCE_EPOCH_MISMATCH"===r||w||N&&(M||T),n="COMMAND_EXPIRED"===r||!N&&T,o=!N&&M;await i({...m,outcome:a?"outcome_unknown":n?"expired":o?"cancelled":"failed",code:"INSTANCE_EPOCH_MISMATCH"===r||w?"INSTANCE_EPOCH_MISMATCH":a?"OUTCOME_UNKNOWN":n?"COMMAND_EXPIRED":o?"OPERATION_CANCELLED":r??"RUNTIME_EXECUTION_FAILED"})}finally{clearInterval(P),clearInterval(H),clearTimeout(j),r.removeEventListener("abort",q),p.abort(),await this.authority.releaseClaim({...s(),reason:r.aborted||R?"worker_shutdown":"completed"}).catch(()=>{})}}async startConversationRunBoundary(e,t,r){const a=e.attempt.command;if("session.prompt"!==a.name)return;const n=r?.conversationRunBoundary;if(!n)throw new RuntimeCommandError("INVALID_PARAMS","Service session.prompt lacks its conversation run boundary");const o=a.context.conversation,i=await n.boundary.start({operationId:e.operationId,requestMessageId:e.requestMessageId,conversationId:o.runtimeConversationUid,historyGeneration:n.historyGeneration,runtimeRunId:a.params.runtimeRunId,lease:exactRunBoundaryLease(t)});if("rejected"===i.status)return{rejected:!0,code:i.reason};if(i.historyGeneration!==n.historyGeneration)throw new RuntimeCommandError("OUTCOME_UNKNOWN","conversation run boundary changed history generation");return{boundary:n.boundary,historyGeneration:n.historyGeneration,activeRunRegistrationId:i.activeRunRegistrationId,runtimeRunId:a.params.runtimeRunId}}currentRuntimeLeaseCredential(e){const t=this.getLease();if(!t||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime lease changed before deferred resource redemption");return t.runtimeLeaseCredential}currentConversationRunBoundaryLease(e){const t=this.getLease();if(!t||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime lease changed before conversation run convergence");return t}async finishConversationRunBoundary(e,t,r,a,n){const o=e.attempt.command.context;if("conversation"!==o.scope)throw new RuntimeCommandError("OUTCOME_UNKNOWN","conversation run boundary lost its target");const i=await r.boundary.finish({activeRunRegistrationId:r.activeRunRegistrationId,operationId:e.operationId,requestMessageId:e.requestMessageId,conversationId:o.conversation.runtimeConversationUid,historyGeneration:r.historyGeneration,runtimeRunId:r.runtimeRunId,terminalState:a,source:n,lease:exactRunBoundaryLease(t)});if("rejected"===i.status||i.activeRunRegistrationId!==r.activeRunRegistrationId)throw new RuntimeCommandError("OUTCOME_UNKNOWN",`conversation run finish rejected: ${"rejected"===i.status?i.reason:"registration_mismatch"}`)}async forwardProgress(e,t,r){for await(const a of e){if(r())return;await this.authority.appendProgress({...t,progress:a})}}}function exactRunBoundaryLease(e){return{leaseId:e.leaseId,handlerIdentity:e.handlerIdentity,runtimeEpoch:e.runtimeEpoch,runtimeLeaseCredential:e.runtimeLeaseCredential}}function serviceRuntimeFailureCode(e){const t=e?.code;return t&&["OUTCOME_UNKNOWN","INSTANCE_EPOCH_MISMATCH","COMMAND_EXPIRED","OPERATION_CANCELLED","IDEMPOTENCY_CONFLICT","UNAUTHORIZED_TARGET","AGENT_DEFAULT_MODEL_NOT_CONFIGURED"].includes(t)?t:void 0}function isUncertainRuntimeError(e){return e instanceof RuntimeCommandError&&("OUTCOME_UNKNOWN"===e.code||"INSTANCE_EPOCH_MISMATCH"===e.code)}function errorText(e){return e instanceof Error?e.message:String(e)}export function sanitizeServiceResult(e,t){if("cron.status"!==e||!t||"object"!=typeof t)return t;const r=t;return{enabled:r.enabled,jobCount:r.jobCount,..."number"==typeof r.nextWakeAtMs?{nextWakeAtMs:r.nextWakeAtMs}:{}}}function assertCommandCorrelation(e,t,r){if(e.operationId!==t.context.operationId||e.payload.paramsHash!==t.context.paramsHash||t.context.dispatchEpoch!==r||e.payload.command.name!==t.name||!sessionSetModelMappingMatches(e.payload.request,t))throw new Error("runtime invocation command correlation failed")}function prestartAttempt(e){return{command:e.payload.command,executionBoundaryId:"prestart",boundaryAssertionHash:e.payload.commandHash,executionMode:"start",validationContext:{},authorization:{},derivationContext:{}}}const aborted=Symbol("aborted");function waitForAbort(e,t){return t?t.aborted?(e.catch(()=>{}),Promise.resolve(aborted)):new Promise((r,a)=>{let n=!1;const o=()=>{n||(n=!0,r(aborted))};t.addEventListener("abort",o,{once:!0}),e.then(e=>{n||(n=!0,t.removeEventListener("abort",o),r(e))},e=>{n||(n=!0,t.removeEventListener("abort",o),a(e))})}):e}const delay=(e,t)=>new Promise(r=>{if(t?.aborted)return r();const a=()=>{clearTimeout(n),r()},n=setTimeout(()=>{t?.removeEventListener("abort",a),r()},e);t?.addEventListener("abort",a,{once:!0})});
|
|
1
|
+
import{randomUUID}from"node:crypto";import{verifyAndCreateServiceCoreExecutionAttempt}from"./core-execution-attempt-context.js";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{RuntimeClaimAuthorityProtocolError}from"./message-service-runtime-claim-authority.js";import{sessionSetModelMappingMatches}from"./session-set-model-mapping.js";export class MsRuntimeRpcAdapter{dispatcher;authority;authorizationVerifier;attemptPorts;authorizationJournal;getLease;options;controller;running;constructor(e,t,r,a,n,o,i){this.dispatcher=e,this.authority=t,this.authorizationVerifier=r,this.attemptPorts=a,this.authorizationJournal=n,this.getLease=o,this.options=i}start(){if(this.running)return;this.controller=new AbortController;const e=this.loop(this.controller.signal).finally(()=>{this.running===e&&(this.running=void 0)});this.running=e}async stop(e){const t=this.running;if(this.controller?.abort(),!t)return;await waitForAbort(t,e)===aborted&&this.running===t&&(this.running=void 0)}async loop(e){let t=randomUUID(),r="",a="";for(;!e.aborted;){const n=this.getLease();if(!n){await delay(this.options.pollDelayMs??1e3,e);continue}const o=`${n.instanceId}\n${n.runtimeEpoch}\n${this.options.handlerIdentity}\n${this.options.journalStoreId}\n${this.options.journalGeneration}`;if(r&&r!==o&&(t=randomUUID()),r=o,a!==o)try{const t=await this.authorizationJournal.listRecoverable(n.runtimeEpoch);for(const r of t){if(e.aborted||this.getLease()?.leaseId!==n.leaseId)break;if(!await this.recoverExact({operationId:r.operationId,requestMessageId:r.requestMessageId,dispatchEpoch:r.runtimeEpoch},n,e))throw new Error("durable runtime invocation is still owned by another claim")}a=o}catch{await delay(this.options.pollDelayMs??1e3,e);continue}let i;try{const r=this.authority.claimNext({claimRequestId:t,instanceId:n.instanceId,handlerIdentity:this.options.handlerIdentity,runtimeEpoch:n.runtimeEpoch,journalStoreId:this.options.journalStoreId,journalGeneration:this.options.journalGeneration,runtimeLeaseCredential:n.runtimeLeaseCredential}),a=await waitForAbort(r,e);if(a===aborted)return;i=a}catch(t){if(e.aborted)return;if(t instanceof RuntimeClaimAuthorityProtocolError)return;await delay(this.options.pollDelayMs??1e3,e);continue}if(e.aborted)return;t=randomUUID(),"empty"!==i.status?"fenced"!==i.status?await this.handleClaimed(i,n,e,"start").catch(()=>{}):await delay(this.options.pollDelayMs??1e3,e):await delay(Math.max(i.retryAfterMs,this.options.pollDelayMs??0),e)}}async recoverExact(e,t,r){if(e.dispatchEpoch!==t.runtimeEpoch)throw new Error("recovery candidate runtime epoch mismatch");const a=await this.authority.claimExact({operationId:e.operationId,requestMessageId:e.requestMessageId,handlerIdentity:this.options.handlerIdentity,dispatchEpoch:e.dispatchEpoch,runtimeEpoch:t.runtimeEpoch,journalStoreId:this.options.journalStoreId,journalGeneration:this.options.journalGeneration,runtimeLeaseCredential:t.runtimeLeaseCredential});return"terminal"===a.status?(await this.authorizationJournal.markTerminal(e.operationId).catch(()=>{}),!0):"claimed"===a.status&&(await this.handleClaimed(a,t,r,"recovery"),!0)}async handleClaimed(e,t,r,a="start"){const n={operationId:e.request.operationId,requestMessageId:e.request.messageId,dispatchEpoch:e.request.payload.command.context.dispatchEpoch},o=async()=>{await this.authorizationJournal.markTerminal(n.operationId).catch(()=>{})},i=async e=>{await this.authority.commitFailure(e),await o()},s=()=>{const r=this.getLease(),a=r?.leaseId===t.leaseId&&r.runtimeEpoch===t.runtimeEpoch?r.runtimeLeaseCredential:t.runtimeLeaseCredential;return{operationId:n.operationId,requestMessageId:n.requestMessageId,...e.fence,runtimeLeaseCredential:a}},c=async e=>{try{await i(e)}finally{await this.authority.releaseClaim({...s(),reason:"claim_rejected"}).catch(()=>{})}};if(e.cancelRequested)return await i({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"cancelled",code:"OPERATION_CANCELLED"}),void await this.authority.releaseClaim({...s(),reason:"cancelled"});let d;try{d=await this.authorizationJournal.prepare({operationId:n.operationId,requestMessageId:n.requestMessageId,method:e.request.payload.request.method,commandHash:e.request.payload.commandHash,runtimeEpoch:t.runtimeEpoch,journalStoreId:e.fence.journalStoreId,journalGeneration:e.fence.journalGeneration})}catch{return void await c({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"failed",code:"UNAUTHORIZED_TARGET"}).catch(()=>{})}const u=await this.authority.authorize({request:e.request,fence:e.fence,lease:t,journal:d});if("authorized"!==u.status)return"terminal"===u.status?(await o(),void await this.authority.releaseClaim({...s(),reason:"completed"}).catch(()=>{})):void await c({operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:prestartAttempt(e.request),outcome:"expired"===u.status?"expired":"cancelled"===u.status?"cancelled":"failed",code:"expired"===u.status?"COMMAND_EXPIRED":"cancelled"===u.status?"OPERATION_CANCELLED":"fenced"===u.status?"INSTANCE_EPOCH_MISMATCH":"UNAUTHORIZED_TARGET"});const m={operationId:n.operationId,requestMessageId:n.requestMessageId,request:e.request,fence:e.fence,claimLeaseExpiresAt:e.leaseExpiresAt,runtimeLeaseCredential:t.runtimeLeaseCredential,attempt:u.attempt},l={...m,attempt:prestartAttempt(e.request)};let h;const p=new AbortController;let E;try{assertCommandCorrelation(e.request,u.attempt.command,t.runtimeEpoch),h=await verifyAndCreateServiceCoreExecutionAttempt({operationId:n.operationId,runtimeEpoch:t.runtimeEpoch,fence:e.fence,authorization:u.attempt.authorization,derivationContext:{...u.attempt.derivationContext,localJournalRevision:d.journalRevisionBeforeAttempt,localPreparedEntryHash:d.preparedEntryHash},verifier:this.authorizationVerifier,abortSignal:p.signal,recordVerifiedAttempt:async t=>{const r=t.request.binding;await this.authorizationJournal.recordBoundary({operationId:n.operationId,expectedRevision:d.revision,preparedEntryHash:d.preparedEntryHash,boundary:{executionBoundaryId:r.executionBoundaryId,boundaryAssertionHash:r.boundaryAssertionHash,executionMode:r.executionMode,fence:e.fence,...r.handoffId?{handoffId:r.handoffId}:{}}})},resolvePorts:async()=>E??=await this.attemptPorts.resolve(m,{getRuntimeLeaseCredential:()=>this.currentRuntimeLeaseCredential(t)})})}catch{return p.abort(),void await c({...l,outcome:"failed",code:"UNAUTHORIZED_TARGET"})}if(!h.ok)return p.abort(),void await c({...l,outcome:"failed",code:"UNAUTHORIZED_TARGET"});let I,C,f,y,w=!1,M=!1,v=0,N=!1,A=!1,R=!1,O=!1,T=!1,_=!1,b=!1;const g=new Promise(e=>{y=e}),L=e=>{b||(b=!0,y(e))};let x;const U=()=>{T=!0,p.abort(),f&&!x&&(x=f.cancel().catch(()=>({status:"not_cancellable"})))},q=()=>{w=!0,L("fenced"),U()},S=()=>q();r.addEventListener("abort",S,{once:!0});const P=()=>{const e=this.getLease();e&&e.leaseId===t.leaseId&&e.runtimeEpoch===t.runtimeEpoch||q()},H=setInterval(P,100);H.unref?.();const D=setInterval(()=>{this.authority.renewClaim(s()).then(e=>{v=0,"renewed"===e.status&&e.cancelRequested?(M=!0,L("cancel_requested"),U()):"renewed"!==e.status&&q()}).catch(()=>{v+=1,v<3||(v=0,q())})},this.options.claimRenewIntervalMs??2e4);D.unref?.();const j=Date.parse(u.attempt.command.context.executionDeadline)-Date.now(),G=setTimeout(()=>{_=!0,L("deadline"),U()},Math.max(0,Math.min(j,2147483647)));G.unref?.();const z=async e=>{if(!I)return;const r=this.currentConversationRunBoundaryLease(t);await this.finishConversationRunBoundary(m,r,I,e,"recovery"===a?"recovery":"runtime"),I=void 0};try{if(P(),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(await this.authority.appendStarted(m),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(_)return void await i({...m,outcome:"expired",code:"COMMAND_EXPIRED"});if(M)return void await i({...m,outcome:"cancelled",code:"OPERATION_CANCELLED"});const e=await this.startConversationRunBoundary(m,t,E);if(e?.rejected)return void await i({...m,outcome:"failed",code:e.code});if(I=e,w||r.aborted)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime authority fenced after the conversation run boundary started");if("recovery"===a&&I)throw new RuntimeCommandError("OUTCOME_UNKNOWN","session.prompt crossed its durable run boundary before recovery");if(_)throw new RuntimeCommandError("COMMAND_EXPIRED","session.prompt expired after the conversation run boundary started");if(M)throw new RuntimeCommandError("OPERATION_CANCELLED","session.prompt was cancelled after the conversation run boundary started");let n;f=this.dispatcher.dispatch(u.attempt.command,h.context),A=!0,T&&U();const s=this.forwardProgress(f.progress,m,()=>w||r.aborted||M||_).catch(e=>{n=e}),c=f.completion.then(e=>(clearTimeout(G),{status:"completed",result:e}),e=>(clearTimeout(G),{status:"failed",error:e}));let d=await Promise.race([c,g.then(e=>({status:"interrupted",reason:e}))]);if("interrupted"===d.status){const e=d.reason;U();if("cancel_requested"===(x?await Promise.race([x,delay(this.options.cancellationGraceMs??1e3).then(()=>({status:"not_cancellable"}))]):{status:"not_cancellable"}).status&&(d=await Promise.race([c,delay(this.options.cancellationGraceMs??1e3).then(()=>({status:"interrupted",reason:e}))])),"interrupted"===d.status)throw new RuntimeCommandError("fenced"===d.reason?"INSTANCE_EPOCH_MISMATCH":"OUTCOME_UNKNOWN","session.prompt execution did not converge after interruption")}if(R=!0,"failed"===d.status)throw d.error;const l=d.result;N=!0;const p="session.prompt"===u.attempt.command.name&&!0===l.requestAborted;if(C=p?"cancelled":"completed",await s,n)throw n;if(await z(C),w||r.aborted)return void await i({...m,outcome:"outcome_unknown",code:"INSTANCE_EPOCH_MISMATCH"});if(_)return void await i({...m,outcome:"outcome_unknown",code:"OUTCOME_UNKNOWN"});if(M&&!p)throw new RuntimeCommandError("OUTCOME_UNKNOWN","cancel request raced a locally completed runtime effect");await this.authority.commitResult({...m,command:u.attempt.command,result:sanitizeServiceResult(u.attempt.command.name,l)}),await o()}catch(e){let t=e;if(I&&(!A||R))try{await z(C??(M||_?"cancelled":"failed"))}catch(r){t=new RuntimeCommandError("OUTCOME_UNKNOWN",`conversation run terminal outcome is unknown: ${errorText(r)}`,void 0,{cause:e})}if(N&&!isUncertainRuntimeError(t)&&(t=new RuntimeCommandError("OUTCOME_UNKNOWN",`runtime effect completed before its terminal receipt: ${errorText(t)}`,void 0,{cause:e})),I)return void(O=!0);const r=serviceRuntimeFailureCode(t),a="OUTCOME_UNKNOWN"===r||"INSTANCE_EPOCH_MISMATCH"===r||w||A&&(M||_),n="COMMAND_EXPIRED"===r||!A&&_,o=!A&&M;await i({...m,outcome:a?"outcome_unknown":n?"expired":o?"cancelled":"failed",code:"INSTANCE_EPOCH_MISMATCH"===r||w?"INSTANCE_EPOCH_MISMATCH":a?"OUTCOME_UNKNOWN":n?"COMMAND_EXPIRED":o?"OPERATION_CANCELLED":r??"RUNTIME_EXECUTION_FAILED"})}finally{clearInterval(H),clearInterval(D),clearTimeout(G),r.removeEventListener("abort",S),p.abort(),await this.authority.releaseClaim({...s(),reason:r.aborted||O?"worker_shutdown":"completed"}).catch(()=>{})}}async startConversationRunBoundary(e,t,r){const a=e.attempt.command;if("session.prompt"!==a.name)return;const n=r?.conversationRunBoundary;if(!n)throw new RuntimeCommandError("INVALID_PARAMS","Service session.prompt lacks its conversation run boundary");const o=a.context.conversation,i=await n.boundary.start({operationId:e.operationId,requestMessageId:e.requestMessageId,conversationId:o.runtimeConversationUid,historyGeneration:n.historyGeneration,runtimeRunId:a.params.runtimeRunId,lease:exactRunBoundaryLease(t)});if("rejected"===i.status)return{rejected:!0,code:i.reason};if(i.historyGeneration!==n.historyGeneration)throw new RuntimeCommandError("OUTCOME_UNKNOWN","conversation run boundary changed history generation");return{boundary:n.boundary,historyGeneration:n.historyGeneration,activeRunRegistrationId:i.activeRunRegistrationId,runtimeRunId:a.params.runtimeRunId}}currentRuntimeLeaseCredential(e){const t=this.getLease();if(!t||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime lease changed before deferred resource redemption");return t.runtimeLeaseCredential}currentConversationRunBoundaryLease(e){const t=this.getLease();if(!t||t.leaseId!==e.leaseId||t.runtimeEpoch!==e.runtimeEpoch||t.handlerIdentity!==e.handlerIdentity)throw new RuntimeCommandError("INSTANCE_EPOCH_MISMATCH","runtime lease changed before conversation run convergence");return t}async finishConversationRunBoundary(e,t,r,a,n){const o=e.attempt.command.context;if("conversation"!==o.scope)throw new RuntimeCommandError("OUTCOME_UNKNOWN","conversation run boundary lost its target");const i=await r.boundary.finish({activeRunRegistrationId:r.activeRunRegistrationId,operationId:e.operationId,requestMessageId:e.requestMessageId,conversationId:o.conversation.runtimeConversationUid,historyGeneration:r.historyGeneration,runtimeRunId:r.runtimeRunId,terminalState:a,source:n,lease:exactRunBoundaryLease(t)});if("rejected"===i.status||i.activeRunRegistrationId!==r.activeRunRegistrationId)throw new RuntimeCommandError("OUTCOME_UNKNOWN",`conversation run finish rejected: ${"rejected"===i.status?i.reason:"registration_mismatch"}`)}async forwardProgress(e,t,r){for await(const a of e){if(r())return;await this.authority.appendProgress({...t,progress:a})}}}function exactRunBoundaryLease(e){return{leaseId:e.leaseId,handlerIdentity:e.handlerIdentity,runtimeEpoch:e.runtimeEpoch,runtimeLeaseCredential:e.runtimeLeaseCredential}}function serviceRuntimeFailureCode(e){const t=e?.code;return t&&["OUTCOME_UNKNOWN","INSTANCE_EPOCH_MISMATCH","COMMAND_EXPIRED","OPERATION_CANCELLED","IDEMPOTENCY_CONFLICT","UNAUTHORIZED_TARGET","AGENT_DEFAULT_MODEL_NOT_CONFIGURED"].includes(t)?t:void 0}function isUncertainRuntimeError(e){return e instanceof RuntimeCommandError&&("OUTCOME_UNKNOWN"===e.code||"INSTANCE_EPOCH_MISMATCH"===e.code)}function errorText(e){return e instanceof Error?e.message:String(e)}export function sanitizeServiceResult(e,t){if("cron.status"!==e||!t||"object"!=typeof t)return t;const r=t;return{enabled:r.enabled,jobCount:r.jobCount,..."number"==typeof r.nextWakeAtMs?{nextWakeAtMs:r.nextWakeAtMs}:{}}}function assertCommandCorrelation(e,t,r){if(e.operationId!==t.context.operationId||e.payload.paramsHash!==t.context.paramsHash||t.context.dispatchEpoch!==r||e.payload.command.name!==t.name||!sessionSetModelMappingMatches(e.payload.request,t))throw new Error("runtime invocation command correlation failed")}function prestartAttempt(e){return{command:e.payload.command,executionBoundaryId:"prestart",boundaryAssertionHash:e.payload.commandHash,executionMode:"start",validationContext:{},authorization:{},derivationContext:{}}}const aborted=Symbol("aborted");function waitForAbort(e,t){return t?t.aborted?(e.catch(()=>{}),Promise.resolve(aborted)):new Promise((r,a)=>{let n=!1;const o=()=>{n||(n=!0,r(aborted))};t.addEventListener("abort",o,{once:!0}),e.then(e=>{n||(n=!0,t.removeEventListener("abort",o),r(e))},e=>{n||(n=!0,t.removeEventListener("abort",o),a(e))})}):e}const delay=(e,t)=>new Promise(r=>{if(t?.aborted)return r();const a=()=>{clearTimeout(n),r()},n=setTimeout(()=>{t?.removeEventListener("abort",a),r()},e);t?.addEventListener("abort",a,{once:!0})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHash,randomBytes,sign}from"node:crypto";import{RUNTIME_INTERNAL_API_V1_ROUTE_CONTRACTS,RUNTIME_HASH_DOMAINS,RUNTIME_RPC_PROTOCOL_VERSION,runtimeHashPreimage,runtimeIdentitySignatureMessage,validateRuntimeInternalRegistrationResult}from"@beeos-ai/beeos-types/runtime";export class RuntimeRegistrationLeaseExpiredError extends Error{constructor(){super("Instance authority returned an expired active lease"),this.name="RuntimeRegistrationLeaseExpiredError"}}export class RuntimeRegistrationHandoffRequiredError extends Error{retryAfterMs;constructor(e=65e3){super("Instance authority requires waiting for the active runtime lease to expire"),this.name="RuntimeRegistrationHandoffRequiredError",this.retryAfterMs=e}}export class RuntimeRegistrationJournalAdvanceRequiredError extends Error{currentJournalGeneration;expectedJournalGeneration;constructor(e){super("Instance authority requires journal generation to advance exactly once after lease expiry"),this.name="RuntimeRegistrationJournalAdvanceRequiredError",this.currentJournalGeneration=e?.currentJournalGeneration,this.expectedJournalGeneration=e?.expectedJournalGeneration}}export class RuntimeRegistrationCoordinator{transport;config;onLeaseLost;now;active=null;timer;expiryTimer;stopped=!1;generation=0;authorityListeners=new Set;constructor(e,t,i,r=Date.now){this.transport=e,this.config=t,this.onLeaseLost=i,this.now=r}get lease(){return!this.active||Date.parse(this.active.leaseExpiresAt)<=this.now()?null:this.active}get canClaimMutations(){return null!==this.lease}get authorityLease(){const e=this.lease;return e?Object.freeze({...e,handlerIdentity:this.config.handlerIdentity}):null}subscribeAuthority(e){return this.authorityListeners.add(e),()=>{this.authorityListeners.delete(e)}}async start(){const e=++this.generation;return this.stopped=!1,await this.registerOnce(e)}async registerOnce(e){if(!this.isCurrent(e))return null;this.timer=void 0,this.active&&(this.clearLeaseExpiry(),this.active=null,this.notifyAuthority());const t={registrationId:this.config.registrationId,handlerIdentity:this.config.handlerIdentity,contractRevision:this.config.contractRevision,runtimeRpcProtocolVersion:RUNTIME_RPC_PROTOCOL_VERSION,runtimeMethods:this.config.runtimeMethods,capabilities:this.config.capabilities,manifestDigest:this.config.manifestDigest,journalStoreId:this.config.journalStoreId,journalGeneration:this.config.journalGeneration};let i;try{i=await this.transport.register(this.signProof("runtime.register",t))}catch(t){if(!this.isCurrent(e))return null;if(t instanceof RuntimeRegistrationLeaseExpiredError||t instanceof RuntimeRegistrationJournalAdvanceRequiredError)return this.onLeaseLost(t instanceof RuntimeRegistrationLeaseExpiredError?"expired":"heartbeat_failed",t),null;this.onLeaseLost("heartbeat_failed",t);const i=t instanceof RuntimeRegistrationHandoffRequiredError?t.retryAfterMs:5e3;return this.scheduleRegistration(i,e),null}return this.isCurrent(e)?"active"!==i.status?("fenced"===i.status?this.onLeaseLost("fenced"):this.scheduleRegistration(i.retryAfterMs,e),null):(this.active={instanceId:i.instanceId,leaseId:i.leaseId,runtimeEpoch:i.runtimeEpoch,runtimeLeaseCredential:i.runtimeLeaseCredential,leaseExpiresAt:i.leaseExpiresAt},this.notifyAuthority(),this.scheduleLeaseExpiry(this.active,e),this.schedule(i.heartbeatIntervalMs,e),this.active):null}stop(){this.generation++,this.stopped=!0,this.timer&&clearTimeout(this.timer),this.expiryTimer&&clearTimeout(this.expiryTimer),this.timer=void 0,this.expiryTimer=void 0,this.active=null,this.notifyAuthority()}schedule(e,t){if(!this.isCurrent(t))return;const i=this.active;i&&(this.timer=setTimeout(()=>{this.sendHeartbeat(e,t,i)},e),this.timer.unref?.())}scheduleRegistration(e,t){this.isCurrent(t)&&(this.timer=setTimeout(()=>{this.registerOnce(t)},e),this.timer.unref?.())}async sendHeartbeat(e,t,i){if(!this.isCurrentLease(t,i))return;const r={leaseId:i.leaseId,handlerIdentity:this.config.handlerIdentity,runtimeEpoch:i.runtimeEpoch};try{const s=await this.transport.heartbeat(this.signProof("runtime.heartbeat",r));if(!this.isCurrentLease(t,i))return;if("renewed"!==s.status)return this.active=null,this.clearLeaseExpiry(),this.notifyAuthority(),void this.onLeaseLost(s.status);this.active={...i,runtimeLeaseCredential:s.runtimeLeaseCredential,leaseExpiresAt:s.leaseExpiresAt},this.notifyAuthority(),this.scheduleLeaseExpiry(this.active,t),this.schedule(e,t)}catch(
|
|
1
|
+
import{createHash,randomBytes,sign}from"node:crypto";import{RUNTIME_INTERNAL_API_V1_ROUTE_CONTRACTS,RUNTIME_HASH_DOMAINS,RUNTIME_RPC_PROTOCOL_VERSION,runtimeHashPreimage,runtimeIdentitySignatureMessage,validateRuntimeInternalRegistrationResult}from"@beeos-ai/beeos-types/runtime";export class RuntimeRegistrationLeaseExpiredError extends Error{constructor(){super("Instance authority returned an expired active lease"),this.name="RuntimeRegistrationLeaseExpiredError"}}export class RuntimeRegistrationHandoffRequiredError extends Error{retryAfterMs;constructor(e=65e3){super("Instance authority requires waiting for the active runtime lease to expire"),this.name="RuntimeRegistrationHandoffRequiredError",this.retryAfterMs=e}}export class RuntimeRegistrationJournalAdvanceRequiredError extends Error{currentJournalGeneration;expectedJournalGeneration;constructor(e){super("Instance authority requires journal generation to advance exactly once after lease expiry"),this.name="RuntimeRegistrationJournalAdvanceRequiredError",this.currentJournalGeneration=e?.currentJournalGeneration,this.expectedJournalGeneration=e?.expectedJournalGeneration}}export class RuntimeRegistrationCoordinator{transport;config;onLeaseLost;now;active=null;timer;expiryTimer;stopped=!1;generation=0;heartbeatTransportFailures=0;authorityListeners=new Set;constructor(e,t,i,r=Date.now){this.transport=e,this.config=t,this.onLeaseLost=i,this.now=r}get lease(){return!this.active||Date.parse(this.active.leaseExpiresAt)<=this.now()?null:this.active}get canClaimMutations(){return null!==this.lease}get authorityLease(){const e=this.lease;return e?Object.freeze({...e,handlerIdentity:this.config.handlerIdentity}):null}subscribeAuthority(e){return this.authorityListeners.add(e),()=>{this.authorityListeners.delete(e)}}async start(){const e=++this.generation;return this.stopped=!1,await this.registerOnce(e)}async registerOnce(e){if(!this.isCurrent(e))return null;this.timer=void 0,this.active&&(this.clearLeaseExpiry(),this.active=null,this.notifyAuthority());const t={registrationId:this.config.registrationId,handlerIdentity:this.config.handlerIdentity,contractRevision:this.config.contractRevision,runtimeRpcProtocolVersion:RUNTIME_RPC_PROTOCOL_VERSION,runtimeMethods:this.config.runtimeMethods,capabilities:this.config.capabilities,manifestDigest:this.config.manifestDigest,journalStoreId:this.config.journalStoreId,journalGeneration:this.config.journalGeneration};let i;try{i=await this.transport.register(this.signProof("runtime.register",t))}catch(t){if(!this.isCurrent(e))return null;if(t instanceof RuntimeRegistrationLeaseExpiredError||t instanceof RuntimeRegistrationJournalAdvanceRequiredError)return this.onLeaseLost(t instanceof RuntimeRegistrationLeaseExpiredError?"expired":"heartbeat_failed",t),null;this.onLeaseLost("heartbeat_failed",t);const i=t instanceof RuntimeRegistrationHandoffRequiredError?t.retryAfterMs:5e3;return this.scheduleRegistration(i,e),null}return this.isCurrent(e)?"active"!==i.status?("fenced"===i.status?this.onLeaseLost("fenced"):this.scheduleRegistration(i.retryAfterMs,e),null):(this.active={instanceId:i.instanceId,leaseId:i.leaseId,runtimeEpoch:i.runtimeEpoch,runtimeLeaseCredential:i.runtimeLeaseCredential,leaseExpiresAt:i.leaseExpiresAt},this.heartbeatTransportFailures=0,this.notifyAuthority(),this.scheduleLeaseExpiry(this.active,e),this.schedule(i.heartbeatIntervalMs,e),this.active):null}stop(){this.generation++,this.stopped=!0,this.timer&&clearTimeout(this.timer),this.expiryTimer&&clearTimeout(this.expiryTimer),this.timer=void 0,this.expiryTimer=void 0,this.active=null,this.notifyAuthority()}schedule(e,t){if(!this.isCurrent(t))return;const i=this.active;i&&(this.timer=setTimeout(()=>{this.sendHeartbeat(e,t,i)},e),this.timer.unref?.())}scheduleRegistration(e,t){this.isCurrent(t)&&(this.timer=setTimeout(()=>{this.registerOnce(t)},e),this.timer.unref?.())}async sendHeartbeat(e,t,i){if(!this.isCurrentLease(t,i))return;const r={leaseId:i.leaseId,handlerIdentity:this.config.handlerIdentity,runtimeEpoch:i.runtimeEpoch};try{const s=await this.transport.heartbeat(this.signProof("runtime.heartbeat",r));if(!this.isCurrentLease(t,i))return;if("renewed"!==s.status)return this.heartbeatTransportFailures=0,this.active=null,this.clearLeaseExpiry(),this.notifyAuthority(),void this.onLeaseLost(s.status);this.heartbeatTransportFailures=0,this.active={...i,runtimeLeaseCredential:s.runtimeLeaseCredential,leaseExpiresAt:s.leaseExpiresAt},this.notifyAuthority(),this.scheduleLeaseExpiry(this.active,t),this.schedule(e,t)}catch(r){if(!this.isCurrentLease(t,i))return;if(this.heartbeatTransportFailures+=1,this.heartbeatTransportFailures<3)return void this.schedule(Math.min(e,5e3),t);this.heartbeatTransportFailures=0,this.active=null,this.clearLeaseExpiry(),this.notifyAuthority(),this.onLeaseLost("heartbeat_failed",r),this.scheduleRegistration(5e3,t)}}scheduleLeaseExpiry(e,t){this.clearLeaseExpiry();const i=`${e.leaseId}\0${e.runtimeEpoch}\0${e.leaseExpiresAt}`,r=()=>{const s=Date.parse(e.leaseExpiresAt)-this.now();if(s>0)return this.expiryTimer=setTimeout(r,Math.min(2147483647,s)),void this.expiryTimer.unref?.();const n=this.active;this.isCurrentLease(t,e)&&n&&`${n.leaseId}\0${n.runtimeEpoch}\0${n.leaseExpiresAt}`===i&&(this.active=null,this.expiryTimer=void 0,this.notifyAuthority(),this.onLeaseLost("expired"))};r()}clearLeaseExpiry(){this.expiryTimer&&clearTimeout(this.expiryTimer),this.expiryTimer=void 0}isCurrent(e){return!this.stopped&&e===this.generation}isCurrentLease(e,t){return this.isCurrent(e)&&this.active===t}notifyAuthority(){const e=this.authorityLease;for(const t of this.authorityListeners)try{t(e)}catch{}}signProof(e,t){const i=new Date(this.now()).toISOString(),r=randomBytes(18).toString("base64url"),s="runtime.register"===e?RUNTIME_HASH_DOMAINS.registration:RUNTIME_HASH_DOMAINS.heartbeat,n=createHash("sha256").update(runtimeHashPreimage(s,t)).digest("hex"),a=runtimeIdentitySignatureMessage({purpose:e,instanceIdentityKeyId:this.config.instanceIdentityKeyId,signedAt:i,nonce:r,payloadHash:n});return{...t,signaturePurpose:e,instanceIdentityKeyId:this.config.instanceIdentityKeyId,signedAt:i,nonce:r,payloadHash:n,signature:sign(null,a,this.config.privateKey).toString("base64url")}}}export class InstanceAuthorityRuntimeRegistrationTransport{options;constructor(e){this.options=e}async register(e){if(e.registrationId!==this.options.registrationId)throw new Error("runtime registration id mismatch");const t=await this.post(RUNTIME_INTERNAL_API_V1_ROUTE_CONTRACTS.runtimeRegister.path,e);if("active"===t.status&&Number.isFinite(Date.parse(t.leaseExpiresAt))&&Date.parse(t.leaseExpiresAt)<=Date.now())throw new RuntimeRegistrationLeaseExpiredError;if(validateRuntimeInternalRegistrationResult(t)||"fenced"!==t.status&&(t.journalStoreId!==e.journalStoreId||t.journalGeneration!==e.journalGeneration)||"active"===t.status&&(!t.instanceId||!t.leaseId||!t.runtimeLeaseCredential||!Number.isInteger(t.heartbeatIntervalMs)||!Number.isFinite(Date.parse(t.leaseExpiresAt))||Date.parse(t.leaseExpiresAt)<=Date.now()))throw new Error("Instance authority returned an invalid registration binding");return t}async heartbeat(e){const t=await this.post(RUNTIME_INTERNAL_API_V1_ROUTE_CONTRACTS.runtimeHeartbeat.path.replace("{registrationId}",encodeURIComponent(this.options.registrationId)),e);if(!(t&&["renewed","fenced","expired"].includes(t.status)&&("renewed"!==t.status||t.runtimeLeaseCredential&&Number.isFinite(Date.parse(t.leaseExpiresAt)))))throw new Error("Instance authority returned an invalid heartbeat result");return t}async post(e,t){const i=new URL(e,this.options.agentGatewayUrl),r=await this.options.agentFetch(i,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.text().catch(()=>"");if(409===r.status&&i.pathname.endsWith("/runtime/registrations"))throw classifyRuntimeRegistrationConflict(e);throw new Error(`runtime Instance authority request failed (${r.status} ${r.statusText}) ${i.pathname}`+(e?`: ${e.slice(0,240)}`:""))}return await r.json()}}export function classifyRuntimeRegistrationConflict(e){const t=extractConflictMessage(e);if(/journal generation must advance exactly once/i.test(t)){const e=t.match(/\bcurrent=(\d+)\b/i)?.[1],i=t.match(/\bexpected=(\d+)\b/i)?.[1];return new RuntimeRegistrationJournalAdvanceRequiredError({currentJournalGeneration:e&&/^[1-9][0-9]*$/.test(e)?e:void 0,expectedJournalGeneration:i&&/^[1-9][0-9]*$/.test(i)?i:void 0})}return/active runtime lease requires explicit handoff/i.test(t)||/waiting for the active runtime lease/i.test(t),new RuntimeRegistrationHandoffRequiredError}function extractConflictMessage(e){if(!e)return"";try{const t=JSON.parse(e),i=t.error?.message;if("string"==typeof i)return i;if("string"==typeof t.message)return t.message}catch{}return e}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.78",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|