beeos-claw 0.6.50 → 0.6.51-staging.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpProjection;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.(`[acp-server] Deferred agentSend failed: ${e instanceof Error?e.message:String(e)}`)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r),openRunOutputRoute(this.runOutputRouter,r,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}ensureImInboundSession(e,t,i,n){const r=this.core.ensureSessionForKey(e);r.channelId=t,r.inboundMessageId=i,r.activeMentions=n.length>0?n:void 0}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,n,r,s,a,o,c,l,h,g,d,u={}){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.gateway=u.gateway??null,this.agentProjection=u.agentProjection,this.markAgentProjectionDirty=u.markAgentProjectionDirty,this.templateOriginReporter=u.templateOriginReporter,this.agentMappings=u.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=u.agentCapabilities??{canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const m=(u.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=m;const p=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:p,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:m,agentMappings:this.agentMappings}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,runOutputRouter:m,schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},agentFetch:this.agentFetchFn,getCronConversationDelivery:()=>this.cronConversationDelivery??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const n=i.currentConsumerBinding();if(!n)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const r=e.runtimeConsumer;let s;r&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===r&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(r).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:n,getBinding:()=>i.currentConsumerBinding()??n,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(this.shouldContinueStart(e)){if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){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.markAgentProjectionDirty?this.markAgentProjectionDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return;if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],n=3e5,r=()=>{const r=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},n))}const s=i[Math.min(e,i.length-1)],a=Math.round(s/1e3),o=`[acp-server] agent sync retry #${r} in ${a}s`;r>0&&r%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${r} attempts — retrying every ${a}s`):r<=i.length?this.logger.info?.(o):this.logger.debug?.(o),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,boundAgentFetch:this.boundAgentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):r()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),r()}finally{this.agentSyncInFlight=!1}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const n=i instanceof Error?i.message:String(i),r=n.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${n}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:r?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:n,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{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})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}resolveLocalAgentIdForPlatformAgent(e){return this.core.resolveLocalAgentId(e)}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&o.push(a("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&o.push(a("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),g&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&o.push(a("websocket-server",()=>{u.removeAllListeners(),u.close()})),await Promise.all(o),await a("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await a("terminal-handler",()=>this.terminalHandler.destroy()),await a("terminal-manager",()=>this.terminalMgr.destroy()),n&&clearTimeout(n),r||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
1
+ import{WebSocketServer,WebSocket}from"ws";import{randomUUID}from"node:crypto";import{AsyncLocalStorage}from"node:async_hooks";let BridgeClientCtor;import{TerminalManager}from"./terminal/session-manager.js";import{getOrCreateRuntimeTerminalService}from"./terminal/runtime-terminal-service.js";import{RuntimeTerminalOutboundRegistration}from"./terminal/runtime-terminal-outbound-registration.js";import{FileTerminalAuthorityJournal}from"./terminal/terminal-authority-journal.js";import{LocalTerminalAuthorityRegistry}from"./terminal/local-terminal-authority.js";import{TerminalLeaseBoundBridgeClient}from"./terminal/terminal-lease-bound-bridge-client.js";import{join}from"node:path";import{AcpTransport}from"./acp-gateway/transport.js";import{AcpGatewaySessionState}from"./acp-gateway/session-state.js";import{AcpGatewayBridgeCore}from"./acp-gateway/bridge-core.js";import{BeeosFileResolver}from"./acp-gateway/beeos-file-resolver.js";import{AcpGatewayEvents}from"./acp-gateway/gateway-events.js";import{AcpRunOutputChannel}from"./acp-gateway/acp-run-output-channel.js";import{openRunOutputRoute}from"./acp-gateway/run-output-route.js";import{createAcpRunOutputRouter}from"./runtime/run-output-composition.js";import{AgentMappingRegistry}from"./runtime/agent-mapping-registry.js";import{sweepStaleSessionLocks}from"./acp-gateway/stale-session-lock-sweeper.js";import{TerminalBridgeHandler}from"./terminal/bridge-handler.js";import{isAgentSyncWaitForGateway,syncAgentsToPlatform}from"./agent-sync.js";import{CanvasOrchestrator}from"./canvas-orchestrator.js";import{discoverBridgeFromGW}from"./bridge-discovery.js";import{writeDiscoveryCache}from"./discovery-cache.js";function formatBridgeEventArgs(e){return 0===e.length?"(no args)":e.map((e,t)=>`arg${t}=${formatBridgeEventValue(e)}`).join(" ")}function formatBridgeEventValue(e){if(void 0===e)return"undefined";if(null===e)return"null";if(e instanceof Error){const t=e.stack?e.stack.split("\n").slice(0,4).join(" | "):"";return`Error(name=${e.name},message=${e.message}${t?`,stack=${t}`:""})`}if("object"==typeof e)try{return JSON.stringify(e,jsonReplacerForLogs)}catch{return String(e)}return String(e)}function jsonReplacerForLogs(e,t){return t instanceof Error?{name:t.name,message:t.message,stack:t.stack}:t}export class ACPServer{agentFetchFn;instanceId;runtimeAuthority;mcpProjection;mcpCredentialReferences;boundAgentFetchFn;wss=null;gateway;client=null;config;logger;writeObs;transport;state;core;events;terminalMgr;terminalHandler;localTerminalAuthorityRegistry;acpConnectionTerminalHandlers=new WeakMap;runtimeTerminalService;terminalOutbound=null;terminalServiceReady=!1;runtimeAgentCapabilities;agentProjection;markAgentProjectionDirty;templateOriginReporter;agentMappings;runtimeRpcConsumer;runtimeRpcConsumerFactory;canvasOrch;sessionMutationCoordinator;runOutputRouter;cronConversationDelivery;pendingToolCallSessionKeys=[];thirdPartyChannels;agentSyncRetryTimer=null;agentSyncInFlight=!1;runtimeServiceFactoryFenced=!1;legacyFencedSyncFailureCount=0;lifecyclePhase="stopped";lifecycleGeneration=0;activeGeneration;bridgeInstance=null;bridgeClosePromises=new WeakMap;consumerClosePromises=new WeakMap;teardownContext=new AsyncLocalStorage;bridgeConsecutiveFailures=0;static RE_DISCOVER_THRESHOLD=2;static LIFECYCLE_CONNECT_TIMEOUT_MS=1e4;static LIFECYCLE_TEARDOWN_TIMEOUT_MS=5e3;drainNextDeferredSend(e){const t=e.pendingAgentSends?.shift();t&&(e.pendingAgentSends?.length||(e.pendingAgentSends=void 0),this.agentSend(t.message,t.attachments,e.gatewaySessionKey).catch(e=>this.logger.warn?.(`[acp-server] Deferred agentSend failed: ${e instanceof Error?e.message:String(e)}`)))}async gatewayRequest(e,t){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");return this.gateway.request(e,t)}async chatAbort(e,t){const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");return i.chatAbort(e,t)}async agentSend(e,t,i){if(!this.gateway?.isConnected)throw new Error("Gateway not connected");const n=this.config.gateway.agentId||this.core.getDefaultAgentId();let r,s;i?(r=this.state.findSessionByKey(i),s=i,r?this.logger.info?.(`[acp-server] agentSend: resolved session for key=${i} sid=${r.sessionId}`):(r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: created session for key=${i} sid=${r.sessionId}`))):(s=`agent:${n}:ephemeral:${randomUUID()}`,r=this.core.ensureSessionForKey(s),this.logger.info?.(`[acp-server] agentSend: no targetSessionKey, created ephemeral session key=${s}`));const a=await this.sessionMutationCoordinator.acquireInternal({sessionKey:s,label:"agentSend"});let o=!1;try{const i=this.gateway;if(!i?.isConnected)throw new Error("Gateway not connected");if(r&&!r.promptDone&&(void 0!==r.pendingPromptId||void 0!==r.currentRunId||void 0!==r.gatewayRequestId))return(r.pendingAgentSends??=[]).push({message:e,attachments:t}),this.logger.info?.("[acp-server] Session has active run, deferred message (queue=%d)",r.pendingAgentSends.length),void this.sessionMutationCoordinator.releaseBeforeRuntime(a);o=!0;const c=await i.agentSend(s,e,n,t);return c?.runId&&r?(this.state.resetForNewRun(r,r.sessionId,this.transport),r.currentRunId=c.runId,this.sessionMutationCoordinator.bindRun(a,c.runId),this.core.observeActiveRun(r),r.assistantStreamStartedAt=Date.now(),r.writeGateToken=a,this.state.promptsByRunId.set(c.runId,r),openRunOutputRoute(this.runOutputRouter,r,c.runId,["acp"])):this.sessionMutationCoordinator.releaseBeforeRuntime(a),c}catch(e){throw o?this.sessionMutationCoordinator.requireRecovery(a,"terminal_not_observed"):this.sessionMutationCoordinator.releaseBeforeRuntime(a),e}}setCronConversationDelivery(e){this.cronConversationDelivery=e}getRuntimeCommandDispatcher(){return this.core.getRuntimeCommandDispatcher()}getActiveRunAuthority(){return this.core.getActiveRunAuthority()}nudgeMcpNativeProjection(){this.core.nudgeMcpNativeProjection()}ensureImInboundSession(e,t,i,n){const r=this.core.ensureSessionForKey(e);r.channelId=t,r.inboundMessageId=i,r.activeMentions=n.length>0?n:void 0}sendBridgeMessage=e=>{const t=this.client;if(t&&t.readyState===WebSocket.OPEN)try{t.send(JSON.stringify(e))}catch(e){this.logger.debug?.("[acp-server] bridge send failed (socket may be closing):",e)}else this.logger.debug?.("[acp-server] bridge not ready, dropping message")};constructor(e,t,i,n,r,s,a,o,c,l,h,g,d,u={}){this.agentFetchFn=r,this.instanceId=s,this.runtimeAuthority=c,this.mcpProjection=h,this.mcpCredentialReferences=g,this.boundAgentFetchFn=d,this.config=e,this.logger=t,this.writeObs=n??(()=>{}),this.thirdPartyChannels=a??new Set,this.sessionMutationCoordinator=i,this.gateway=u.gateway??null,this.agentProjection=u.agentProjection,this.markAgentProjectionDirty=u.markAgentProjectionDirty,this.templateOriginReporter=u.templateOriginReporter,this.agentMappings=u.agentMappings??new AgentMappingRegistry,this.runtimeRpcConsumerFactory=l,this.runtimeAgentCapabilities=u.agentCapabilities??{canvas:this.config.canvasEnabled,terminal:!1,webSsh:!1,fileSystem:!0,image:!0,audio:!1,mcp:!0,cron:!0,streaming:!0,pushNotifications:this.config.messageBus.enabled},this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("canvas",this.config.canvasEnabled),this.runtimeAuthority?.setRuntimeRealtimeCapabilityReady("terminal",!0===this.config.terminalServiceEnabled),this.transport=new AcpTransport(this.sendBridgeMessage,this.writeObs,void 0,()=>{const e=this.client;return!!e&&e.readyState===WebSocket.OPEN}),this.state=new AcpGatewaySessionState(this.logger);const m=(u.createRunOutputRouter??createAcpRunOutputRouter)({acp:new AcpRunOutputChannel({state:this.state,transport:this.transport}),lifecycle:{terminal:(e,t)=>this.core.commitRunOutputTerminal(e,t)}});this.runOutputRouter=m;const p=this.agentFetchFn&&this.config.agentGatewayUrl?new BeeosFileResolver({agentGatewayUrl:this.config.agentGatewayUrl,agentFetch:this.agentFetchFn,writeObs:this.writeObs}):void 0;this.core=new AcpGatewayBridgeCore({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getGateway:()=>this.gateway,instanceId:this.instanceId,getAuthoritativeInstanceId:()=>process.env.BEEOS_INSTANCE_ID?.trim()||this.getDefaultPlatformAgent()?.instanceId,thirdPartyChannels:this.thirdPartyChannels,beeosFileResolver:p,agentFetch:this.agentFetchFn,agentGatewayUrl:this.config.agentGatewayUrl,sessionMutationCoordinator:i,triggerAgentSync:()=>this.tryAgentSync(),runtimeAgentIdentities:this.runtimeAuthority?.identities,runtimeCommandJournal:this.runtimeAuthority?.journal,markAgentProjectionDirty:this.markAgentProjectionDirty,getAgentProjection:this.agentProjection?()=>this.agentProjection:void 0,templateOriginReporter:this.templateOriginReporter,mcpProjection:this.mcpProjection,mcpCredentialReferences:this.mcpCredentialReferences,runOutputRouter:m,agentMappings:this.agentMappings}),this.canvasOrch=new CanvasOrchestrator({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,getSessionIdForToolCall:e=>this.getSessionIdForToolCall(e),gateway:null,sendAgentMessage:(e,t)=>this.agentSend(e,void 0,t),resolvePlatformAgentId:e=>this.core.resolvePlatformAgentId(e)??""}),this.events=new AcpGatewayEvents({state:this.state,transport:this.transport,config:this.config,logger:this.logger,writeObs:this.writeObs,runOutputRouter:m,schedulePromptTimeout:e=>this.core.schedulePromptTimeout(e),adoptBackgroundMutation:(e,t)=>this.core.adoptBackgroundMutation(e,t),abortGatewayRun:async(e,t)=>{await this.chatAbort(e,t)},agentFetch:this.agentFetchFn,getCronConversationDelivery:()=>this.cronConversationDelivery??null,getCanvasState:()=>this.canvasOrch.canvasState,gatewayRequest:(e,t)=>{const i=this.gateway;return i?.isConnected?i.request(e,t):Promise.reject(new Error("gateway not connected"))},cronChannelCache:this.core.cronChannelCache}),this.core.onRunEnded=(e,t)=>{"complete"===t.kind?this.canvasOrch.onPromptComplete(e):this.canvasOrch.onPromptFailed(),this.drainNextDeferredSend(e)},this.terminalMgr=o??new TerminalManager,this.localTerminalAuthorityRegistry=new LocalTerminalAuthorityRegistry(this.terminalMgr),this.terminalHandler=this.createTerminalHandler(this.transport),this.runtimeTerminalService=getOrCreateRuntimeTerminalService(this.terminalMgr),this.core.setRuntimeTerminalService(this.runtimeTerminalService),this.core.handleTerminalRequest=e=>this.terminalHandler.handleRequest(e),this.core.handleTerminalNotification=(e,t)=>this.terminalHandler.handleNotification(e,t),this.core.handleCanvasNotification=(e,t)=>{this.canvasOrch.handleCanvasNotification(e,t)},this.core.getCanvasState=()=>this.canvasOrch.canvasState,this.core.onSessionLoaded=(e,t,i)=>{this.canvasOrch.onSessionLoaded(e,t,i)},this.core.onSkillInstalled=(e,t)=>{this.registerSkillFormSchemas(e,t)},this.core.onSessionDeleted=e=>{this.canvasOrch.onSessionDeleted(e)},this.core.onSessionCleared=(e,t)=>{this.canvasOrch.onSessionCleared(e,t)},this.core.handleSlashCommand=async e=>this.canvasOrch.handleSlashCommand(e),this.core.onPromptStart=()=>{this.canvasOrch.onPromptStart()},this.state.startSessionGc(e=>{e.writeGateToken&&this.sessionMutationCoordinator.requireRecovery(e.writeGateToken,"terminal_not_observed"),this.core.cleanupPrompt(e)},e=>{this.canvasOrch.canvasState?.removeSession(e.gatewaySessionKey),this.transport.resetSessionUpdateCount(e.sessionId)}),this.gateway&&this.bindGateway(this.gateway)}setRuntimeRpcConsumerFactory(e){if(e!==this.runtimeRpcConsumerFactory){if("stopped"!==this.lifecyclePhase)throw new Error("runtime Service factory is immutable after ACP start");this.runtimeRpcConsumerFactory=e}}async rebindRuntimeDelivery(){if("started"!==this.lifecyclePhase)return;const e=this.activeGeneration,t=e?.factory??this.runtimeRpcConsumerFactory,i=e?.authority??this.runtimeAuthority;if(!e||!t||!i)return;if(!i.authorityLease)return;const n=i.currentConsumerBinding();if(!n)return void this.logger.warn?.("[acp-server] runtime Service delivery rebind skipped — no consumer binding");const r=e.runtimeConsumer;let s;r&&(e.runtimeConsumer=void 0,this.runtimeRpcConsumer===r&&(this.runtimeRpcConsumer=void 0),this.core.setRuntimeReceiptHistoryPort(void 0),await this.closeConsumerOnce(r).catch(()=>{}));try{s=await t.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:n,getBinding:()=>i.currentConsumerBinding()??n,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal})}catch(e){this.logger.error?.(`[acp-server] runtime Service delivery rebind failed: ${e instanceof Error?e.stack??e.message:String(e)}`);try{i.setRuntimeConsumerLive(!1)}catch{}return}if(this.activeGeneration!==e||"started"!==this.lifecyclePhase||e.abortController.signal.aborted)await this.closeConsumerOnce(s).catch(()=>{});else{this.core.setRuntimeReceiptHistoryPort(s.receiptHistory),e.runtimeConsumer=s,this.runtimeRpcConsumer=s,s.start();try{i.setRuntimeConsumerLive(!0)}catch{}this.runtimeServiceFactoryFenced=!1,this.logger.info?.("[acp-server] runtime Service delivery rebound after MessageClient connect")}}start(e){const t=this.activeGeneration;if("starting"===this.lifecyclePhase||"started"===this.lifecyclePhase)return this.logger.info?.("[acp-server] start() skipped — already started (singleton reuse)"),t?.startPromise??Promise.resolve();if("stopping"===this.lifecyclePhase){return(t?.stopPromise??Promise.resolve()).then(()=>this.start(e),()=>this.start(e))}const i={id:++this.lifecycleGeneration,factory:this.runtimeRpcConsumerFactory,authority:this.runtimeAuthority,abortController:new AbortController,startPromise:Promise.resolve(),authorityOwned:!1};return this.lifecyclePhase="starting",this.activeGeneration=i,i.startPromise=this.startGeneration(i,e).then(()=>{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.lifecyclePhase="started")}).catch(async e=>{try{await this.teardownGeneration(i)}catch(e){this.logger.debug?.(`[acp-server] failed-start teardown also failed: ${e instanceof Error?e.message:String(e)}`)}finally{this.activeGeneration===i&&"starting"===this.lifecyclePhase&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}throw e}),i.startPromise}async startGeneration(e,t){const i=e.authority,n=e.factory;let r=!1;if(i&&n){e.authorityOwned=!0;const t=await this.waitForGeneration(i.prepareConsumerBinding(),e);if("completed"!==t.status||!this.shouldContinueStart(e))return;const s=n.create({dispatcher:this.core.getRuntimeCommandDispatcher(),binding:t.value,getBinding:()=>i.currentConsumerBinding()??t.value,getLease:()=>i.authorityLease,createRuntimeSession:async(e,t)=>await this.core.createRuntimeSession(e,t),signal:e.abortController.signal}),a=await this.waitForGeneration(s,e);if("completed"!==a.status)return void("cancelled"===a.status&&s.then(e=>this.closeConsumerOnce(e),()=>{}));const o=a.value;if(!this.shouldContinueStart(e))return void this.closeConsumerOnce(o,e.abortController.signal).catch(()=>{});if(this.core.setRuntimeReceiptHistoryPort(o.receiptHistory),e.runtimeConsumer=o,this.runtimeRpcConsumer=o,o.start(),!this.shouldContinueStart(e))return;i.setRuntimeConsumerLive(!0),r=!0,this.runtimeServiceFactoryFenced=!1}else i&&(e.authorityOwned=!0),i?.setRuntimeConsumerLive(!1),i&&(this.runtimeServiceFactoryFenced=!0,this.logger.warn?.("[acp-server] runtime Service factory unavailable — registration fenced"));if(this.core.startMcp().catch(e=>{this.logger.warn?.(`[acp-server] MCP desired-state reconcile failed: ${e instanceof Error?e.message:String(e)}`)}),!this.shouldContinueStart(e))return;if(i&&r){if("completed"!==(await this.waitForGeneration(i.start(e.abortController.signal),e)).status||!this.shouldContinueStart(e))return}const s=Date.now();this.logger.info?.("[acp-server] start() begin"),t&&(this.config.bridge.url=t);const a=this.config.bridge.url,o=this.config.keyFile;if(a&&!BridgeClientCtor)try{const t=Date.now(),i=await this.waitForGeneration(import("@beeos-ai/bridge-client"),e);if("completed"!==i.status)return;BridgeClientCtor=i.value.BridgeClient,this.logger.info?.(`[acp-server] bridge-client import took ${Date.now()-t}ms`)}catch(e){this.logger.warn?.("[acp-server] Bridge SDK not available")}if(this.shouldContinueStart(e)){if(!0===this.config.terminalServiceEnabled&&a&&i){const t=new TerminalLeaseBoundBridgeClient({bridgeUrl:a,authority:i,keys:i.agentKeys}),n=new RuntimeTerminalOutboundRegistration({client:t,manager:this.terminalMgr,service:this.runtimeTerminalService,authorityJournal:new FileTerminalAuthorityJournal(join(i.stateDirectory,"terminal-authority-journal.json")),onAvailabilityChange:t=>this.setTerminalServiceReady(e,t),logger:this.logger});if(!this.shouldContinueStart(e))return void n.drain({timeoutMs:5e3,terminateAfterTimeout:!0,signal:e.abortController.signal}).catch(()=>{});e.terminalOutbound=n,this.terminalOutbound=n;try{const t=await this.waitForGeneration(n.start(),e,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===t.status)return;if("timed_out"===t.status)throw new Error("Terminal connection timed out")}catch(e){this.logger.warn?.(`[terminal-service] initial outbound registration failed; no legacy fallback: ${e instanceof Error?e.message:String(e)}`)}if(!this.shouldContinueStart(e))return}else!0!==this.config.terminalServiceEnabled||this.runtimeAuthority||this.logger.warn?.("[terminal-service] disabled: active Runtime registration authority unavailable");try{const t=await this.waitForGeneration(sweepStaleSessionLocks({logger:this.logger}),e);if("completed"!==t.status)return;const i=t.value;this.logger.info?.(`[acp-server] stale lock sweep scanned=${i.scanned} quarantined=${i.quarantined} skipped=${i.skipped}`)}catch(e){this.logger.warn?.(`[acp-server] stale lock sweep failed: ${e instanceof Error?e.message:String(e)}`)}if(this.shouldContinueStart(e)){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.markAgentProjectionDirty?this.markAgentProjectionDirty():this.tryAgentSync())}async waitForGeneration(e,t,i){const n=t.abortController.signal;return n.aborted?(e.catch(()=>{}),{status:"cancelled"}):await new Promise((t,r)=>{let s,a=!1;const o=e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),t(e))},c=()=>o({status:"cancelled"});n.addEventListener("abort",c,{once:!0}),void 0!==i&&(s=setTimeout(()=>o({status:"timed_out"}),i),s.unref?.()),e.then(e=>o({status:"completed",value:e}),e=>{a||(a=!0,n.removeEventListener("abort",c),s&&clearTimeout(s),r(e))})})}closeConsumerOnce(e,t){const i=this.consumerClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.stop(t));return this.consumerClosePromises.set(e,n),n}async startBridgeMode(e,t,i){if(!BridgeClientCtor)throw new Error("@beeos-ai/bridge-client not available");if(!this.isGenerationActive(i))return;const n=new BridgeClientCtor({bridgeUrl:e,service:this.config.bridge.service||"acp",keyFile:t});if(!this.isGenerationActive(i))return void this.closeBridgeOnce(n,!1).catch(()=>{});i.bridge=n,this.bridgeInstance=n;const r=()=>this.bridgeInstance===n;n.on("connected",()=>{r()&&(this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.bridgeConsecutiveFailures=0,this.logger.info?.("[acp-server] Connected to Bridge (publicKey auth)"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"bridge.connected",severity:"info"}),this.tryAgentSync())}),n.on("disconnected",(...e)=>{if(!r())return;this.terminalHandler.destroy();const s=String(e[0]??"unknown"),a=formatBridgeEventArgs(e);if(this.logger.warn?.(`[acp-server] Bridge disconnected: ${s} (detail=${a})`),this.core.initialized=!1,this.bridgeConsecutiveFailures++,this.bridgeConsecutiveFailures>=ACPServer.RE_DISCOVER_THRESHOLD&&this.agentFetchFn&&this.config.agentGatewayUrl){this.bridgeConsecutiveFailures=0;const e=this.config.bridge.url;discoverBridgeFromGW(this.config.agentGatewayUrl,this.agentFetchFn).then(async s=>{if(this.isGenerationActive(i)&&r()&&s&&s.url!==e){if(this.logger.info?.(`[acp-server] Bridge re-discovery: URL changed ${e} → ${s.url}, switching`),this.config.bridge.url=s.url,writeDiscoveryCache({bridge:{url:s.url,discoveredAt:Date.now()}}),await this.closeBridgeOnce(n,!1),!this.isGenerationActive(i)||this.bridgeInstance!==n)return;i.bridge===n&&(i.bridge=void 0),this.activeGeneration!==i||"starting"!==this.lifecyclePhase&&"started"!==this.lifecyclePhase||await this.startBridgeMode(s.url,t,i)}}).catch(()=>{})}}),n.on("message",(...e)=>{if(!r())return;const t=e[0],i="string"==typeof t?t:Buffer.from(t).toString("utf-8");try{const e=JSON.parse(i);"id"in e&&void 0!==e.id?this.core.handleRequest(e).catch(e=>{this.logger.error?.(`[acp-server] Bridge request failed: ${e instanceof Error?e.message:String(e)}`)}):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message from Bridge")}}),n.on("error",(...e)=>{if(!r())return;const t=formatBridgeEventArgs(e);this.logger.error?.(`[acp-server] Bridge error: ${t}`)});const s={readyState:WebSocket.OPEN,send:(e,t)=>{n.send(e).then(()=>t?.(),e=>{const i=e instanceof Error?e:new Error(String(e));this.logger.warn?.(`[acp-server] bridge.send failed: ${i.message}`),t?.(i)})},close:()=>{this.closeBridgeOnce(n).catch(e=>{this.logger.debug?.(`[acp-server] bridge.close failed: ${e instanceof Error?e.message:String(e)}`)})},terminate:()=>{this.closeBridgeOnce(n,!1).catch(e=>{this.logger.debug?.(`[acp-server] bridge.terminate failed: ${e instanceof Error?e.message:String(e)}`)})},on:()=>s,once:()=>s,removeListener:()=>s,removeAllListeners:()=>s,ping:()=>{}};Object.defineProperty(s,"readyState",{get:()=>"connected"===n.state?WebSocket.OPEN:WebSocket.CLOSED}),i.bridgeSocket=s,this.terminalHandler.destroy(),this.terminalHandler=this.createTerminalHandler(this.transport),this.client=s;const a=Date.now();try{const e=await this.waitForGeneration(n.connect(),i,ACPServer.LIFECYCLE_CONNECT_TIMEOUT_MS);if("cancelled"===e.status)return;if("timed_out"===e.status)throw new Error("Bridge connection timed out");this.logger.info?.(`[acp-server] bridge.connect() took ${Date.now()-a}ms`)}catch(e){this.logger.warn?.(`[acp-server] Initial bridge connect failed after ${Date.now()-a}ms (will retry in background): ${e?.message}`)}this.core.initialized=!1,this.logger.info?.("[acp-server] Running in Bridge mode (WS client)")}closeBridgeOnce(e,t){const i=this.bridgeClosePromises.get(e);if(i)return i;const n=Promise.resolve().then(()=>e.close(t));return this.bridgeClosePromises.set(e,n),n}handleUpgrade(e,t,i){this.wss||(this.wss=new WebSocketServer({noServer:!0}),this.wss.on("connection",(e,t)=>this.handleConnection(e,t))),this.wss.handleUpgrade(e,t,i,t=>{this.wss.emit("connection",t,e)})}async tryAgentSync(e=0){if(this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),!this.client||this.client.readyState!==WebSocket.OPEN)return;if(!this.gateway||!this.agentFetchFn||!this.config.agentGatewayUrl)return void this.core.markAgentSyncDone();if(this.agentProjection){if(this.runtimeAuthority?.authorityLease)return;if(!this.runtimeServiceFactoryFenced)return;0===e&&this.logger.error?.("[acp-server] runtime Service factory fenced with no authority lease — attempting one-shot legacy agent-sync bootstrap to recover Agent Identity")}const t=!!this.agentProjection;if(t||(this.legacyFencedSyncFailureCount=0),this.agentSyncInFlight)return;const i=[1e3,2e3,5e3,1e4],n=3e5,r=()=>{const r=e+1;if(t&&(this.legacyFencedSyncFailureCount+=1,this.legacyFencedSyncFailureCount>=4)){const e=Math.round(5);return this.logger.error?.(`[acp-server] legacy agent-sync bootstrap fallback has failed ${this.legacyFencedSyncFailureCount} consecutive times while the runtime Service factory is fenced — this fallback path is confirmed incompatible with a backend that requires the runtime-projection agent-sync schema and cannot self-heal; restart this instance once Message Service / the runtime Service factory recover. Backing off to a ${e}m retry interval to stop busy-looping a request that cannot succeed.`),void(this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},n))}const s=i[Math.min(e,i.length-1)],a=Math.round(s/1e3),o=`[acp-server] agent sync retry #${r} in ${a}s`;r>0&&r%30==0?this.logger.warn?.(`[acp-server] agent sync still failing after ${r} attempts — retrying every ${a}s`):r<=i.length?this.logger.info?.(o):this.logger.debug?.(o),this.agentSyncRetryTimer=setTimeout(()=>{this.tryAgentSync(r)},s)},s=Date.now();this.agentSyncInFlight=!0;try{const e=await syncAgentsToPlatform({gateway:this.gateway,agentGatewayUrl:this.config.agentGatewayUrl,capabilities:{canvas:this.config.canvasEnabled,terminal:this.terminalServiceReady,fileSystem:!0,cron:!0,streaming:!0},deliveryMode:this.config.messageBus.enabled?"push":void 0,boundAgentFetch:this.boundAgentFetchFn,logger:this.logger});isAgentSyncWaitForGateway(e)?this.logger.info?.("[acp-server] agent sync waiting for OpenClaw Gateway connection"):e?(this.legacyFencedSyncFailureCount=0,await this.core.setAgentMapping(e),this.logger.info?.(`[acp-server] agent mapping loaded: ${[...e.keys()].join(", ")} (sync + session hydration took ${Date.now()-s}ms)`),this.agentProjection&&this.runtimeServiceFactoryFenced&&this.logger.error?.("[acp-server] agent bootstrap sync recovered Agent Identity while the runtime Service factory was fenced — restart this instance to complete registration")):r()}catch(e){this.logger.warn?.(`[acp-server] agent sync failed: ${e}`),r()}finally{this.agentSyncInFlight=!1}}dispatchGatewayEvent(e,t,i){try{i()}catch(i){const n=i instanceof Error?i.message:String(i),r=n.startsWith("run output route unavailable:");this.logger.error?.(`[acp-server] Gateway ${e} handler failed${t?` runId=${t}`:""}: ${n}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:r?"run_output.route_missing":"gateway_event.handler_failed",severity:"error",summary:n,payload:{event:e,...t?{runId:t}:{}}})}}bindGateway(e){e.on("chat",e=>{this.dispatchGatewayEvent("chat",e.runId,()=>this.events.handleChatEvent(e))}),e.on("agent",e=>{this.dispatchGatewayEvent("agent",e.runId,()=>this.events.handleAgentEvent(e))}),e.on("cron",e=>{this.dispatchGatewayEvent("cron",void 0,()=>this.events.handleCronEvent(e))}),e.on("sessionMessage",e=>{this.dispatchGatewayEvent("session.message",void 0,()=>this.core.handleSessionMessage(e))}),e.on("response",e=>{this.dispatchGatewayEvent("response",void 0,()=>this.core.handleGatewayResponse(e))});const t=e;this.canvasOrch.updateDeps({gateway:t?{get isConnected(){return t.isConnected},request:(e,t)=>this.gateway.request(e,t)}:null}),this.config.openclawCanvasCompat&&(e.on("canvas",e=>this.canvasOrch.handleOpenClawCanvasEvent(e)),this.logger.info?.("[acp-server] OpenClaw native canvas compat enabled — listening for canvas events")),e.on("connected",()=>{this.core.beginGatewaySessionPreload(),this.logger.info?.("[acp-server] Connected to OpenClaw Gateway"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.connected",severity:"info"}),this.core.resubscribeAllSessionMessages(),this.core.refreshCronIsolatedSessionKeys(),this.tryAgentSync()}),e.on("disconnected",()=>{this.core.failGatewaySessionPreload(),this.logger.warn?.("[acp-server] Gateway disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"gateway.disconnected",severity:"warn"}),this.core.cleanupAllPendingPrompts()}),e.on("reconnecting",e=>{this.logger.info?.(`[acp-server] Reconnecting to Gateway attempt=${e.attempt} delay=${Math.round(e.delayMs)}ms`)}),e.on("reconnect-exhausted",e=>{this.logger.error?.(`[acp-server] Gateway reconnect exhausted after ${e} attempts`)}),e.on("error",e=>{this.logger.error?.("[acp-server] Gateway error:",e.message)}),e.on("transport",e=>{const t="connect_fail"===e.event&&!0===e.retryable,i=`[acp-server] transport.${e.event} durationMs=${e.durationMs??"?"}`;t?this.logger.debug?.(i):this.logger.info?.(i),this.writeObs({component:"gateway-client",domain:"lifecycle",name:`transport.${e.event}`,severity:t?"debug":"connect_fail"===e.event||"error"===e.event?"warn":"debug",payload:e})}),e.on("reconnect-notify-scheduled",e=>{this.logger.info?.(`[acp-server] Server-initiated reconnect scheduled in ${e.delayMs}ms`)}),e.on("reconnect-notify-fired",()=>{this.logger.info?.("[acp-server] Server-initiated reconnect firing")})}handleConnection(e,t){if(this.client){this.logger.warn?.("[acp-server] Replacing existing ACP client with new connection (takeover)");const e=this.client;this.client=null;const t=this.acpConnectionTerminalHandlers.get(e);t?t.destroy():this.terminalHandler.destroy(),this.acpConnectionTerminalHandlers.delete(e);try{e.close(1008,"replaced by new client")}catch{e.terminate()}}this.client=e;const i=this.createTerminalHandler(new AcpTransport(t=>{if(this.client===e&&e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch(e){this.logger.debug?.("[acp-server] connection terminal send failed",e)}},this.writeObs));this.acpConnectionTerminalHandlers.set(e,i),this.core.initialized=!1,this.logger.info?.("[acp-server] ACP client connected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.connected",severity:"info"}),e.on("message",t=>{if(this.client!==e)return;const n="string"==typeof t?t:t.toString("utf-8");try{const e=JSON.parse(n);if("id"in e&&void 0!==e.id){(e.method.startsWith("terminal/")?i.handleRequest(e):this.core.handleRequest(e)).catch(e=>{this.logger.error?.(`[acp-server] request failed: ${e instanceof Error?e.message:String(e)}`)})}else e.method.startsWith("terminal/")?i.handleNotification(e.method,e.params):this.core.handleNotification(e)}catch{this.logger.error?.("[acp-server] Invalid JSON-RPC message")}}),e.on("close",()=>{i.destroy(),this.acpConnectionTerminalHandlers.delete(e),this.client===e&&(this.client=null,this.core.initialized=!1),this.logger.info?.("[acp-server] ACP client disconnected"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"client.disconnected",severity:"info"})})}createTerminalHandler(e){const t=new TerminalBridgeHandler({terminalMgr:this.terminalMgr,transport:e,config:this.config,logger:this.logger,writeObs:this.writeObs,localAuthority:this.localTerminalAuthorityRegistry.forPrincipal(this.localTerminalAuthorityRegistry.issuePrincipal()),executeCanonical:(e,t,i,n)=>this.core.dispatchLegacyTerminalCapability(e,t,i,n)});return t.setupListeners(),t}setCanvasState(e,t){this.canvasOrch.setCanvasState(e,t)}wireRelayActionHandler(){this.canvasOrch.wireRelayActionHandler()}isCanvasActive(e){return this.canvasOrch.isCanvasActive(e)}isCanvasActiveForGatewayKey(e){return this.canvasOrch.isCanvasActiveForGatewayKey(e)}broadcastNotification(e,t){this.transport.sendNotification(e,t)}getActiveSessionId(){return this.state.lastActiveSessionId}getSessionIdForToolCall(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(t){const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);if(i?.sessionId)return e&&this.state.toolCallToSessionMap.set(e,i.sessionId),i.sessionId}if(e){const t=this.state.toolCallToSessionMap.get(e);if(t)return t;const i=this.state.toolCallIdMap.get(e);if(i&&i!==e){const e=this.state.toolCallToSessionMap.get(i);if(e)return e}const n=e.split("_");if(n.length>=3&&"tc"===n[0]){const e=n[1],t=this.state.promptsByRunId.get(e);if(t?.sessionId)return t.sessionId}}this.logger.warn?.(`[acp-server] getSessionIdForToolCall MISS: toolCallId=${e??"(none)"}, sessions=${this.state.sessions.size}, pendingKey=${t??"none"}`)}getSessionIdForActivePrompt(){return this.getSessionIdForToolCall()}setPendingToolCallSessionKey(e){e&&(this.pendingToolCallSessionKeys.push(e),this.pendingToolCallSessionKeys.length>16&&this.pendingToolCallSessionKeys.shift())}preMapToolCallSession(e){const t=this.pendingToolCallSessionKeys.length>0?this.pendingToolCallSessionKeys[this.pendingToolCallSessionKeys.length-1]:void 0;if(!t||!e)return;if(this.state.toolCallToSessionMap.has(e))return;const i=this.state.findSessionByKey(t)??this.core.ensureSessionForKey(t);i?.sessionId&&this.state.toolCallToSessionMap.set(e,i.sessionId)}waitForAgentMapping(){return this.agentMappings.waitForSynchronization()}onceAgentMappingReady(e){this.agentMappings.onceReady(e)}onAgentMappingUpdated(e){const t=this.agentMappings.snapshot();t&&t.size>0&&e(new Map(t)),this.agentMappings.subscribe(t=>e(new Map(t)))}flushPendingCanvasLinks(){this.canvasOrch.flushPendingCanvasLinks()}onRelayReconnected(){this.canvasOrch.onRelayReconnected()}getLocalAgentId(){return this.config.gateway.agentId||this.core.getDefaultAgentId()}resolveLocalAgentIdForPlatformAgent(e){return this.core.resolveLocalAgentId(e)}getDefaultPlatformAgentId(){return this.getDefaultPlatformAgent()?.id}getDefaultPlatformAgent(){const e=this.agentMappings.snapshot();if(e&&0!==e.size)return e.values().next().value??void 0}getPlatformAgentMappings(){const e=this.agentMappings.snapshot();return e&&0!==e.size?[...e.entries()].map(([e,t])=>({localAgentId:e,platformAgent:t})):[]}getPlatformAgentIdForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return this.core.resolvePlatformAgentId(i?.agentId??"")??i?.platformAgentId}getGatewaySessionKeyForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.gatewaySessionKey}getMentionsForToolCall(e){const t=this.getSessionIdForToolCall(e);if(!t)return;const i=this.state.sessions.get(t);return i?.activeMentions}getResourceRenderer(){return this.canvasOrch.resourceRenderer}getDescriptorRegistry(){return this.canvasOrch.descriptorRegistry}getFormBuilder(){return this.canvasOrch.formBuilder}getSchemaRegistry(){return this.canvasOrch.schemaRegistry}registerSkillFormSchemas(e,t){const i=this.canvasOrch.schemaRegistry;i&&import("node:fs/promises").then(async({readFile:n})=>{try{const r=`${t}/skill.json`,s=await n(r,"utf-8"),a=JSON.parse(s);if(a.formSchemas?.length){for(const e of a.formSchemas)i.register({...e,submitLabel:e.submitLabel||"Submit",fields:e.fields});this.logger.info?.(`[acp-server] Registered ${a.formSchemas.length} form schema(s) from skill "${e}"`)}}catch{}}).catch(e=>{this.logger.debug?.(`[acp-server] skill form schema import failed: ${e instanceof Error?e.message:String(e)}`)})}emitCanvasActionAsUserMessage(e,t){this.canvasOrch.emitCanvasActionAsUserMessage(e,t)}stop(){const e=this.teardownContext.getStore();if(e?.server===this){const t=e.generation;return this.activeGeneration!==t?t.stopPromise??t.teardownPromise??Promise.resolve():(e.detachOwner(),t.stopPromise??Promise.resolve())}const t=this.activeGeneration;if(!t||"stopped"===this.lifecyclePhase)return Promise.resolve();if("stopping"===this.lifecyclePhase)return t.stopPromise??Promise.resolve();let i,n;const r=new Promise((e,t)=>{i=e,n=t});t.stopPromise=r,this.lifecyclePhase="stopping",t.abortController.abort();const s=this.teardownGeneration(t);return(async()=>{try{t.startPromise.catch(()=>{}),await s,i()}catch(e){n(e)}finally{this.activeGeneration===t&&(this.activeGeneration=void 0,this.lifecyclePhase="stopped")}})(),r}teardownGeneration(e){return e.teardownPromise||(e.teardownPromise=this.teardownGenerationOnce(e)),e.teardownPromise}async teardownGenerationOnce(e){const t=[],i=new AbortController;let n,r=!1;const s=new Promise(e=>{n=setTimeout(()=>{r=!0,i.abort(),e()},ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS),n.unref?.()}),a=async(n,r)=>{let a;const o=new Promise(e=>{a=e}),c=Promise.resolve().then(()=>this.teardownContext.run({server:this,generation:e,detachOwner:a},()=>r(i.signal))),l=await Promise.race([c.then(()=>({status:"completed"}),e=>({status:"failed",error:e})),s.then(()=>({status:"timed_out"})),o.then(()=>({status:"detached"}))]);if("failed"===l.status)t.push(l.error);else if("timed_out"===l.status){const i=new Error(`lifecycle teardown timed out: ${n}`);t.push(i),this.logger.error?.(`[acp-server] ${i.message} generation=${e.id}`),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.teardown_timeout",severity:"error",payload:{owner:n,generation:e.id,timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS}})}},o=[];this.setTerminalServiceReady(e,!1),o.push(a("session-mutation-coordinator",()=>this.sessionMutationCoordinator.fenceActive("gateway_disconnected"))),this.agentSyncRetryTimer&&(clearTimeout(this.agentSyncRetryTimer),this.agentSyncRetryTimer=null),o.push(a("session-state",()=>this.state.destroy())),o.push(a("canvas-orchestrator",()=>this.canvasOrch.destroy())),o.push(a("mcp-receipt-history",()=>this.core.setRuntimeReceiptHistoryPort(void 0)));const c=e.bridge,l=e.bridgeSocket,h=e.terminalOutbound,g=e.runtimeConsumer,d=this.client&&this.client!==l?this.client:null,u=this.wss;if(e.bridge=void 0,e.bridgeSocket=void 0,e.terminalOutbound=void 0,e.runtimeConsumer=void 0,this.bridgeInstance===c&&(this.bridgeInstance=null),this.client!==d&&this.client!==l||(this.client=null),this.terminalOutbound===h&&(this.terminalOutbound=null),this.runtimeRpcConsumer===g&&(this.runtimeRpcConsumer=void 0),this.wss=null,this.bridgeConsecutiveFailures=0,d&&o.push(a("incoming-client",()=>{d.removeAllListeners(),d.close()})),c&&o.push(a("bridge",async()=>{try{await this.closeBridgeOnce(c)}catch(e){this.logger.debug?.(`[acp-server] bridge.close failed during stop: ${e instanceof Error?e.message:String(e)}`)}})),h&&o.push(a("terminal-outbound",e=>h.drain({timeoutMs:ACPServer.LIFECYCLE_TEARDOWN_TIMEOUT_MS,terminateAfterTimeout:!0,signal:e}))),e.authorityOwned&&(e.authorityOwned=!1,o.push(a("runtime-authority",()=>e.authority?.stop()))),g&&o.push(a("runtime-consumer",e=>this.closeConsumerOnce(g,e))),u&&o.push(a("websocket-server",()=>{u.removeAllListeners(),u.close()})),await Promise.all(o),await a("runtime-authority-reset",()=>e.authority?.setRuntimeConsumerLive(!1)),await a("terminal-handler",()=>this.terminalHandler.destroy()),await a("terminal-manager",()=>this.terminalMgr.destroy()),n&&clearTimeout(n),r||i.abort(),this.logger.info?.("[acp-server] Stopped"),this.writeObs({component:"acp-server",domain:"lifecycle",name:"lifecycle.shutdown",severity:"info"}),t.length>0)throw t[0]}}
@@ -1 +1 @@
1
- import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const MIN_PROTOCOL_VERSION=3,MAX_PROTOCOL_VERSION=4,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,CHAT_HISTORY_PAGE_TIMEOUT_MS=1e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";export class GatewayHandshakeError extends Error{code;retryable;retryAfterMs;constructor(e){super(e?.message||"Gateway connect rejected"),this.name="GatewayHandshakeError",this.code=e?.code,this.retryable=!0===e?.retryable||"UNAVAILABLE"===e?.code,this.retryAfterMs="number"==typeof e?.retryAfterMs?e.retryAfterMs:void 0}}export function decodeChatAbortResult(e){if("object"!=typeof e||null===e||Array.isArray(e))throw new Error("Invalid chat.abort response: expected object");const t=e;if("boolean"!=typeof t.aborted)throw new Error("Invalid chat.abort response: expected boolean aborted");if(!Array.isArray(t.runIds)||t.runIds.some(e=>"string"!=typeof e))throw new Error("Invalid chat.abort response: expected string[] runIds");return{aborted:t.aborted,runIds:[...t.runIds]}}function wsUrlToHttpBase(e){try{const t=new URL(e),n="wss:"===t.protocol?"https:":"ws:"===t.protocol?"http:":null;return n?`${n}//${t.host}`:null}catch{return null}}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;nextReconnectDelayHintMs=null;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let i=null;const r=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),o=t=>{clearTimeout(r),this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},c=e=>{const i="string"==typeof e?e:e.message;clearTimeout(r),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:i,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:i}),s.close(),t("string"==typeof e?new Error(e):e)};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const a="string"==typeof e?e:e.toString("utf-8");if("pong"===a)return void(this.lastPongTs=Date.now());let h;this.emitMessageHash(a);try{h=JSON.parse(a)}catch{return}const l=h.method;l&&!h.id&&(this.isReconnectNotification(l)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",l,h.params));const d=h.type;if("event"===d&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===d){const e=h.event,t=h.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n});let r=0;const o=setInterval(()=>{r++},200),c=Date.now(),a=setInterval(()=>{const e=Date.now()-c,t=e-200*r;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:r})},200),h=()=>{clearInterval(o),clearInterval(a),this.off("connected",h),this.off("disconnected",h)};setTimeout(h,3e4),this.once("connected",h),this.once("disconnected",h)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):"session.message"===e?this.emit("sessionMessage",t):this.emit("gateway-event",e,t)}else if("hello-ok"===d)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),o(h);else if("hello-error"===d||"connect-error"===d)c("Authentication rejected by Gateway");else if("res"===d){const e=h.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!h.ok,durationMs:Date.now()-n}),h.ok){const e=h.payload;o(e??h)}else{const e=h.error,i=new GatewayHandshakeError(e);i.retryable?(clearTimeout(r),this.emit("transport",{event:"connect_fail",reason:i.message,retryable:!0,retryAfterMs:i.retryAfterMs,durationMs:Date.now()-n}),"number"==typeof i.retryAfterMs&&i.retryAfterMs>0&&(this.nextReconnectDelayHintMs=i.retryAfterMs),s.close(),t(i)):c(i)}return}const a=this.pending.get(e);if(a)if(this.pending.delete(e),clearTimeout(a.timer),h.ok)a.resolve(h.payload);else{const e=h.error;a.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",h)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=this.connected;this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(r),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.stopped?clearTimeout(r):(this.connected||(clearTimeout(r),t(e)),this.emit("error",e))})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);let t;const n=this.nextReconnectDelayHintMs,s=null!==n&&n>0;s?(t=Math.min(n,e.maxMs),this.nextReconnectDelayHintMs=null):(t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs),this.reconnectAttempt++),this.emit("reconnecting",{attempt:s?this.reconnectAttempt+1:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emitReconnectFailure(e)})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emitReconnectFailure(e)}))}emitReconnectFailure(e){this.stopped||this.authFailed||this.emit("error",e instanceof Error?e:new Error(String(e)))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",i=["operator.admin","operator.approvals","operator.pairing"],r={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:i,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();r.nonce=e,r.signature=signDeviceAuth(this.deviceIdentity,o);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const o=randomUUID(),c={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:4,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:i,auth:{token:this.config.token},device:r}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const u=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:u}),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),i={type:"req",id:s,method:e,params:t};return new Promise((t,r)=>{const o=setTimeout(()=>{this.pending.delete(s),r(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:r,timer:o}),this.ws.send(JSON.stringify(i))})}async agentSend(e,t,n,s,i){const r={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(r.attachments=s),i&&(r.inputProvenance=i);return await this.request("agent",r,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){const n={sessionKey:e};t&&(n.runId=t);return decodeChatAbortResult(await this.request("chat.abort",n))}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async chatHistoryPage(e,t){const n=wsUrlToHttpBase(this.config.url);if(!n)return null;let s;try{s=new URL(`${n}/sessions/${encodeURIComponent(e)}/history`)}catch{return null}null!=t?.limit&&s.searchParams.set("limit",String(t.limit)),t?.cursor&&s.searchParams.set("cursor",t.cursor);try{const e=await fetch(s,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${this.config.token}`},signal:AbortSignal.timeout(1e4)});if(!e.ok)return null;const t=await e.json(),n=Array.isArray(t.messages)?t.messages:Array.isArray(t.items)?t.items:[];return{messages:n.filter(e=>"object"==typeof e&&null!==e),nextCursor:"string"==typeof t.nextCursor&&t.nextCursor.length>0?t.nextCursor:void 0,hasMore:!0===t.hasMore}}catch{return null}}async sessionsMessagesSubscribe(e){return this.request("sessions.messages.subscribe",{key:e})}async sessionsMessagesUnsubscribe(e){return this.request("sessions.messages.unsubscribe",{key:e})}async skillsStatus(e){return this.request("skills.status",e?{agentId:e}:{})}async skillsUpdate(e,t,n){return this.request("skills.update",{skillKey:e,enabled:t,...n?{agentId:n}:{}})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
1
+ import{WebSocket}from"ws";import{EventEmitter}from"node:events";import{randomUUID,createHash}from"node:crypto";import{loadOrCreateDeviceIdentity,buildDeviceAuthPayload,signDeviceAuth,ensureDevicePaired,warmupCryptoSign}from"./device-identity.js";const MIN_PROTOCOL_VERSION=3,MAX_PROTOCOL_VERSION=4,CONNECT_TIMEOUT_MS=3e4,HEARTBEAT_INTERVAL_MS=15e3,LIVENESS_TIMEOUT_MS=6e4,CHAT_HISTORY_PAGE_TIMEOUT_MS=1e4,AUTH_FAILURE_CLOSE_CODE=4001,EL_LAG_INTERVAL_MS=200,EL_LAG_THRESHOLD_MS=500,RECONNECT_NOTIFY_MIN_MS=6e4,RECONNECT_NOTIFY_MAX_MS=18e4,RECONNECT_NOTIFY_METHOD="server/reconnect";export class GatewayHandshakeError extends Error{code;retryable;retryAfterMs;constructor(e){super(e?.message||"Gateway connect rejected"),this.name="GatewayHandshakeError",this.code=e?.code,this.retryable=!0===e?.retryable||"UNAVAILABLE"===e?.code,this.retryAfterMs="number"==typeof e?.retryAfterMs?e.retryAfterMs:void 0}}export function decodeChatAbortResult(e){if("object"!=typeof e||null===e||Array.isArray(e))throw new Error("Invalid chat.abort response: expected object");const t=e;if("boolean"!=typeof t.aborted)throw new Error("Invalid chat.abort response: expected boolean aborted");if(!Array.isArray(t.runIds)||t.runIds.some(e=>"string"!=typeof e))throw new Error("Invalid chat.abort response: expected string[] runIds");return{aborted:t.aborted,runIds:[...t.runIds]}}function wsUrlToHttpBase(e){try{const t=new URL(e),n="wss:"===t.protocol?"https:":"ws:"===t.protocol?"http:":null;return n?`${n}//${t.host}`:null}catch{return null}}const MANUAL_RECONNECT_SIGNAL="SIGUSR1";function registerReconnectSignal(e,t,n){detachReconnectSignal(e),e.handler=t;try{process.on("SIGUSR1",t),n?.info?.("[gateway] manual reconnect enabled signal=SIGUSR1")}catch{n?.warn?.("[gateway] manual reconnect signal not supported")}}function detachReconnectSignal(e){if(e.handler){try{process.off("SIGUSR1",e.handler)}catch{}e.handler=null}}export class GatewayClient extends EventEmitter{ws=null;config;pending=new Map;connected=!1;stopped=!1;authFailed=!1;connectPromise=null;reconnectTimer=null;reconnectAttempt=0;nextReconnectDelayHintMs=null;signalState={handler:null};deviceIdentity;heartbeatTimer=null;lastPongTs=0;_connectionSeq=0;_currentConnectionId=null;_lastReadyAt=0;reconnectNotifyTimer=null;constructor(e){super(),this.config=e,this.deviceIdentity=loadOrCreateDeviceIdentity();try{ensureDevicePaired(this.deviceIdentity)}catch(e){}Date.now();warmupCryptoSign(this.deviceIdentity)}async start(){this.stopped=!1,this.reconnectAttempt=0,registerReconnectSignal(this.signalState,()=>{this.stopped||(this.emit("manual-reconnect"),this.reconnectNow())}),await this.connect()}stop(){this.stopped=!0,this.stopHeartbeat(),this.clearReconnectNotify(),detachReconnectSignal(this.signalState),this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close()}async connect(){if(this.connectPromise)return this.connectPromise;this.connectPromise=this._connect();try{await this.connectPromise}finally{this.connectPromise=null}}_connect(){return new Promise((e,t)=>{if(this.authFailed)return void t(new Error("Authentication failed — not retrying"));const n=Date.now();this.emit("transport",{event:"connect_start",url:this.config.url,attempt:this.reconnectAttempt});const s=new WebSocket(this.config.url);this.ws=s;let r,i=null,o=!1;const c=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),a=t=>{clearTimeout(c),o=!0,this.connected=!0,this.reconnectAttempt=0,this.authFailed=!1,this._connectionSeq++,this._currentConnectionId=randomUUID(),this._lastReadyAt=Date.now(),this.startHeartbeat(),this.emit("transport",{event:"connect_ok",connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId,durationMs:Date.now()-n}),this.emit("connected",{...t,connectionSeq:this._connectionSeq,connectionId:this._currentConnectionId}),e()},h=e=>{const r="string"==typeof e?e:e.message;clearTimeout(c),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:r,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:r}),s.close(),t("string"==typeof e?new Error(e):e)};s.on("open",()=>{this.emit("transport",{event:"ws_open",durationMs:Date.now()-n})}),s.on("message",e=>{const o="string"==typeof e?e:e.toString("utf-8");if("pong"===o)return void(this.lastPongTs=Date.now());let l;this.emitMessageHash(o);try{l=JSON.parse(o)}catch{return}const d=l.method;d&&!l.id&&(this.isReconnectNotification(d)?this.scheduleReconnectNotify():this.reconnectNotifyTimer&&this.clearReconnectNotify(),this.emit("notification",d,l.params));const u=l.type;if("event"===u&&this.reconnectNotifyTimer&&this.clearReconnectNotify(),"event"===u){const e=l.event,t=l.payload;if("connect.challenge"===e){const e=Date.now();this.emit("transport",{event:"handshake.challenge_recv",durationMs:e-n});const s=t?.nonce;i=this._sendConnect(s),this.emit("transport",{event:"handshake.connect_sent",durationMs:Date.now()-n}),r?.();let o=0;const c=setInterval(()=>{o++},200),a=Date.now(),h=setInterval(()=>{const e=Date.now()-a,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200);let l;const d=()=>{clearInterval(c),clearInterval(h),l&&clearTimeout(l),this.off("connected",d),r===d&&(r=void 0)};r=d,l=setTimeout(d,3e4),this.once("connected",d)}else"chat"===e?this.emit("chat",t):"agent"===e?this.emit("agent",t):"cron"===e?this.emit("cron",t):"canvas"===e?this.emit("canvas",t):"session.message"===e?this.emit("sessionMessage",t):this.emit("gateway-event",e,t)}else if("hello-ok"===u)this.emit("transport",{event:"handshake.hello_ok_recv",durationMs:Date.now()-n}),a(l);else if("hello-error"===u||"connect-error"===u)h("Authentication rejected by Gateway");else if("res"===u){const e=l.id;if(e&&e===i){if(this.emit("transport",{event:"handshake.connect_res_recv",ok:!!l.ok,durationMs:Date.now()-n}),l.ok){const e=l.payload;a(e??l)}else{const e=l.error,r=new GatewayHandshakeError(e);r.retryable?(clearTimeout(c),this.emit("transport",{event:"connect_fail",reason:r.message,retryable:!0,retryAfterMs:r.retryAfterMs,durationMs:Date.now()-n}),"number"==typeof r.retryAfterMs&&r.retryAfterMs>0&&(this.nextReconnectDelayHintMs=r.retryAfterMs),s.close(),t(r)):h(r)}return}const r=this.pending.get(e);if(r)if(this.pending.delete(e),clearTimeout(r.timer),l.ok)r.resolve(l.payload);else{const e=l.error;r.reject(new Error(e?.message||"Gateway request failed"))}else this.emit("response",l)}}),s.on("pong",()=>{this.lastPongTs=Date.now()}),s.on("close",e=>{const s=o;r?.(),this.stopHeartbeat(),this.clearReconnectNotify(),this.emit("transport",{event:"close",code:e,durationMs:Date.now()-n}),this.connected=!1,this.connectPromise=null;for(const[,e]of this.pending)clearTimeout(e.timer),e.reject(new Error("Connection closed"));this.pending.clear(),s||(clearTimeout(c),t(new Error(`Connection closed during handshake (code=${e})`))),4001===e&&(this.authFailed=!0,this.emit("auth-failed",{code:e})),s&&this.emit("disconnected"),this.stopped||this.authFailed||this.scheduleReconnect()}),s.on("error",e=>{this.emit("transport",{event:"error",message:e.message,durationMs:Date.now()-n}),this.stopped?clearTimeout(c):(this.connected||(clearTimeout(c),t(e)),this.emit("error",e))})})}emitMessageHash(e){if(e.length<32)return;const t=createHash("sha256").update(e).digest("hex").slice(0,16);this.emit("transport",{event:"message_recv",hash:t,size:e.length})}startHeartbeat(){this.stopHeartbeat(),this.lastPongTs=Date.now(),this.heartbeatTimer=setInterval(()=>{if(!this.ws||!this.connected)return;try{this.ws.ping()}catch{}const e=Date.now()-this.lastPongTs;e>6e4&&(this.emit("liveness-timeout",{elapsed:e}),this.ws.close())},15e3),"object"==typeof this.heartbeatTimer&&"unref"in this.heartbeatTimer&&this.heartbeatTimer.unref()}stopHeartbeat(){this.heartbeatTimer&&(clearInterval(this.heartbeatTimer),this.heartbeatTimer=null)}isReconnectNotification(e){return"server/reconnect"===e||e.endsWith("/reconnect")}scheduleReconnectNotify(){this.clearReconnectNotify();const e=6e4+12e4*Math.random();this.emit("reconnect-notify-scheduled",{delayMs:Math.round(e)}),this.reconnectNotifyTimer=setTimeout(()=>{this.reconnectNotifyTimer=null,!this.stopped&&this.connected&&(this.emit("reconnect-notify-fired"),this.reconnectNow())},e),"object"==typeof this.reconnectNotifyTimer&&"unref"in this.reconnectNotifyTimer&&this.reconnectNotifyTimer.unref()}clearReconnectNotify(){this.reconnectNotifyTimer&&(clearTimeout(this.reconnectNotifyTimer),this.reconnectNotifyTimer=null)}scheduleReconnect(){if(this.stopped)return;if(this.reconnectTimer)return;const e=this.config.retry??{baseMs:1e3,maxMs:6e5,maxAttempts:0};if(e.maxAttempts>0&&this.reconnectAttempt>=e.maxAttempts)return void this.emit("reconnect-exhausted",this.reconnectAttempt);let t;const n=this.nextReconnectDelayHintMs,s=null!==n&&n>0;s?(t=Math.min(n,e.maxMs),this.nextReconnectDelayHintMs=null):(t=Math.min(e.baseMs*Math.pow(2,this.reconnectAttempt)+500*Math.random(),e.maxMs),this.reconnectAttempt++),this.emit("reconnecting",{attempt:s?this.reconnectAttempt+1:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emitReconnectFailure(e)})},t),"object"==typeof this.reconnectTimer&&"unref"in this.reconnectTimer&&this.reconnectTimer.unref()}reconnectNow(){this.stopped||(this.reconnectTimer&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=null),this.close(),this.reconnectAttempt=0,this.connect().catch(e=>{this.emitReconnectFailure(e)}))}emitReconnectFailure(e){this.stopped||this.authFailed||this.emit("error",e instanceof Error?e:new Error(String(e)))}_sendConnect(e){const t=Date.now(),n=Date.now(),s="operator",r=["operator.admin","operator.approvals","operator.pairing"],i={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),o=buildDeviceAuthPayload({deviceId:this.deviceIdentity.deviceId,clientId:"cli",clientMode:"cli",role:s,scopes:r,signedAtMs:n,token:this.config.token,nonce:e}),c=Date.now()-t,a=Date.now();i.nonce=e,i.signature=signDeviceAuth(this.deviceIdentity,o);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const o=randomUUID(),c={type:"req",id:o,method:"connect",params:{minProtocol:3,maxProtocol:4,client:{id:"cli",displayName:"BeeOS Platform Bridge",version:"0.1.0",platform:"beeos",mode:"cli"},caps:this.config.openclawCanvasCompat?["tool-events","canvas"]:["tool-events"],role:s,scopes:r,auth:{token:this.config.token},device:i}},a=Date.now(),h=JSON.stringify(c),l=Date.now()-a,d=Date.now();this.ws?.send(h);const u=Date.now()-d;return this.emit("transport",{event:"handshake._sendConnect_done",totalMs:Date.now()-t,serializeMs:l,sendMs:u}),o}async request(e,t,n=6e4){if(!this.ws||!this.connected)throw new Error("Not connected to Gateway");const s=randomUUID(),r={type:"req",id:s,method:e,params:t};return new Promise((t,i)=>{const o=setTimeout(()=>{this.pending.delete(s),i(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:i,timer:o}),this.ws.send(JSON.stringify(r))})}async agentSend(e,t,n,s,r){const i={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(i.attachments=s),r&&(i.inputProvenance=r);return await this.request("agent",i,12e4)}async chatSend(e,t,n){const s={sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};n?.length&&(s.attachments=n);return await this.request("chat.send",s,12e4)}async chatAbort(e,t){const n={sessionKey:e};t&&(n.runId=t);return decodeChatAbortResult(await this.request("chat.abort",n))}sendFrame(e){if(!this.ws||!this.connected)return!1;try{return this.ws.send(JSON.stringify(e)),!0}catch{return!1}}agentCancel(e,t){const n={sessionKey:e};t?.runId&&(n.runId=t.runId);const s=`abort_${t?.runId??t?.requestId??e}`;return this.sendFrame({type:"req",id:s,method:"chat.abort",params:n})}async chatHistory(e,t){return this.request("chat.history",{sessionKey:e,limit:t})}async chatHistoryPage(e,t){const n=wsUrlToHttpBase(this.config.url);if(!n)return null;let s;try{s=new URL(`${n}/sessions/${encodeURIComponent(e)}/history`)}catch{return null}null!=t?.limit&&s.searchParams.set("limit",String(t.limit)),t?.cursor&&s.searchParams.set("cursor",t.cursor);try{const e=await fetch(s,{method:"GET",headers:{accept:"application/json",authorization:`Bearer ${this.config.token}`},signal:AbortSignal.timeout(1e4)});if(!e.ok)return null;const t=await e.json(),n=Array.isArray(t.messages)?t.messages:Array.isArray(t.items)?t.items:[];return{messages:n.filter(e=>"object"==typeof e&&null!==e),nextCursor:"string"==typeof t.nextCursor&&t.nextCursor.length>0?t.nextCursor:void 0,hasMore:!0===t.hasMore}}catch{return null}}async sessionsMessagesSubscribe(e){return this.request("sessions.messages.subscribe",{key:e})}async sessionsMessagesUnsubscribe(e){return this.request("sessions.messages.unsubscribe",{key:e})}async skillsStatus(e){return this.request("skills.status",e?{agentId:e}:{})}async skillsUpdate(e,t,n){return this.request("skills.update",{skillKey:e,enabled:t,...n?{agentId:n}:{}})}async sessionsList(){return this.request("sessions.list",{includeDerivedTitles:!0})}async sessionsResolve(e){return this.request("sessions.resolve",{sessionKey:e})}async sessionsPatch(e,t){return this.request("sessions.patch",{key:e,...t})}async sessionsDelete(e){return this.request("sessions.delete",{key:e})}async sessionsReset(e){return this.request("sessions.reset",{key:e,reason:"new"})}close(){if(this.ws)try{this.ws.close()}catch{}this.ws=null,this.connected=!1,this.connectPromise=null,this._currentConnectionId=null}get isConnected(){return this.connected}get connectionSeq(){return this._connectionSeq}get currentConnectionId(){return this._currentConnectionId}get lastReadyAt(){return this._lastReadyAt}}
@@ -1 +1 @@
1
- export class OpenClawRuntimeProcessOwner{client;projection;logger;registrations=new Set;startInFlight;stopInFlight;active=!1;constructor(t,i,s){this.client=t,this.projection=i,this.logger=s,this.client.on("connected",()=>this.projection?.onGatewayReconnect?.())}createRegistrationToken(){return Symbol("openclaw-runtime-registration")}async acquire(t){if(this.registrations.has(t))this.startInFlight&&await this.startInFlight;else{this.registrations.add(t);try{if(this.stopInFlight&&await this.stopInFlight,this.active)return void(this.startInFlight&&await this.startInFlight);this.active=!0,this.projection?.start(),this.startInFlight=this.client.start().catch(t=>{this.logger.error?.("[runtime-owner] Initial Gateway connect failed:",t)}).finally(()=>{this.startInFlight=void 0}),await this.startInFlight}catch(i){throw this.registrations.delete(t),this.active=!1,await(this.projection?.stop().catch(()=>{})),i}}}async release(t){this.registrations.delete(t)&&(this.startInFlight&&await this.startInFlight.catch(()=>{}),this.registrations.size>0||!this.active||(this.active=!1,this.stopInFlight=(async()=>{this.client.stop(),await(this.projection?.stop())})().finally(()=>{this.stopInFlight=void 0}),await this.stopInFlight))}get referenceCount(){return this.registrations.size}}
1
+ import{GatewayHandshakeError}from"../gateway-client.js";export class OpenClawRuntimeProcessOwner{client;projection;logger;registrations=new Set;startInFlight;stopInFlight;active=!1;startupConnectStartedAt;startupRetryCount=0;constructor(t,i,e){this.client=t,this.projection=i,this.logger=e,this.client.on("connected",()=>{this.projection?.onGatewayReconnect?.(),void 0!==this.startupConnectStartedAt&&this.startupRetryCount>0&&this.logger.info?.(`[runtime-owner] Gateway ready after ${Date.now()-this.startupConnectStartedAt}ms startupRetries=${this.startupRetryCount}`),this.resetStartupConnectTracking()}),this.client.on("reconnecting",()=>{void 0!==this.startupConnectStartedAt&&(this.startupRetryCount+=1)})}createRegistrationToken(){return Symbol("openclaw-runtime-registration")}async acquire(t){if(this.registrations.has(t))this.startInFlight&&await this.startInFlight;else{this.registrations.add(t);try{if(this.stopInFlight&&await this.stopInFlight,this.active)return void(this.startInFlight&&await this.startInFlight);this.active=!0,this.projection?.start(),this.startupConnectStartedAt=Date.now(),this.startupRetryCount=0,this.startInFlight=this.client.start().catch(t=>{if(t instanceof GatewayHandshakeError&&t.retryable){const i=void 0===t.retryAfterMs?"":` retryAfterMs=${t.retryAfterMs}`;return void this.logger.info?.(`[runtime-owner] Gateway is starting; reconnect scheduled${i}`)}this.resetStartupConnectTracking(),this.logger.error?.("[runtime-owner] Initial Gateway connect failed:",t)}).finally(()=>{this.startInFlight=void 0}),await this.startInFlight}catch(i){throw this.registrations.delete(t),this.active=!1,await(this.projection?.stop().catch(()=>{})),i}}}async release(t){this.registrations.delete(t)&&(this.startInFlight&&await this.startInFlight.catch(()=>{}),this.registrations.size>0||!this.active||(this.active=!1,this.resetStartupConnectTracking(),this.stopInFlight=(async()=>{this.client.stop(),await(this.projection?.stop())})().finally(()=>{this.stopInFlight=void 0}),await this.stopInFlight))}get referenceCount(){return this.registrations.size}resetStartupConnectTracking(){this.startupConnectStartedAt=void 0,this.startupRetryCount=0}}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{execFile}from"node:child_process";import{link,mkdir,open,readFile,readdir,rename,rm,stat,unlink}from"node:fs/promises";import{hostname}from"node:os";import path from"node:path";import{performance}from"node:perf_hooks";import{promisify}from"node:util";class CreatorFencedError extends Error{}const execFileAsync=promisify(execFile),delay=(t,e)=>new Promise((r,a)=>{if(e?.aborted)return void a(abortError());const o=setTimeout(function(){n(),r()},t),i=()=>{clearTimeout(o),n(),a(abortError())};function n(){e?.removeEventListener("abort",i)}e?.addEventListener("abort",i,{once:!0})});export async function withOwnedDirectoryLock(t,e,r={}){const a=await acquireOwnedDirectoryLock(t,r);try{const t=await e(a);return await a.assertOwned(),t}finally{await a.release()}}export async function acquireOwnedDirectoryLock(t,e={}){const r=positive(e.ttlMs??2e3,"lock ttl"),a=positive(e.heartbeatMs??Math.max(25,Math.floor(r/3)),"heartbeat"),o=positive(e.acquireTimeoutMs??3e4,"acquire timeout"),i=performance.now()+o,n=e.now??Date.now,c=e.random??Math.random,s=e.ownerIdentity??{pid:process.pid,hostname:hostname(),processStartIdentity:await readProcessStartIdentity(process.pid)??""};if(!s.processStartIdentity)throw new Error("cannot establish process-start identity for local lock owner");const w=e.observeProcess??observeLocalProcess;await mkdir(path.dirname(t),{recursive:!0,mode:448});let l=0;const d=async()=>{if(e.signal?.aborted)throw abortError();const r=i-performance.now();if(r<=0)throw new Error(`timed out acquiring lock ${t}`);const a=c();if(!Number.isFinite(a)||a<0||a>=1)throw new Error("lock retry random source must return a value in [0, 1)");const o=Math.min(100,5*2**Math.min(l,5));l+=1;const n=Math.max(1,Math.floor(a*o));await delay(Math.min(n,Math.max(1,Math.ceil(r))),e.signal)};for(;;){if(e.signal?.aborted)throw abortError();const o=randomUUID();try{await mkdir(t,{mode:448});const i=path.join(t,"owner.json"),c=await open(t,"r"),w=(await c.stat()).ino,l=path.join(t,`.owner.${o}.${randomUUID()}.initial`);let d,h,f,y;try{if((await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced before publication");if(d=await open(l,"wx",384),(await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced after binding its initial file")}catch(e){const r=await stat(t).then(t=>t.ino===w,()=>!1);if(await(d?.close().catch(()=>{})),await unlink(l).catch(()=>{}),await c.close().catch(()=>{}),!r)throw new CreatorFencedError("owned directory lock creator lost its directory while binding");throw e}try{h=await reserveEpoch(t,await(e.epochFloor?.())??0n)}catch(t){throw await(d?.close().catch(()=>{})),await unlink(l).catch(()=>{}),await c.close().catch(()=>{}),t}let p,u=n()+r,m=!1,k=Promise.resolve();const E=async()=>{const t=await open(i,"r");let e,r;try{e=parseOwner(await t.readFile("utf8")),r=(await t.stat()).ino}finally{await t.close()}if(e.token!==o||e.epoch!==h||r!==y)throw new Error("owned directory lock was fenced by a successor epoch");return e},b=async()=>{if(m)throw new Error("owned directory lock was released");if(p)throw p;const t=await E();if(n()>=t.expiresAtMs)throw new Error("owned directory lock lease expired")},v=async t=>{const e=k.catch(()=>{}).then(t);return k=e.catch(()=>{}),await e},I=async(r=!0)=>{const a=await E();if(r&&n()>=a.expiresAtMs)throw new Error("owned directory lock lease expired");const l=path.join(t,`.owner.${o}.${randomUUID()}.tmp`),d=await open(l,"wx",384);let p=!1;try{if((await stat(t)).ino!==w)throw new Error("owned directory lock was fenced before heartbeat publication");if(await d.writeFile(JSON.stringify({token:o,epoch:h,...s,expiresAtMs:u}),"utf8"),await d.sync(),await(e.beforeOwnerRename?.("heartbeat")),(await stat(t)).ino!==w)throw new Error("owned directory lock was fenced by a successor directory");const r=(await d.stat()).ino;await rename(l,i),p=!0;const a=f;f=d,y=r;try{await c.sync()}catch(t){throw await d.close().catch(()=>{}),f=void 0,await(a?.close().catch(()=>{})),t}await(a?.close())}finally{p||(await d.close().catch(()=>{}),await rm(l,{force:!0}).catch(()=>{}))}};try{if(await d.writeFile(JSON.stringify({token:o,epoch:h,...s,expiresAtMs:u}),"utf8"),await d.sync(),await(e.beforeOwnerRename?.("initial")),(await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced by a successor directory");await link(l,i),await unlink(l),await c.sync(),f=d,y=(await d.stat()).ino,d=void 0}catch(e){const r=await stat(t).then(t=>t.ino===w,()=>!1);if(await(d?.close().catch(()=>{})),await unlink(l).catch(()=>{}),await c.close().catch(()=>{}),!r)throw new CreatorFencedError("owned directory lock creator lost its directory before publication");throw e}const x=async()=>await v(async()=>{if(m||p)throw p??new Error("owned directory lock was released");await b(),u=n()+r,await I(!0)});let F=!1;const M=setInterval(()=>{m||p||F||(F=!0,x().catch(t=>{p=t instanceof Error?t:new Error("lock heartbeat failed")}).finally(()=>{F=!1}))},a);return M.unref?.(),{epoch:h,assertOwned:()=>v(b),heartbeat:x,release:async()=>{clearInterval(M),await v(async()=>{if(!m){try{await E(),u=n()-1,await I(!1)}catch{}m=!0,await(f?.close().catch(()=>{})),f=void 0,await c.close().catch(()=>{})}})}}}catch(e){if(e instanceof CreatorFencedError){await d();continue}const a=e.code;if("ENOENT"===a){await d();continue}if("EEXIST"!==a)throw e;const o=await reclaimable(t,s,w,n(),r);if(o){const e=`${t}.expired.${randomUUID()}`;try{if(await rename(t,e),await verifyReclaimProof(e,o,s,w,n(),r)){await rm(e,{recursive:!0,force:!0,maxRetries:8,retryDelay:1}),await syncDirectory(path.dirname(t));continue}await rename(e,t).catch(()=>{})}catch(t){if("ENOENT"!==t.code)throw t}}await d()}}}async function reclaimable(t,e,r,a,o){let i;try{const e=await open(path.join(t,"owner.json"),"r");let r,a;try{r=parseOwner(await e.readFile("utf8")),a=(await e.stat()).ino}finally{await e.close()}i={owner:r,inode:a}}catch{try{const e=await stat(t);return e.mtimeMs+o<=a?{kind:"directory",inode:e.ino,mtimeMs:e.mtimeMs}:null}catch{return null}}return i.owner.expiresAtMs<=a||i.owner.hostname===e.hostname&&await isProvenDead(i.owner,r)?{kind:"owner",...i}:null}async function verifyReclaimProof(t,e,r,a,o,i){try{const n=await reclaimable(t,r,a,o,i);return!(!n||n.kind!==e.kind)&&("directory"===e.kind?"directory"===n.kind&&n.inode===e.inode:"owner"===n.kind&&n.owner.token===e.owner.token&&n.owner.epoch===e.owner.epoch)}catch{return!1}}async function reserveEpoch(t,e){const r=`${t}.epochs`;await mkdir(r,{recursive:!0,mode:448});let a=0n;try{const e=(await readFile(`${t}.epoch`,"utf8")).trim();if(!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("lock epoch is corrupt");a=BigInt(e)}catch(t){if("ENOENT"!==t.code)throw t}for(;;){const o=await readdir(r),i=`${[e,a,o.reduce((t,e)=>/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)>t?BigInt(e):t,0n)].reduce((t,e)=>e>t?e:t,0n)+1n}`;try{const e=await open(path.join(r,i),"wx",384);try{await e.writeFile(`${i}\n`,"utf8"),await e.sync()}finally{await e.close()}return await Promise.all(o.filter(t=>/^(0|[1-9][0-9]*)$/.test(t)&&BigInt(t)<BigInt(i)).map(t=>unlink(path.join(r,t)).catch(t=>{if("ENOENT"!==t.code)throw t}))),await syncDirectory(r),await syncDirectory(path.dirname(t)),i}catch(t){if("EEXIST"!==t.code)throw t}}}function parseOwner(t){const e=JSON.parse(t);if(!e||"string"!=typeof e.token||!/^[1-9][0-9]*$/.test(e.epoch)||!Number.isSafeInteger(e.pid)||e.pid<=0||"string"!=typeof e.hostname||!e.hostname||"string"!=typeof e.processStartIdentity||!e.processStartIdentity||!Number.isFinite(e.expiresAtMs))throw new Error("lock owner is invalid");return e}async function isProvenDead(t,e){const r=await e(t.pid);return!r.alive||void 0!==r.processStartIdentity&&r.processStartIdentity!==t.processStartIdentity}async function observeLocalProcess(t){try{process.kill(t,0)}catch(t){return"ESRCH"===t.code?{alive:!1}:{alive:!0}}const e=await readProcessStartIdentity(t);return{alive:!0,...e?{processStartIdentity:e}:{}}}async function readProcessStartIdentity(t){if("linux"===process.platform)try{const e=await readFile(`/proc/${t}/stat`,"utf8"),r=e.slice(e.lastIndexOf(")")+2).trim().split(/\s+/)[19];return r?`linux:${r}`:null}catch{return null}if("darwin"===process.platform)try{const{stdout:e}=await execFileAsync("/bin/ps",["-o","lstart=","-p",String(t)]),r=e.trim();return r?`darwin:${r}`:null}catch{return null}return null}function positive(t,e){if(!Number.isSafeInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function abortError(){const t=new Error("lock acquisition aborted");return t.name="AbortError",t}async function syncDirectory(t){const e=await open(t,"r");try{await e.sync()}finally{await e.close()}}
1
+ import{randomUUID}from"node:crypto";import{execFile}from"node:child_process";import{link,mkdir,open,readFile,readdir,rename,rm,stat,unlink}from"node:fs/promises";import{hostname}from"node:os";import path from"node:path";import{performance}from"node:perf_hooks";import{promisify}from"node:util";class CreatorFencedError extends Error{}export class OwnedDirectoryLockContendedError extends Error{lockPath;constructor(t){super(`owned directory lock is busy: ${t}`),this.lockPath=t,this.name="OwnedDirectoryLockContendedError"}}const execFileAsync=promisify(execFile),delay=(t,e)=>new Promise((r,a)=>{if(e?.aborted)return void a(abortError());const o=setTimeout(function(){n(),r()},t),i=()=>{clearTimeout(o),n(),a(abortError())};function n(){e?.removeEventListener("abort",i)}e?.addEventListener("abort",i,{once:!0})});export async function withOwnedDirectoryLock(t,e,r={}){const a=await acquireOwnedDirectoryLock(t,r);try{const t=await e(a);return await a.assertOwned(),t}finally{await a.release()}}export async function acquireOwnedDirectoryLock(t,e={}){const r=positive(e.ttlMs??2e3,"lock ttl"),a=positive(e.heartbeatMs??Math.max(25,Math.floor(r/3)),"heartbeat"),o=positive(e.acquireTimeoutMs??3e4,"acquire timeout"),i=performance.now()+o,n=e.now??Date.now,c=e.random??Math.random,s=e.ownerIdentity??{pid:process.pid,hostname:hostname(),processStartIdentity:await readProcessStartIdentity(process.pid)??""};if(!s.processStartIdentity)throw new Error("cannot establish process-start identity for local lock owner");const w=e.observeProcess??observeLocalProcess;await mkdir(path.dirname(t),{recursive:!0,mode:448});let d=0;const l=async()=>{if(e.signal?.aborted)throw abortError();const r=i-performance.now();if(r<=0)throw new Error(`timed out acquiring lock ${t}`);const a=c();if(!Number.isFinite(a)||a<0||a>=1)throw new Error("lock retry random source must return a value in [0, 1)");const o=Math.min(100,5*2**Math.min(d,5));d+=1;const n=Math.max(1,Math.floor(a*o));await delay(Math.min(n,Math.max(1,Math.ceil(r))),e.signal)};for(;;){if(e.signal?.aborted)throw abortError();const o=randomUUID();try{await mkdir(t,{mode:448});const i=path.join(t,"owner.json"),c=await open(t,"r"),w=(await c.stat()).ino,d=path.join(t,`.owner.${o}.${randomUUID()}.initial`);let l,h,y,f;try{if((await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced before publication");if(l=await open(d,"wx",384),(await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced after binding its initial file")}catch(e){const r=await stat(t).then(t=>t.ino===w,()=>!1);if(await(l?.close().catch(()=>{})),await unlink(d).catch(()=>{}),await c.close().catch(()=>{}),!r)throw new CreatorFencedError("owned directory lock creator lost its directory while binding");throw e}try{h=await reserveEpoch(t,await(e.epochFloor?.())??0n)}catch(t){throw await(l?.close().catch(()=>{})),await unlink(d).catch(()=>{}),await c.close().catch(()=>{}),t}let p,u=n()+r,m=!1,k=Promise.resolve();const E=async()=>{const t=await open(i,"r");let e,r;try{e=parseOwner(await t.readFile("utf8")),r=(await t.stat()).ino}finally{await t.close()}if(e.token!==o||e.epoch!==h||r!==f)throw new Error("owned directory lock was fenced by a successor epoch");return e},b=async()=>{if(m)throw new Error("owned directory lock was released");if(p)throw p;const t=await E();if(n()>=t.expiresAtMs)throw new Error("owned directory lock lease expired")},v=async t=>{const e=k.catch(()=>{}).then(t);return k=e.catch(()=>{}),await e},I=async(r=!0)=>{const a=await E();if(r&&n()>=a.expiresAtMs)throw new Error("owned directory lock lease expired");const d=path.join(t,`.owner.${o}.${randomUUID()}.tmp`),l=await open(d,"wx",384);let p=!1;try{if((await stat(t)).ino!==w)throw new Error("owned directory lock was fenced before heartbeat publication");if(await l.writeFile(JSON.stringify({token:o,epoch:h,...s,expiresAtMs:u}),"utf8"),await l.sync(),await(e.beforeOwnerRename?.("heartbeat")),(await stat(t)).ino!==w)throw new Error("owned directory lock was fenced by a successor directory");const r=(await l.stat()).ino;await rename(d,i),p=!0;const a=y;y=l,f=r;try{await c.sync()}catch(t){throw await l.close().catch(()=>{}),y=void 0,await(a?.close().catch(()=>{})),t}await(a?.close())}finally{p||(await l.close().catch(()=>{}),await rm(d,{force:!0}).catch(()=>{}))}};try{if(await l.writeFile(JSON.stringify({token:o,epoch:h,...s,expiresAtMs:u}),"utf8"),await l.sync(),await(e.beforeOwnerRename?.("initial")),(await stat(t)).ino!==w)throw new CreatorFencedError("owned directory lock creator was fenced by a successor directory");await link(d,i),await unlink(d),await c.sync(),y=l,f=(await l.stat()).ino,l=void 0}catch(e){const r=await stat(t).then(t=>t.ino===w,()=>!1);if(await(l?.close().catch(()=>{})),await unlink(d).catch(()=>{}),await c.close().catch(()=>{}),!r)throw new CreatorFencedError("owned directory lock creator lost its directory before publication");throw e}const x=async()=>await v(async()=>{if(m||p)throw p??new Error("owned directory lock was released");await b(),u=n()+r,await I(!0)});let F=!1;const M=setInterval(()=>{m||p||F||(F=!0,x().catch(t=>{p=t instanceof Error?t:new Error("lock heartbeat failed")}).finally(()=>{F=!1}))},a);return M.unref?.(),{epoch:h,assertOwned:()=>v(b),heartbeat:x,release:async()=>{clearInterval(M),await v(async()=>{if(!m){try{await E(),u=n()-1,await I(!1)}catch{}m=!0,await(y?.close().catch(()=>{})),y=void 0,await c.close().catch(()=>{})}})}}}catch(a){if(a instanceof CreatorFencedError){await l();continue}const o=a.code;if("ENOENT"===o){await l();continue}if("EEXIST"!==o)throw a;const i=await reclaimable(t,s,w,n(),r);if(i){const e=`${t}.expired.${randomUUID()}`;try{if(await rename(t,e),await verifyReclaimProof(e,i,s,w,n(),r)){await rm(e,{recursive:!0,force:!0,maxRetries:8,retryDelay:1}),await syncDirectory(path.dirname(t));continue}await rename(e,t).catch(()=>{})}catch(t){if("ENOENT"!==t.code)throw t}}if(e.failIfContended)throw new OwnedDirectoryLockContendedError(t);await l()}}}async function reclaimable(t,e,r,a,o){let i;try{const e=await open(path.join(t,"owner.json"),"r");let r,a;try{r=parseOwner(await e.readFile("utf8")),a=(await e.stat()).ino}finally{await e.close()}i={owner:r,inode:a}}catch{try{const e=await stat(t);return e.mtimeMs+o<=a?{kind:"directory",inode:e.ino,mtimeMs:e.mtimeMs}:null}catch{return null}}return i.owner.expiresAtMs<=a||i.owner.hostname===e.hostname&&await isProvenDead(i.owner,r)?{kind:"owner",...i}:null}async function verifyReclaimProof(t,e,r,a,o,i){try{const n=await reclaimable(t,r,a,o,i);return!(!n||n.kind!==e.kind)&&("directory"===e.kind?"directory"===n.kind&&n.inode===e.inode:"owner"===n.kind&&n.owner.token===e.owner.token&&n.owner.epoch===e.owner.epoch)}catch{return!1}}async function reserveEpoch(t,e){const r=`${t}.epochs`;await mkdir(r,{recursive:!0,mode:448});let a=0n;try{const e=(await readFile(`${t}.epoch`,"utf8")).trim();if(!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("lock epoch is corrupt");a=BigInt(e)}catch(t){if("ENOENT"!==t.code)throw t}for(;;){const o=await readdir(r),i=`${[e,a,o.reduce((t,e)=>/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)>t?BigInt(e):t,0n)].reduce((t,e)=>e>t?e:t,0n)+1n}`;try{const e=await open(path.join(r,i),"wx",384);try{await e.writeFile(`${i}\n`,"utf8"),await e.sync()}finally{await e.close()}return await Promise.all(o.filter(t=>/^(0|[1-9][0-9]*)$/.test(t)&&BigInt(t)<BigInt(i)).map(t=>unlink(path.join(r,t)).catch(t=>{if("ENOENT"!==t.code)throw t}))),await syncDirectory(r),await syncDirectory(path.dirname(t)),i}catch(t){if("EEXIST"!==t.code)throw t}}}function parseOwner(t){const e=JSON.parse(t);if(!e||"string"!=typeof e.token||!/^[1-9][0-9]*$/.test(e.epoch)||!Number.isSafeInteger(e.pid)||e.pid<=0||"string"!=typeof e.hostname||!e.hostname||"string"!=typeof e.processStartIdentity||!e.processStartIdentity||!Number.isFinite(e.expiresAtMs))throw new Error("lock owner is invalid");return e}async function isProvenDead(t,e){const r=await e(t.pid);return!r.alive||void 0!==r.processStartIdentity&&r.processStartIdentity!==t.processStartIdentity}async function observeLocalProcess(t){try{process.kill(t,0)}catch(t){return"ESRCH"===t.code?{alive:!1}:{alive:!0}}const e=await readProcessStartIdentity(t);return{alive:!0,...e?{processStartIdentity:e}:{}}}async function readProcessStartIdentity(t){if("linux"===process.platform)try{const e=await readFile(`/proc/${t}/stat`,"utf8"),r=e.slice(e.lastIndexOf(")")+2).trim().split(/\s+/)[19];return r?`linux:${r}`:null}catch{return null}if("darwin"===process.platform)try{const{stdout:e}=await execFileAsync("/bin/ps",["-o","lstart=","-p",String(t)]),r=e.trim();return r?`darwin:${r}`:null}catch{return null}return null}function positive(t,e){if(!Number.isSafeInteger(t)||t<=0)throw new Error(`${e} must be a positive integer`);return t}function abortError(){const t=new Error("lock acquisition aborted");return t.name="AbortError",t}async function syncDirectory(t){const e=await open(t,"r");try{await e.sync()}finally{await e.close()}}
@@ -1 +1 @@
1
- import{createHash,randomUUID}from"node:crypto";import{readFile}from"node:fs/promises";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";import{createRuntimeAppendTimingRecorder}from"./runtime-append-timing.js";import{withOwnedDirectoryLock}from"./owned-directory-lock.js";import{MonotonicJournalSegments}from"./monotonic-journal-segments.js";const HASH_ONLY_RESULT_COMMANDS=new Set(["session.load","session.history","session.list","agent.listProjection","skills.list","models.list","cron.list","cron.status","cron.runs","mcp.list","terminal.ping","terminal.wait"]),SAFE_CANONICAL_RESULT_COMMANDS=new Set(["session.clear","session.setMode","session.setModel","session.cancel","skills.install","skills.uninstall","skills.setEnabled","cron.remove","mcp.prepare","mcp.set","mcp.unset","terminal.input","terminal.resize","terminal.close","terminal.release","terminal.kill","canvas.toggle","canvas.clear","canvas.reference","canvas.dimensions"]);export const runtimeCommandResultPersistencePolicy=e=>SAFE_CANONICAL_RESULT_COMMANDS.has(e)?"canonical_result":"hash_only";export const runtimeCommandIsReplayableQuery=e=>HASH_ONLY_RESULT_COMMANDS.has(e);export const runtimeCommandResultHash=e=>createHash("sha256").update(canonicalizeJcs(e)).digest("hex");const emptyDocument=()=>({version:2,lineage:{journalStoreId:randomUUID(),journalGeneration:"1"},records:[],cancellations:[]}),CHECKPOINT_INTERVAL_MS=6e5,CHECKPOINT_DELTA_COUNT=512,CHECKPOINT_DELTA_BYTES=8388608;export class RuntimeCommandJournalConflictError extends Error{constructor(e){super(e),this.name="RuntimeCommandJournalConflictError"}}export class FileRuntimeCommandJournal{file;retentionMs;now;publication;segments;maintenance;forceCheckpointRequested=!1;constructor(e,t=6048e5,n=Date.now,o={}){this.file=e,this.retentionMs=t,this.now=n,this.publication=o,this.segments=new MonotonicJournalSegments(e)}async lineage(){return await this.update(e=>({...e.lineage}))}async advanceLineageGeneration(e){return await this.update(t=>{if(t.lineage.journalStoreId!==e.journalStoreId)throw new RuntimeCommandJournalConflictError("journal store changed during incarnation advance");const n=BigInt(t.lineage.journalGeneration),o=BigInt(e.floor),r=(n>o?n:o)+1n;if(r>9223372036854775807n)throw new RuntimeCommandJournalConflictError("journal generation overflow");return t.lineage={journalStoreId:t.lineage.journalStoreId,journalGeneration:String(r)},{...t.lineage}})}async get(e){return(await this.read()).records.find(t=>t.operationId===e)??null}async getCancellation(e){return(await this.read()).cancellations.find(t=>t.operationId===e)??null}async listMcpLiveReleases(){const e=await this.read();return Object.freeze(e.records.flatMap(e=>e.mcpLiveRelease?[e.mcpLiveRelease]:[]))}async listMcpActiveDesiredStates(){const e=await this.read();return Object.freeze(e.records.flatMap(e=>e.mcpActiveDesiredState?[e.mcpActiveDesiredState]:[]))}async recordMcpDesiredStateCommit(e){const t=normalizeMcpDesiredStateCommit(e);return await this.update(e=>{const n=e.records.findIndex(e=>e.operationId===t.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP desired state has no command record");const o=e.records[n];if(assertMcpCommitBindsRecord(t,o),"started"!==o.phase){if(sameMcpDesiredStateCommit(o.mcpDesiredStateCommit,t))return o;throw new RuntimeCommandJournalConflictError("MCP desired state commit crossed its journal boundary")}if(o.mcpDesiredStateCommit){if(sameMcpDesiredStateCommit(o.mcpDesiredStateCommit,t))return o;throw new RuntimeCommandJournalConflictError("MCP desired state commit conflicts with journal")}const r={...o,mcpDesiredStateCommit:t,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return e.records[n]=r,r})}async releaseMcpLiveEffect(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP release has no command record");const o=t.records[n];if("effect_committed"!==o.phase&&"projection_committed"!==o.phase)throw new RuntimeCommandJournalConflictError("MCP release requires a committed effect");if(!o.mcpDesiredStateCommit)throw new RuntimeCommandJournalConflictError("MCP release lacks a durable desired-state commit");if("service"===e.source&&!o.messageBinding)throw new RuntimeCommandJournalConflictError("Service MCP release lacks a Message Service request binding");const r=releaseFromCommit(o.mcpDesiredStateCommit,e,new Date(this.now()).toISOString());if(o.mcpLiveRelease){if(sameMcpLiveRelease(o.mcpLiveRelease,r))return o;throw new RuntimeCommandJournalConflictError("MCP release conflicts with journal")}const i={...o,mcpLiveRelease:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async promoteMcpDesiredStateActive(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP active promotion has no command record");const o=t.records[n],r=activeFromRelease(o.mcpLiveRelease,e,new Date(this.now()).toISOString());if(o.mcpActiveDesiredState){if(sameMcpActiveDesiredState(o.mcpActiveDesiredState,r))return o;throw new RuntimeCommandJournalConflictError("MCP active desired state conflicts with journal")}const i={...o,mcpActiveDesiredState:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async acceptMessageRequest(e){return await this.update(t=>{let n=t.records.find(t=>t.operationId===e.operationId);if(n||(n={operationId:e.operationId,command:e.command,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,phase:"prepared",revision:1,updatedAt:new Date(this.now()).toISOString()},t.records.push(n)),n.command!==e.command||n.paramsHash!==e.paramsHash||n.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("message request conflicts with command journal");const o={requestMessageId:e.requestMessageId,requestHash:e.requestHash,acceptedAt:new Date(this.now()).toISOString()};if(n.messageBinding&&(n.messageBinding.requestMessageId!==o.requestMessageId||n.messageBinding.requestHash!==o.requestHash))throw new RuntimeCommandJournalConflictError("operation reused by a different message request");return n.messageBinding??=o,n})}async observeCancellation(e){return await this.update(t=>{const n=t.cancellations.find(t=>t.eventId===e.eventId);if(n){if(n.operationId!==e.operationId||n.requestMessageId!==e.requestMessageId||n.requestHash!==e.requestHash)throw new RuntimeCommandJournalConflictError("operation cancellation observation conflict");return n}const o=t.cancellations.find(t=>t.operationId===e.operationId);if(o&&(o.requestMessageId!==e.requestMessageId||o.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("operation cancellation request correlation conflict");const r={...e,observedAt:new Date(this.now()).toISOString()};return t.cancellations.push(r),r})}async prepare(e){return await this.update(t=>{const n=t.records.find(t=>t.operationId===e.operationId);if(n){if(n.command!==e.command||n.paramsHash!==e.paramsHash)throw new RuntimeCommandJournalConflictError("operation identity reused with different command or params hash");if(n.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("operation belongs to a different runtime epoch");return n}const o={...e,phase:"prepared",revision:1,updatedAt:new Date(this.now()).toISOString()};return t.records.push(o),o})}async transition(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision)throw new RuntimeCommandJournalConflictError("journal revision conflict");const r="effect_committed"===o.phase&&"effect_committed"===e.phase&&void 0!==e.agentProjection&&void 0===o.agentProjectionIntent&&"pending"===o.agentProjectionPreparationFailure?.state;if("projection_committed"===o.phase||"failed"===o.phase||"outcome_unknown"===o.phase||"effect_committed"===o.phase&&"projection_committed"!==e.phase&&!r)throw new RuntimeCommandJournalConflictError("terminal journal record is immutable");if(void 0!==e.projectionSnapshot&&"projection_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("projection receipt requires projection_committed phase");if(void 0!==e.agentProjection&&"effect_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("agent projection intent requires effect_committed phase");const i=void 0===e.agentProjection?void 0:normalizeAgentProjectionPreparation(e.agentProjection,o);if(r&&i&&!sameProjectionPreparationBase(o.agentProjectionPreparationFailure,i))throw new RuntimeCommandJournalConflictError("agent projection preparation conflicts with committed operation");const{agentProjectionIntent:a,agentProjectionPreparationFailure:s,...c}=o,d={...void 0===i?o:c,phase:e.phase,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString(),...void 0===e.result?{}:persistedResult(o.command,e.result),...void 0===e.projectionSnapshot?{}:{projectionReceipt:projectionReceipt(e.projectionSnapshot)},...void 0===i?{}:isAgentProjectionIntent(i)?{agentProjectionIntent:i}:{agentProjectionPreparationFailure:i},...void 0===e.errorCode?{}:{errorCode:e.errorCode}};return t.records[n]=d,d})}async recordAgentProjectionIntent(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision||"effect_committed"!==o.phase)throw new RuntimeCommandJournalConflictError("projection intent cannot mutate this command journal record");const r=normalizeAgentProjectionIntent(e.intent,o);if(o.agentProjectionIntent){if(canonicalizeJcs(o.agentProjectionIntent)!==canonicalizeJcs(r))throw new RuntimeCommandJournalConflictError("agent projection intent conflicts with committed operation");return o}if("pending"!==o.agentProjectionPreparationFailure?.state||!sameProjectionPreparationBase(o.agentProjectionPreparationFailure,r))throw new RuntimeCommandJournalConflictError("projection intent requires matching retryable preparation failure");const{agentProjectionPreparationFailure:i,...a}=o,s={...a,agentProjectionIntent:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=s,s})}async recordAgentProjectionBlocked(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision||"effect_committed"!==o.phase||!o.agentProjectionIntent||o.agentProjectionIntent.projectionRevision!==e.revision)throw new RuntimeCommandJournalConflictError("blocked projection cannot mutate this command journal record");if(o.agentProjectionTerminal){if("blocked"===o.agentProjectionTerminal.state&&o.agentProjectionTerminal.revision===e.revision)return o;throw new RuntimeCommandJournalConflictError("agent projection terminal conflicts with committed operation")}const r=agentProjectionTerminalReceipt(e.revision),i={...o,agentProjectionTerminal:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async compact(){return await this.startMaintenance()}async checkpoint(){return this.forceCheckpointRequested=!0,await this.startMaintenance()}async read(){return(await this.readState()).document}async readState(){const e=await this.readLegacy(),t=new Map;if(e){t.set("meta:lineage",{value:e.lineage,logicalRevision:e.lineage.journalGeneration,publisherEpoch:"0"});for(const n of e.records)t.set(`record:${n.operationId}`,{value:n,logicalRevision:String(n.revision),publisherEpoch:"0"});for(const n of e.cancellations)t.set(`cancellation:${n.eventId}`,{value:n,logicalRevision:"1",publisherEpoch:"0"})}const n=await this.segments.readState(t),o=n.values,r=o.get("meta:lineage")?.value;return{document:{version:2,lineage:r??emptyDocument().lineage,records:[...o.entries()].filter(([e])=>e.startsWith("record:")).map(([,e])=>validateRecordReceipts(migrateLegacyCommittedPhase(e.value))),cancellations:[...o.entries()].filter(([e])=>e.startsWith("cancellation:")).map(([,e])=>e.value)},authorityFloor:n.authorityFloor,checkpointAtMs:n.checkpointAtMs,deltaCount:n.deltaCount,deltaBytes:n.deltaBytes,oldestDeltaAtMs:n.oldestDeltaAtMs,hasDurableLineage:o.has("meta:lineage")&&n.hasDurableState}}async readLegacy(){try{const e=JSON.parse(await readFile(this.file,"utf8"));if(!Array.isArray(e.records))throw new Error("unsupported command journal");const t=e.records.map(e=>validateRecordReceipts(migrateLegacyCommittedPhase(e)));if(1===e.version)return{...emptyDocument(),records:t};if(2!==e.version||!e.lineage?.journalStoreId||!/^[1-9][0-9]*$/.test(e.lineage.journalGeneration))throw new Error("unsupported command journal");return{...e,records:t,cancellations:Array.isArray(e.cancellations)?e.cancellations:[]}}catch(e){if("ENOENT"===e.code)return null;throw e}}async update(e){const t=createRuntimeAppendTimingRecorder("journal.update",{file:this.file});let n=!1;try{return await withOwnedDirectoryLock(`${this.file}.lock`,async o=>{t.mark("lock");const r=await this.readState();t.mark("read");const i=r.document,a=structuredClone(i),s=e(i),c=[];if(r.hasDurableLineage&&sameValue(a.lineage,i.lineage)||c.push({key:"meta:lineage",logicalRevision:i.lineage.journalGeneration,value:i.lineage}),diffValues(a.records,i.records,e=>e.operationId,e=>String(e.revision),"record:",c),diffValues(a.cancellations,i.cancellations,e=>e.eventId,()=>"1","cancellation:",c),0===c.length)return t.mark("noop"),s;await(this.publication.beforePublish?.(o.epoch)),t.mark("before-publish"),await o.assertOwned(),t.mark("assert-owned");const d=await this.segments.publishDelta(o.epoch,c,r.authorityFloor);return t.mark("publish-delta"),n=checkpointDue(r,1,d),s},{...this.publication.lockOptions,epochFloor:()=>this.segments.authorityFloor()})}finally{t.finish(),n&&this.scheduleMaintenance()}}scheduleMaintenance(){this.startMaintenance().catch(e=>{try{this.publication.onMaintenanceError?this.publication.onMaintenanceError(e):emitMaintenanceWarning(e)}catch(e){emitMaintenanceWarning(e)}})}startMaintenance(){if(this.maintenance)return this.maintenance;const e=(async()=>{let e=0;do{const t=this.forceCheckpointRequested;this.forceCheckpointRequested=!1,e+=await this.runMaintenance(t)}while(this.forceCheckpointRequested);return e})().finally(()=>{this.maintenance===e&&(this.maintenance=void 0)});return this.maintenance=e,e}async runMaintenance(e){const t=createRuntimeAppendTimingRecorder("journal.checkpoint",{file:this.file});try{return await withOwnedDirectoryLock(`${this.file}.lock`,async n=>{t.mark("lock");const o=await this.readState();if(t.mark("read"),!e&&!checkpointDue(o))return t.mark("not-due"),0;const r=o.document,i=this.now()-this.retentionMs,a=r.records.length;r.records=r.records.filter(e=>"prepared"===e.phase||"started"===e.phase||Date.parse(e.updatedAt)>=i);const s=r.cancellations.length;r.cancellations=r.cancellations.filter(e=>Date.parse(e.observedAt)>=i);const c=a-r.records.length+s-r.cancellations.length,d=documentValues(r);return await(this.publication.beforePublish?.(n.epoch)),t.mark("before-publish"),await n.assertOwned(),t.mark("assert-owned"),await this.segments.checkpointSnapshot(n.epoch,d,o.authorityFloor),t.mark("publish-snapshot"),await n.assertOwned(),await this.segments.prune(),t.mark("prune"),c},{...this.publication.lockOptions,epochFloor:()=>this.segments.authorityFloor()})}finally{t.finish(e?"forced":"scheduled")}}}function checkpointDue(e,t=0,n=0,o=Date.now()){const r=e.deltaCount+t,i=e.deltaBytes+n,a=e.oldestDeltaAtMs||(t>0?o:0),s=e.checkpointAtMs||a;return r>=512||i>=8388608||r>0&&s>0&&o-s>=6e5}function emitMaintenanceWarning(e){process.emitWarning(e instanceof Error?e.message:String(e),{code:"BEEOS_RUNTIME_JOURNAL_CHECKPOINT_FAILED"})}function diffValues(e,t,n,o,r,i){const a=new Map(e.map(e=>[n(e),e])),s=new Map(t.map(e=>[n(e),e]));for(const[e,t]of s){const n=a.get(e);void 0!==n&&sameValue(n,t)||i.push({key:`${r}${e}`,logicalRevision:o(t),value:t})}for(const[e,t]of a)s.has(e)||i.push({key:`${r}${e}`,logicalRevision:o(t),tombstone:!0})}function documentValues(e){return[{key:"meta:lineage",logicalRevision:e.lineage.journalGeneration,value:e.lineage},...e.records.map(e=>({key:`record:${e.operationId}`,logicalRevision:String(e.revision),value:e})),...e.cancellations.map(e=>({key:`cancellation:${e.eventId}`,logicalRevision:"1",value:e}))]}function sameValue(e,t){return canonicalizeJcs(e)===canonicalizeJcs(t)}function migrateLegacyCommittedPhase(e){return{...e,phase:"committed"===e.phase?"effect_committed":e.phase}}export class InMemoryRuntimeCommandJournal{records=new Map;cancellations=new Map;async get(e){return this.records.get(e)??null}async getCancellation(e){return[...this.cancellations.values()].find(t=>t.operationId===e)??null}async listMcpLiveReleases(){return Object.freeze([...this.records.values()].flatMap(e=>e.mcpLiveRelease?[e.mcpLiveRelease]:[]))}async listMcpActiveDesiredStates(){return Object.freeze([...this.records.values()].flatMap(e=>e.mcpActiveDesiredState?[e.mcpActiveDesiredState]:[]))}async recordMcpDesiredStateCommit(e){const t=normalizeMcpDesiredStateCommit(e),n=this.records.get(t.operationId);if(!n)throw new RuntimeCommandJournalConflictError("MCP desired state has no command record");if(assertMcpCommitBindsRecord(t,n),"started"!==n.phase){if(sameMcpDesiredStateCommit(n.mcpDesiredStateCommit,t))return n;throw new RuntimeCommandJournalConflictError("MCP desired state commit crossed its journal boundary")}if(n.mcpDesiredStateCommit){if(sameMcpDesiredStateCommit(n.mcpDesiredStateCommit,t))return n;throw new RuntimeCommandJournalConflictError("MCP desired state commit conflicts with journal")}const o={...n,mcpDesiredStateCommit:t,revision:n.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(t.operationId,o),o}async releaseMcpLiveEffect(e){const t=this.records.get(e.operationId);if(!t)throw new RuntimeCommandJournalConflictError("MCP release has no command record");if("effect_committed"!==t.phase&&"projection_committed"!==t.phase)throw new RuntimeCommandJournalConflictError("MCP release requires a committed effect");if(!t.mcpDesiredStateCommit)throw new RuntimeCommandJournalConflictError("MCP release lacks a durable desired-state commit");if("service"===e.source&&!t.messageBinding)throw new RuntimeCommandJournalConflictError("Service MCP release lacks a Message Service request binding");const n=releaseFromCommit(t.mcpDesiredStateCommit,e,(new Date).toISOString());if(t.mcpLiveRelease){if(sameMcpLiveRelease(t.mcpLiveRelease,n))return t;throw new RuntimeCommandJournalConflictError("MCP release conflicts with journal")}const o={...t,mcpLiveRelease:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,o),o}async promoteMcpDesiredStateActive(e){const t=this.records.get(e.operationId);if(!t)throw new RuntimeCommandJournalConflictError("MCP active promotion has no command record");const n=activeFromRelease(t.mcpLiveRelease,e,(new Date).toISOString());if(t.mcpActiveDesiredState){if(sameMcpActiveDesiredState(t.mcpActiveDesiredState,n))return t;throw new RuntimeCommandJournalConflictError("MCP active desired state conflicts with journal")}const o={...t,mcpActiveDesiredState:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,o),o}async acceptMessageRequest(e){let t=this.records.get(e.operationId);if(t||(t={operationId:e.operationId,command:e.command,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,phase:"prepared",revision:1,updatedAt:(new Date).toISOString()},this.records.set(e.operationId,t)),t.command!==e.command||t.paramsHash!==e.paramsHash||t.runtimeEpoch!==e.runtimeEpoch||t.messageBinding&&(t.messageBinding.requestMessageId!==e.requestMessageId||t.messageBinding.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("message request conflicts with command journal");return t.messageBinding??={requestMessageId:e.requestMessageId,requestHash:e.requestHash,acceptedAt:(new Date).toISOString()},t}async observeCancellation(e){const t=this.cancellations.get(e.eventId);if(t){if(t.operationId!==e.operationId||t.requestMessageId!==e.requestMessageId||t.requestHash!==e.requestHash)throw new RuntimeCommandJournalConflictError("operation cancellation observation conflict");return t}const n=[...this.cancellations.values()].find(t=>t.operationId===e.operationId);if(n&&(n.requestMessageId!==e.requestMessageId||n.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("operation cancellation request correlation conflict");const o={...e,observedAt:(new Date).toISOString()};return this.cancellations.set(e.eventId,o),o}async prepare(e){const t=this.records.get(e.operationId);if(t){if(t.command!==e.command||t.paramsHash!==e.paramsHash||t.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("operation identity conflict");return t}const n={...e,phase:"prepared",revision:1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,n),n}async transition(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision)throw new RuntimeCommandJournalConflictError("revision conflict");const n="effect_committed"===t.phase&&"effect_committed"===e.phase&&void 0!==e.agentProjection&&void 0===t.agentProjectionIntent&&"pending"===t.agentProjectionPreparationFailure?.state;if("projection_committed"===t.phase||"failed"===t.phase||"outcome_unknown"===t.phase||"effect_committed"===t.phase&&"projection_committed"!==e.phase&&!n)throw new RuntimeCommandJournalConflictError("terminal journal record is immutable");if(void 0!==e.projectionSnapshot&&"projection_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("projection receipt requires projection_committed phase");if(void 0!==e.agentProjection&&"effect_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("agent projection intent requires effect_committed phase");const o=void 0===e.agentProjection?void 0:normalizeAgentProjectionPreparation(e.agentProjection,t);if(n&&o&&!sameProjectionPreparationBase(t.agentProjectionPreparationFailure,o))throw new RuntimeCommandJournalConflictError("agent projection preparation conflicts with committed operation");const{agentProjectionIntent:r,agentProjectionPreparationFailure:i,...a}=t,s={...void 0===o?t:a,phase:e.phase,revision:t.revision+1,updatedAt:(new Date).toISOString(),...void 0===e.result?{}:persistedResult(t.command,e.result),...void 0===e.projectionSnapshot?{}:{projectionReceipt:projectionReceipt(e.projectionSnapshot)},...void 0===o?{}:isAgentProjectionIntent(o)?{agentProjectionIntent:o}:{agentProjectionPreparationFailure:o},...void 0===e.errorCode?{}:{errorCode:e.errorCode}};return this.records.set(e.operationId,s),s}async recordAgentProjectionIntent(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision||"effect_committed"!==t.phase)throw new RuntimeCommandJournalConflictError("projection intent cannot mutate this command journal record");const n=normalizeAgentProjectionIntent(e.intent,t);if(t.agentProjectionIntent){if(canonicalizeJcs(t.agentProjectionIntent)!==canonicalizeJcs(n))throw new RuntimeCommandJournalConflictError("agent projection intent conflicts with committed operation");return t}if("pending"!==t.agentProjectionPreparationFailure?.state||!sameProjectionPreparationBase(t.agentProjectionPreparationFailure,n))throw new RuntimeCommandJournalConflictError("projection intent requires matching retryable preparation failure");const{agentProjectionPreparationFailure:o,...r}=t,i={...r,agentProjectionIntent:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,i),i}async recordAgentProjectionBlocked(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision||"effect_committed"!==t.phase||!t.agentProjectionIntent||t.agentProjectionIntent.projectionRevision!==e.revision)throw new RuntimeCommandJournalConflictError("blocked projection cannot mutate this command journal record");if(t.agentProjectionTerminal){if("blocked"===t.agentProjectionTerminal.state&&t.agentProjectionTerminal.revision===e.revision)return t;throw new RuntimeCommandJournalConflictError("agent projection terminal conflicts with committed operation")}const n={...t,agentProjectionTerminal:agentProjectionTerminalReceipt(e.revision),revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,n),n}async compact(){return 0}async checkpoint(){return 0}}function persistedResult(e,t){return"canonical_result"===runtimeCommandResultPersistencePolicy(e)?{result:t}:{resultReceipt:{policy:"hash_only",resultHash:runtimeCommandResultHash(t),redacted:{kind:runtimeCommandIsReplayableQuery(e)?"query_result":"mutation_result",command:e}}}}function projectionReceipt(e){if("synced"!==e.state||!/^(0|[1-9][0-9]*)$/.test(e.revision))throw new Error("invalid projection revision");if(void 0!==e.platformAgentId&&!/^[a-zA-Z0-9._:-]{1,256}$/.test(e.platformAgentId))throw new Error("invalid projection platform agent id");const t=void 0===e.mcpReceipt?void 0:normalizeMcpProjectionReceipt(e.mcpReceipt),n=Object.freeze({revision:e.revision,state:"synced",..."string"==typeof e.platformAgentId&&e.platformAgentId?{platformAgentId:e.platformAgentId}:{},...t?{mcpReceipt:t}:{}});return Object.freeze({schemaVersion:1,revision:n.revision,snapshot:n,snapshotHash:runtimeCommandResultHash(n)})}function validateRecordReceipts(e){if(void 0!==e.agentProjectionIntent&&void 0!==e.agentProjectionPreparationFailure)throw new Error("agent projection journal record is ambiguous");const t=void 0!==e.agentProjectionIntent?{...e,agentProjectionIntent:normalizeAgentProjectionIntent(e.agentProjectionIntent,e)}:void 0!==e.agentProjectionPreparationFailure?{...e,agentProjectionPreparationFailure:normalizeAgentProjectionPreparation(e.agentProjectionPreparationFailure,e)}:e,n=void 0===t.agentProjectionTerminal?t:{...t,agentProjectionTerminal:normalizeAgentProjectionTerminal(t.agentProjectionTerminal)};if(n.agentProjectionTerminal&&(!n.agentProjectionIntent||"effect_committed"!==n.phase||n.agentProjectionIntent.projectionRevision!==n.agentProjectionTerminal.revision))throw new Error("invalid agent projection terminal receipt");const o=void 0===n.mcpDesiredStateCommit?void 0:normalizeMcpDesiredStateCommit(n.mcpDesiredStateCommit);o&&assertMcpCommitBindsRecord(o,n);const r=void 0===n.mcpLiveRelease?void 0:normalizeMcpLiveRelease(n.mcpLiveRelease);if(r&&(!o||!sameMcpDesiredStateCommit(o,r)||"effect_committed"!==n.phase&&"projection_committed"!==n.phase||"service"===r.source&&!n.messageBinding))throw new Error("invalid MCP live release receipt");const i=void 0===n.mcpActiveDesiredState?void 0:normalizeMcpActiveDesiredState(n.mcpActiveDesiredState);if(i&&(!r||!sameMcpDesiredStateCommit(i,r)||i.effectRecordHash!==r.effectRecordHash||i.releasedAt!==r.releasedAt))throw new Error("invalid MCP active desired-state receipt");const a={...n,...o?{mcpDesiredStateCommit:o}:{},...r?{mcpLiveRelease:r}:{},...i?{mcpActiveDesiredState:i}:{}};if(!a.projectionReceipt)return a;const s=a.projectionReceipt,c=["revision","state",...void 0===s.snapshot?.platformAgentId?[]:["platformAgentId"],...void 0===s.snapshot?.mcpReceipt?[]:["mcpReceipt"]].sort().join(",");if("revision,schemaVersion,snapshot,snapshotHash"!==Object.keys(s).sort().join(",")||Object.keys(s.snapshot??{}).sort().join(",")!==c||"projection_committed"!==a.phase||1!==s.schemaVersion||!/^(0|[1-9][0-9]*)$/.test(s.revision)||"synced"!==s.snapshot?.state||s.snapshot.revision!==s.revision)throw new Error("invalid projection receipt");const d=projectionReceipt(s.snapshot),m=d.snapshot.mcpReceipt;if(m&&(m.operationId!==a.operationId||m.method!==a.command||m.runtimeEpoch!==a.runtimeEpoch))throw new Error("MCP projection receipt does not bind journal operation");if(d.snapshotHash!==s.snapshotHash)throw new Error("invalid projection receipt hash");return{...a,projectionReceipt:d}}function normalizeMcpDesiredStateCommit(e){if(!e||"object"!=typeof e||"desiredStateHash,desiredStateRevision,method,operationId,paramsHash,runtimeEpoch,schemaVersion,server,source"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"service"!==e.source&&"acp_local"!==e.source||!safeReceiptIdentifier(e.operationId)||"mcp.set"!==e.method&&"mcp.unset"!==e.method||!isSha256(e.paramsHash)||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||!/^(0|[1-9][0-9]*)$/.test(e.desiredStateRevision)||!isSha256(e.desiredStateHash))throw new Error("invalid MCP desired-state commit");return Object.freeze({schemaVersion:1,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:normalizeMcpReleaseServer(e.server)})}function normalizeMcpLiveRelease(e){if(!e||"object"!=typeof e)throw new Error("invalid MCP live release receipt");const t=["desiredStateHash","desiredStateRevision","method","operationId","paramsHash","releasedAt","runtimeEpoch","schemaVersion","server","source",..."service"===e.source?["effectRecordHash"]:[]].sort().join(",");if(Object.keys(e).sort().join(",")!==t||"service"===e.source&&!isSha256(e.effectRecordHash)||"acp_local"===e.source&&void 0!==e.effectRecordHash||"string"!=typeof e.releasedAt||!Number.isFinite(Date.parse(e.releasedAt)))throw new Error("invalid MCP live release receipt");const n=normalizeMcpDesiredStateCommit({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server});return Object.freeze({...n,...e.effectRecordHash?{effectRecordHash:e.effectRecordHash}:{},releasedAt:e.releasedAt})}function normalizeMcpActiveDesiredState(e){if(!e||"object"!=typeof e)throw new Error("invalid MCP active desired-state receipt");const t=["activatedAt","desiredStateHash","desiredStateRevision","method","operationId","paramsHash","releasedAt","runtimeEpoch","schemaVersion","server","source",..."service"===e.source?["effectRecordHash","terminalMessageId"]:[]].sort().join(",");if(Object.keys(e).sort().join(",")!==t||"string"!=typeof e.activatedAt||!Number.isFinite(Date.parse(e.activatedAt))||"service"===e.source&&!safeReceiptIdentifier(e.terminalMessageId))throw new Error("invalid MCP active desired-state receipt");const n=normalizeMcpLiveRelease({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server,releasedAt:e.releasedAt,...e.effectRecordHash?{effectRecordHash:e.effectRecordHash}:{}});return Object.freeze({...n,...e.terminalMessageId?{terminalMessageId:e.terminalMessageId}:{},activatedAt:e.activatedAt})}function normalizeMcpReleaseServer(e){const t=e&&"object"==typeof e?Object.keys(e).sort().join(","):"",n=!0===e?.removed;if(!(t===(n?"name,removed":void 0===e?.credentialRef?"definitionRef,definitionSha256,name,version":"credentialRef,definitionRef,definitionSha256,name,version")&&safeReceiptIdentifier(e?.name)&&(n||safeMcpReference(e?.definitionRef)&&safeMcpReference(e?.version)&&isSha256(e?.definitionSha256)&&(void 0===e?.credentialRef||safeMcpReference(e.credentialRef)))))throw new Error("invalid MCP desired-state server identity");return Object.freeze(n?{name:e.name,removed:!0}:{name:e.name,definitionRef:e.definitionRef,version:e.version,definitionSha256:e.definitionSha256,...e.credentialRef?{credentialRef:e.credentialRef}:{}})}function assertMcpCommitBindsRecord(e,t){if(e.operationId!==t.operationId||e.method!==t.command||e.paramsHash!==t.paramsHash||e.runtimeEpoch!==t.runtimeEpoch)throw new RuntimeCommandJournalConflictError("MCP desired-state commit does not bind command journal")}function releaseFromCommit(e,t,n){if(t.operationId!==e.operationId||t.source!==e.source||"service"===t.source&&!isSha256(t.effectRecordHash)||"acp_local"===t.source&&void 0!==t.effectRecordHash)throw new RuntimeCommandJournalConflictError("MCP live release proof does not bind desired state");return Object.freeze({...e,...t.effectRecordHash?{effectRecordHash:t.effectRecordHash}:{},releasedAt:n})}function activeFromRelease(e,t,n){if(!(e&&t.operationId===e.operationId&&t.source===e.source&&Number.isFinite(Date.parse(n))&&("service"!==t.source||isSha256(t.effectRecordHash)&&t.effectRecordHash===e.effectRecordHash&&safeReceiptIdentifier(t.terminalMessageId))&&("acp_local"!==t.source||void 0===t.effectRecordHash&&void 0===t.terminalMessageId)))throw new RuntimeCommandJournalConflictError("MCP active promotion does not bind terminal release proof");return Object.freeze({...e,...t.terminalMessageId?{terminalMessageId:t.terminalMessageId}:{},activatedAt:n})}function sameMcpDesiredStateCommit(e,t){if(!e||!t)return!1;const n=e=>({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function sameMcpLiveRelease(e,t){const n=e=>({...e,releasedAt:""});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function sameMcpActiveDesiredState(e,t){const n=e=>({...e,releasedAt:"",activatedAt:""});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function normalizeMcpProjectionReceipt(e){if(!e||"object"!=typeof e||"desiredStateHash,desiredStateRevision,method,operationId,runtimeEpoch,schemaVersion,server"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||!safeReceiptIdentifier(e.operationId)||"mcp.set"!==e.method&&"mcp.unset"!==e.method||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||!/^(0|[1-9][0-9]*)$/.test(e.desiredStateRevision)||!isSha256(e.desiredStateHash))throw new Error("invalid MCP projection receipt");const t=e.server;if((t&&"object"==typeof t?Object.keys(t).sort().join(","):"")!==(!0===t?.removed?"name,removed":void 0===t?.definitionRef?"name":"definitionRef,definitionSha256,name")||!safeReceiptIdentifier(t?.name)||void 0!==t?.definitionRef&&(!safeReceiptIdentifier(t.definitionRef)||!isSha256(t.definitionSha256)))throw new Error("invalid MCP projection server receipt");return Object.freeze({schemaVersion:1,operationId:e.operationId,method:e.method,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:Object.freeze(!0===t.removed?{name:t.name,removed:!0}:void 0===t.definitionRef?{name:t.name}:{name:t.name,definitionRef:t.definitionRef,definitionSha256:t.definitionSha256})})}function isSha256(e){return"string"==typeof e&&/^[a-f0-9]{64}$/.test(e)}function safeReceiptIdentifier(e){return"string"==typeof e&&/^[a-zA-Z0-9._:@/-]{1,512}$/.test(e)&&!/(authorization|bearer|credential|secret|token|password|private(?:_|-)?key|headers?|env)/i.test(e)}function safeMcpReference(e){return"string"==typeof e&&/^[a-zA-Z0-9._:@/-]{1,512}$/.test(e)&&!/(authorization|bearer|secret|token|password|private(?:_|-)?key|headers?|env)/i.test(e)}function agentProjectionTerminalReceipt(e){if(!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("invalid agent projection terminal revision");return Object.freeze({schemaVersion:1,state:"blocked",revision:e})}function normalizeAgentProjectionTerminal(e){if("revision,schemaVersion,state"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"blocked"!==e.state)throw new Error("invalid agent projection terminal receipt");return agentProjectionTerminalReceipt(e.revision)}function normalizeAgentProjectionIntent(e,t){const n=Object.keys(e).sort(),o=["canonicalHash","coreResultHash","method","operationId","projectionRevision","runtimeAgentUid","runtimeEpoch","runtimeLocalKey","schemaVersion","snapshotRef",...void 0===e.expectedPlatformAgentId?[]:["expectedPlatformAgentId"],...void 0===e.removedBindings?[]:["removedBindings"]].sort();if(n.length!==o.length||n.some((e,t)=>e!==o[t])||1!==e.schemaVersion||!["agent.create","agent.update","agent.delete","agent.applyTemplate"].includes(e.method)||!nonEmptyText(e.operationId)||!nonEmptyText(e.runtimeAgentUid)||!nonEmptyText(e.runtimeLocalKey)||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||e.operationId!==t.operationId||e.method!==t.command||e.runtimeEpoch!==t.runtimeEpoch||void 0!==e.expectedPlatformAgentId&&!nonEmptyText(e.expectedPlatformAgentId)||!/^[0-9a-f]{64}$/.test(e.coreResultHash)||!/^(0|[1-9][0-9]*)$/.test(e.projectionRevision)||!/^[0-9a-f]{64}$/.test(e.canonicalHash)||!nonEmptyText(e.snapshotRef)||e.snapshotRef!==projectionSnapshotRef(e.runtimeEpoch,e.projectionRevision,e.canonicalHash)||void 0!==e.removedBindings&&(!Number.isSafeInteger(e.removedBindings)||e.removedBindings<0))throw new Error("invalid agent projection intent");return Object.freeze({...e})}function normalizeAgentProjectionPreparation(e,t){if(isAgentProjectionIntent(e))return normalizeAgentProjectionIntent(e,t);const n=e,o=Object.keys(n).sort(),r=["coreResultHash","errorKind","method","operationId","runtimeAgentUid","runtimeEpoch","runtimeLocalKey","schemaVersion","state",...void 0===n.expectedPlatformAgentId?[]:["expectedPlatformAgentId"],...void 0===n.removedBindings?[]:["removedBindings"]].sort();if(o.length!==r.length||o.some((e,t)=>e!==r[t])||1!==n.schemaVersion||!["agent.create","agent.update","agent.delete","agent.applyTemplate"].includes(n.method)||n.operationId!==t.operationId||n.method!==t.command||n.runtimeEpoch!==t.runtimeEpoch||!nonEmptyText(n.runtimeAgentUid)||!nonEmptyText(n.runtimeLocalKey)||!/^(0|[1-9][0-9]*)$/.test(n.runtimeEpoch)||!/^[0-9a-f]{64}$/.test(n.coreResultHash)||"pending"!==n.state&&"blocked"!==n.state||"snapshot_retryable"!==n.errorKind&&"snapshot_nonretryable"!==n.errorKind||"pending"===n.state!=("snapshot_retryable"===n.errorKind)||void 0!==n.expectedPlatformAgentId&&!nonEmptyText(n.expectedPlatformAgentId)||void 0!==n.removedBindings&&(!Number.isSafeInteger(n.removedBindings)||n.removedBindings<0))throw new Error("invalid agent projection preparation failure");return Object.freeze({...n})}function sameProjectionPreparationBase(e,t){return e.operationId===t.operationId&&e.method===t.method&&e.runtimeEpoch===t.runtimeEpoch&&e.runtimeAgentUid===t.runtimeAgentUid&&e.runtimeLocalKey===t.runtimeLocalKey&&e.coreResultHash===t.coreResultHash&&e.expectedPlatformAgentId===t.expectedPlatformAgentId&&e.removedBindings===t.removedBindings}function isAgentProjectionIntent(e){return"projectionRevision"in e}function projectionSnapshotRef(e,t,n){return`runtime-agent-projection:v1:${e}:${t}:${n}`}function nonEmptyText(e){return"string"==typeof e&&e.length>0}
1
+ import{createHash,randomUUID}from"node:crypto";import{readFile}from"node:fs/promises";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";import{createRuntimeAppendTimingRecorder}from"./runtime-append-timing.js";import{OwnedDirectoryLockContendedError,withOwnedDirectoryLock}from"./owned-directory-lock.js";import{MonotonicJournalSegments}from"./monotonic-journal-segments.js";const HASH_ONLY_RESULT_COMMANDS=new Set(["session.load","session.history","session.list","agent.listProjection","skills.list","models.list","cron.list","cron.status","cron.runs","mcp.list","terminal.ping","terminal.wait"]),SAFE_CANONICAL_RESULT_COMMANDS=new Set(["session.clear","session.setMode","session.setModel","session.cancel","skills.install","skills.uninstall","skills.setEnabled","cron.remove","mcp.prepare","mcp.set","mcp.unset","terminal.input","terminal.resize","terminal.close","terminal.release","terminal.kill","canvas.toggle","canvas.clear","canvas.reference","canvas.dimensions"]);export const runtimeCommandResultPersistencePolicy=e=>SAFE_CANONICAL_RESULT_COMMANDS.has(e)?"canonical_result":"hash_only";export const runtimeCommandIsReplayableQuery=e=>HASH_ONLY_RESULT_COMMANDS.has(e);export const runtimeCommandResultHash=e=>createHash("sha256").update(canonicalizeJcs(e)).digest("hex");const emptyDocument=()=>({version:2,lineage:{journalStoreId:randomUUID(),journalGeneration:"1"},records:[],cancellations:[]}),CHECKPOINT_INTERVAL_MS=6e5,CHECKPOINT_DELTA_COUNT=512,CHECKPOINT_DELTA_BYTES=8388608,CHECKPOINT_FOREGROUND_QUIET_MS=50;export class RuntimeCommandJournalConflictError extends Error{constructor(e){super(e),this.name="RuntimeCommandJournalConflictError"}}export class FileRuntimeCommandJournal{file;retentionMs;now;publication;segments;maintenance;forceCheckpointRequested=!1;maintenanceDue;maintenanceDueAtMs;prunePending=!1;foregroundUpdates=0;foregroundRevision=0;constructor(e,t=6048e5,n=Date.now,o={}){this.file=e,this.retentionMs=t,this.now=n,this.publication=o,this.segments=new MonotonicJournalSegments(e)}async lineage(){return await this.update(e=>({...e.lineage}))}async advanceLineageGeneration(e){return await this.update(t=>{if(t.lineage.journalStoreId!==e.journalStoreId)throw new RuntimeCommandJournalConflictError("journal store changed during incarnation advance");const n=BigInt(t.lineage.journalGeneration),o=BigInt(e.floor),r=(n>o?n:o)+1n;if(r>9223372036854775807n)throw new RuntimeCommandJournalConflictError("journal generation overflow");return t.lineage={journalStoreId:t.lineage.journalStoreId,journalGeneration:String(r)},{...t.lineage}})}async get(e){return(await this.read()).records.find(t=>t.operationId===e)??null}async getCancellation(e){return(await this.read()).cancellations.find(t=>t.operationId===e)??null}async listMcpLiveReleases(){const e=await this.read();return Object.freeze(e.records.flatMap(e=>e.mcpLiveRelease?[e.mcpLiveRelease]:[]))}async listMcpActiveDesiredStates(){const e=await this.read();return Object.freeze(e.records.flatMap(e=>e.mcpActiveDesiredState?[e.mcpActiveDesiredState]:[]))}async recordMcpDesiredStateCommit(e){const t=normalizeMcpDesiredStateCommit(e);return await this.update(e=>{const n=e.records.findIndex(e=>e.operationId===t.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP desired state has no command record");const o=e.records[n];if(assertMcpCommitBindsRecord(t,o),"started"!==o.phase){if(sameMcpDesiredStateCommit(o.mcpDesiredStateCommit,t))return o;throw new RuntimeCommandJournalConflictError("MCP desired state commit crossed its journal boundary")}if(o.mcpDesiredStateCommit){if(sameMcpDesiredStateCommit(o.mcpDesiredStateCommit,t))return o;throw new RuntimeCommandJournalConflictError("MCP desired state commit conflicts with journal")}const r={...o,mcpDesiredStateCommit:t,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return e.records[n]=r,r})}async releaseMcpLiveEffect(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP release has no command record");const o=t.records[n];if("effect_committed"!==o.phase&&"projection_committed"!==o.phase)throw new RuntimeCommandJournalConflictError("MCP release requires a committed effect");if(!o.mcpDesiredStateCommit)throw new RuntimeCommandJournalConflictError("MCP release lacks a durable desired-state commit");if("service"===e.source&&!o.messageBinding)throw new RuntimeCommandJournalConflictError("Service MCP release lacks a Message Service request binding");const r=releaseFromCommit(o.mcpDesiredStateCommit,e,new Date(this.now()).toISOString());if(o.mcpLiveRelease){if(sameMcpLiveRelease(o.mcpLiveRelease,r))return o;throw new RuntimeCommandJournalConflictError("MCP release conflicts with journal")}const i={...o,mcpLiveRelease:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async promoteMcpDesiredStateActive(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("MCP active promotion has no command record");const o=t.records[n],r=activeFromRelease(o.mcpLiveRelease,e,new Date(this.now()).toISOString());if(o.mcpActiveDesiredState){if(sameMcpActiveDesiredState(o.mcpActiveDesiredState,r))return o;throw new RuntimeCommandJournalConflictError("MCP active desired state conflicts with journal")}const i={...o,mcpActiveDesiredState:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async acceptMessageRequest(e){return await this.update(t=>{let n=t.records.find(t=>t.operationId===e.operationId);if(n||(n={operationId:e.operationId,command:e.command,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,phase:"prepared",revision:1,updatedAt:new Date(this.now()).toISOString()},t.records.push(n)),n.command!==e.command||n.paramsHash!==e.paramsHash||n.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("message request conflicts with command journal");const o={requestMessageId:e.requestMessageId,requestHash:e.requestHash,acceptedAt:new Date(this.now()).toISOString()};if(n.messageBinding&&(n.messageBinding.requestMessageId!==o.requestMessageId||n.messageBinding.requestHash!==o.requestHash))throw new RuntimeCommandJournalConflictError("operation reused by a different message request");return n.messageBinding??=o,n})}async observeCancellation(e){return await this.update(t=>{const n=t.cancellations.find(t=>t.eventId===e.eventId);if(n){if(n.operationId!==e.operationId||n.requestMessageId!==e.requestMessageId||n.requestHash!==e.requestHash)throw new RuntimeCommandJournalConflictError("operation cancellation observation conflict");return n}const o=t.cancellations.find(t=>t.operationId===e.operationId);if(o&&(o.requestMessageId!==e.requestMessageId||o.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("operation cancellation request correlation conflict");const r={...e,observedAt:new Date(this.now()).toISOString()};return t.cancellations.push(r),r})}async prepare(e){return await this.update(t=>{const n=t.records.find(t=>t.operationId===e.operationId);if(n){if(n.command!==e.command||n.paramsHash!==e.paramsHash)throw new RuntimeCommandJournalConflictError("operation identity reused with different command or params hash");if(n.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("operation belongs to a different runtime epoch");return n}const o={...e,phase:"prepared",revision:1,updatedAt:new Date(this.now()).toISOString()};return t.records.push(o),o})}async transition(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision)throw new RuntimeCommandJournalConflictError("journal revision conflict");const r="effect_committed"===o.phase&&"effect_committed"===e.phase&&void 0!==e.agentProjection&&void 0===o.agentProjectionIntent&&"pending"===o.agentProjectionPreparationFailure?.state;if("projection_committed"===o.phase||"failed"===o.phase||"outcome_unknown"===o.phase||"effect_committed"===o.phase&&"projection_committed"!==e.phase&&!r)throw new RuntimeCommandJournalConflictError("terminal journal record is immutable");if(void 0!==e.projectionSnapshot&&"projection_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("projection receipt requires projection_committed phase");if(void 0!==e.agentProjection&&"effect_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("agent projection intent requires effect_committed phase");const i=void 0===e.agentProjection?void 0:normalizeAgentProjectionPreparation(e.agentProjection,o);if(r&&i&&!sameProjectionPreparationBase(o.agentProjectionPreparationFailure,i))throw new RuntimeCommandJournalConflictError("agent projection preparation conflicts with committed operation");const{agentProjectionIntent:a,agentProjectionPreparationFailure:s,...c}=o,d={...void 0===i?o:c,phase:e.phase,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString(),...void 0===e.result?{}:persistedResult(o.command,e.result),...void 0===e.projectionSnapshot?{}:{projectionReceipt:projectionReceipt(e.projectionSnapshot)},...void 0===i?{}:isAgentProjectionIntent(i)?{agentProjectionIntent:i}:{agentProjectionPreparationFailure:i},...void 0===e.errorCode?{}:{errorCode:e.errorCode}};return t.records[n]=d,d})}async recordAgentProjectionIntent(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision||"effect_committed"!==o.phase)throw new RuntimeCommandJournalConflictError("projection intent cannot mutate this command journal record");const r=normalizeAgentProjectionIntent(e.intent,o);if(o.agentProjectionIntent){if(canonicalizeJcs(o.agentProjectionIntent)!==canonicalizeJcs(r))throw new RuntimeCommandJournalConflictError("agent projection intent conflicts with committed operation");return o}if("pending"!==o.agentProjectionPreparationFailure?.state||!sameProjectionPreparationBase(o.agentProjectionPreparationFailure,r))throw new RuntimeCommandJournalConflictError("projection intent requires matching retryable preparation failure");const{agentProjectionPreparationFailure:i,...a}=o,s={...a,agentProjectionIntent:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=s,s})}async recordAgentProjectionBlocked(e){return await this.update(t=>{const n=t.records.findIndex(t=>t.operationId===e.operationId);if(n<0)throw new RuntimeCommandJournalConflictError("operation was not prepared");const o=t.records[n];if(o.revision!==e.expectedRevision||"effect_committed"!==o.phase||!o.agentProjectionIntent||o.agentProjectionIntent.projectionRevision!==e.revision)throw new RuntimeCommandJournalConflictError("blocked projection cannot mutate this command journal record");if(o.agentProjectionTerminal){if("blocked"===o.agentProjectionTerminal.state&&o.agentProjectionTerminal.revision===e.revision)return o;throw new RuntimeCommandJournalConflictError("agent projection terminal conflicts with committed operation")}const r=agentProjectionTerminalReceipt(e.revision),i={...o,agentProjectionTerminal:r,revision:o.revision+1,updatedAt:new Date(this.now()).toISOString()};return t.records[n]=i,i})}async compact(){return this.maintenance?await this.maintenance:!1===this.maintenanceDue&&!this.prunePending&&void 0!==this.maintenanceDueAtMs&&Date.now()<this.maintenanceDueAtMs?0:await this.startMaintenance()}async checkpoint(){return this.forceCheckpointRequested=!0,await this.startMaintenance()}async read(){return(await this.readState()).document}async readState(){const e=await this.readLegacy(),t=new Map;if(e){t.set("meta:lineage",{value:e.lineage,logicalRevision:e.lineage.journalGeneration,publisherEpoch:"0"});for(const n of e.records)t.set(`record:${n.operationId}`,{value:n,logicalRevision:String(n.revision),publisherEpoch:"0"});for(const n of e.cancellations)t.set(`cancellation:${n.eventId}`,{value:n,logicalRevision:"1",publisherEpoch:"0"})}const n=await this.segments.readState(t),o=n.values,r=o.get("meta:lineage")?.value;return{document:{version:2,lineage:r??emptyDocument().lineage,records:[...o.entries()].filter(([e])=>e.startsWith("record:")).map(([,e])=>validateRecordReceipts(migrateLegacyCommittedPhase(e.value))),cancellations:[...o.entries()].filter(([e])=>e.startsWith("cancellation:")).map(([,e])=>e.value)},authorityFloor:n.authorityFloor,checkpointAtMs:n.checkpointAtMs,deltaCount:n.deltaCount,deltaBytes:n.deltaBytes,oldestDeltaAtMs:n.oldestDeltaAtMs,hasDurableLineage:o.has("meta:lineage")&&n.hasDurableState}}async readLegacy(){try{const e=JSON.parse(await readFile(this.file,"utf8"));if(!Array.isArray(e.records))throw new Error("unsupported command journal");const t=e.records.map(e=>validateRecordReceipts(migrateLegacyCommittedPhase(e)));if(1===e.version)return{...emptyDocument(),records:t};if(2!==e.version||!e.lineage?.journalStoreId||!/^[1-9][0-9]*$/.test(e.lineage.journalGeneration))throw new Error("unsupported command journal");return{...e,records:t,cancellations:Array.isArray(e.cancellations)?e.cancellations:[]}}catch(e){if("ENOENT"===e.code)return null;throw e}}async update(e){const t=createRuntimeAppendTimingRecorder("journal.update",{file:this.file});let n=!1;this.foregroundUpdates+=1;try{return await withOwnedDirectoryLock(`${this.file}.lock`,async o=>{t.mark("lock");const r=await this.readState();t.mark("read");const i=r.document,a=structuredClone(i),s=e(i),c=[];if(r.hasDurableLineage&&sameValue(a.lineage,i.lineage)||c.push({key:"meta:lineage",logicalRevision:i.lineage.journalGeneration,value:i.lineage}),diffValues(a.records,i.records,e=>e.operationId,e=>String(e.revision),"record:",c),diffValues(a.cancellations,i.cancellations,e=>e.eventId,()=>"1","cancellation:",c),0===c.length)return t.mark("noop"),s;await(this.publication.beforePublish?.(o.epoch)),t.mark("before-publish"),await o.assertOwned(),t.mark("assert-owned");const d=await this.segments.publishDelta(o.epoch,c,r.authorityFloor);t.mark("publish-delta");const m=Date.now();return this.maintenanceDueAtMs=checkpointDueAt(r,1,d,m),n=this.maintenanceDueAtMs<=m,this.maintenanceDue=n,s},{...this.publication.lockOptions,epochFloor:()=>this.segments.authorityFloor()})}finally{this.foregroundUpdates-=1,this.foregroundRevision+=1,t.finish(),n&&this.scheduleMaintenance()}}scheduleMaintenance(){this.startMaintenance().catch(e=>{try{this.publication.onMaintenanceError?this.publication.onMaintenanceError(e):emitMaintenanceWarning(e)}catch(e){emitMaintenanceWarning(e)}})}startMaintenance(){if(this.maintenance)return this.maintenance;const e=(async()=>{let e=0,t=!1;do{let n=this.forceCheckpointRequested;this.forceCheckpointRequested=!1,n||(await this.waitForForegroundQuiet(),n=this.forceCheckpointRequested,this.forceCheckpointRequested=!1);const o=await this.runMaintenance(n);e+=o.removed,t=o.contended}while(this.forceCheckpointRequested||t);return e})().finally(()=>{this.maintenance===e&&(this.maintenance=void 0)});return this.maintenance=e,e}async waitForForegroundQuiet(){for(;;){if(this.forceCheckpointRequested)return;const e=this.foregroundRevision;if(0===this.foregroundUpdates){if(await new Promise(e=>setTimeout(e,50)),this.forceCheckpointRequested||0===this.foregroundUpdates&&this.foregroundRevision===e)return}else await new Promise(e=>setTimeout(e,50))}}async runMaintenance(e){const t=createRuntimeAppendTimingRecorder("journal.checkpoint",{file:this.file});try{if(!e&&this.prunePending)return await this.segments.prune(),this.prunePending=!1,t.mark("prune-pending"),{removed:0,contended:!1};let n=!1;const o=await withOwnedDirectoryLock(`${this.file}.lock`,async o=>{t.mark("lock");const r=await this.readState();if(t.mark("read"),!e&&!checkpointDue(r))return this.maintenanceDue=!1,this.maintenanceDueAtMs=checkpointDueAt(r),t.mark("not-due"),0;const i=r.document,a=this.now()-this.retentionMs,s=i.records.length;i.records=i.records.filter(e=>"prepared"===e.phase||"started"===e.phase||Date.parse(e.updatedAt)>=a);const c=i.cancellations.length;i.cancellations=i.cancellations.filter(e=>Date.parse(e.observedAt)>=a);const d=s-i.records.length+c-i.cancellations.length,m=documentValues(i);return await(this.publication.beforePublish?.(o.epoch)),t.mark("before-publish"),await o.assertOwned(),t.mark("assert-owned"),await this.segments.checkpointSnapshot(o.epoch,m,r.authorityFloor),t.mark("publish-snapshot"),this.maintenanceDue=!1,this.maintenanceDueAtMs=Number.POSITIVE_INFINITY,this.prunePending=!0,n=!0,d},{...this.publication.lockOptions,failIfContended:!e,epochFloor:()=>this.segments.authorityFloor()});return t.mark("lock-released"),n&&(await this.segments.prune(),this.prunePending=!1,t.mark("prune")),{removed:o,contended:!1}}catch(n){if(!e&&n instanceof OwnedDirectoryLockContendedError)return t.mark("deferred-busy"),{removed:0,contended:!0};throw n}finally{t.finish(e?"forced":"scheduled")}}}function checkpointDue(e,t=0,n=0,o=Date.now()){return checkpointDueAt(e,t,n,o)<=o}function checkpointDueAt(e,t=0,n=0,o=Date.now()){const r=e.deltaCount+t,i=e.deltaBytes+n,a=e.oldestDeltaAtMs||(t>0?o:0),s=e.checkpointAtMs||a;return r>=512||i>=8388608?o:r>0&&s>0?s+6e5:Number.POSITIVE_INFINITY}function emitMaintenanceWarning(e){process.emitWarning(e instanceof Error?e.message:String(e),{code:"BEEOS_RUNTIME_JOURNAL_CHECKPOINT_FAILED"})}function diffValues(e,t,n,o,r,i){const a=new Map(e.map(e=>[n(e),e])),s=new Map(t.map(e=>[n(e),e]));for(const[e,t]of s){const n=a.get(e);void 0!==n&&sameValue(n,t)||i.push({key:`${r}${e}`,logicalRevision:o(t),value:t})}for(const[e,t]of a)s.has(e)||i.push({key:`${r}${e}`,logicalRevision:o(t),tombstone:!0})}function documentValues(e){return[{key:"meta:lineage",logicalRevision:e.lineage.journalGeneration,value:e.lineage},...e.records.map(e=>({key:`record:${e.operationId}`,logicalRevision:String(e.revision),value:e})),...e.cancellations.map(e=>({key:`cancellation:${e.eventId}`,logicalRevision:"1",value:e}))]}function sameValue(e,t){return canonicalizeJcs(e)===canonicalizeJcs(t)}function migrateLegacyCommittedPhase(e){return{...e,phase:"committed"===e.phase?"effect_committed":e.phase}}export class InMemoryRuntimeCommandJournal{records=new Map;cancellations=new Map;async get(e){return this.records.get(e)??null}async getCancellation(e){return[...this.cancellations.values()].find(t=>t.operationId===e)??null}async listMcpLiveReleases(){return Object.freeze([...this.records.values()].flatMap(e=>e.mcpLiveRelease?[e.mcpLiveRelease]:[]))}async listMcpActiveDesiredStates(){return Object.freeze([...this.records.values()].flatMap(e=>e.mcpActiveDesiredState?[e.mcpActiveDesiredState]:[]))}async recordMcpDesiredStateCommit(e){const t=normalizeMcpDesiredStateCommit(e),n=this.records.get(t.operationId);if(!n)throw new RuntimeCommandJournalConflictError("MCP desired state has no command record");if(assertMcpCommitBindsRecord(t,n),"started"!==n.phase){if(sameMcpDesiredStateCommit(n.mcpDesiredStateCommit,t))return n;throw new RuntimeCommandJournalConflictError("MCP desired state commit crossed its journal boundary")}if(n.mcpDesiredStateCommit){if(sameMcpDesiredStateCommit(n.mcpDesiredStateCommit,t))return n;throw new RuntimeCommandJournalConflictError("MCP desired state commit conflicts with journal")}const o={...n,mcpDesiredStateCommit:t,revision:n.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(t.operationId,o),o}async releaseMcpLiveEffect(e){const t=this.records.get(e.operationId);if(!t)throw new RuntimeCommandJournalConflictError("MCP release has no command record");if("effect_committed"!==t.phase&&"projection_committed"!==t.phase)throw new RuntimeCommandJournalConflictError("MCP release requires a committed effect");if(!t.mcpDesiredStateCommit)throw new RuntimeCommandJournalConflictError("MCP release lacks a durable desired-state commit");if("service"===e.source&&!t.messageBinding)throw new RuntimeCommandJournalConflictError("Service MCP release lacks a Message Service request binding");const n=releaseFromCommit(t.mcpDesiredStateCommit,e,(new Date).toISOString());if(t.mcpLiveRelease){if(sameMcpLiveRelease(t.mcpLiveRelease,n))return t;throw new RuntimeCommandJournalConflictError("MCP release conflicts with journal")}const o={...t,mcpLiveRelease:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,o),o}async promoteMcpDesiredStateActive(e){const t=this.records.get(e.operationId);if(!t)throw new RuntimeCommandJournalConflictError("MCP active promotion has no command record");const n=activeFromRelease(t.mcpLiveRelease,e,(new Date).toISOString());if(t.mcpActiveDesiredState){if(sameMcpActiveDesiredState(t.mcpActiveDesiredState,n))return t;throw new RuntimeCommandJournalConflictError("MCP active desired state conflicts with journal")}const o={...t,mcpActiveDesiredState:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,o),o}async acceptMessageRequest(e){let t=this.records.get(e.operationId);if(t||(t={operationId:e.operationId,command:e.command,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,phase:"prepared",revision:1,updatedAt:(new Date).toISOString()},this.records.set(e.operationId,t)),t.command!==e.command||t.paramsHash!==e.paramsHash||t.runtimeEpoch!==e.runtimeEpoch||t.messageBinding&&(t.messageBinding.requestMessageId!==e.requestMessageId||t.messageBinding.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("message request conflicts with command journal");return t.messageBinding??={requestMessageId:e.requestMessageId,requestHash:e.requestHash,acceptedAt:(new Date).toISOString()},t}async observeCancellation(e){const t=this.cancellations.get(e.eventId);if(t){if(t.operationId!==e.operationId||t.requestMessageId!==e.requestMessageId||t.requestHash!==e.requestHash)throw new RuntimeCommandJournalConflictError("operation cancellation observation conflict");return t}const n=[...this.cancellations.values()].find(t=>t.operationId===e.operationId);if(n&&(n.requestMessageId!==e.requestMessageId||n.requestHash!==e.requestHash))throw new RuntimeCommandJournalConflictError("operation cancellation request correlation conflict");const o={...e,observedAt:(new Date).toISOString()};return this.cancellations.set(e.eventId,o),o}async prepare(e){const t=this.records.get(e.operationId);if(t){if(t.command!==e.command||t.paramsHash!==e.paramsHash||t.runtimeEpoch!==e.runtimeEpoch)throw new RuntimeCommandJournalConflictError("operation identity conflict");return t}const n={...e,phase:"prepared",revision:1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,n),n}async transition(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision)throw new RuntimeCommandJournalConflictError("revision conflict");const n="effect_committed"===t.phase&&"effect_committed"===e.phase&&void 0!==e.agentProjection&&void 0===t.agentProjectionIntent&&"pending"===t.agentProjectionPreparationFailure?.state;if("projection_committed"===t.phase||"failed"===t.phase||"outcome_unknown"===t.phase||"effect_committed"===t.phase&&"projection_committed"!==e.phase&&!n)throw new RuntimeCommandJournalConflictError("terminal journal record is immutable");if(void 0!==e.projectionSnapshot&&"projection_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("projection receipt requires projection_committed phase");if(void 0!==e.agentProjection&&"effect_committed"!==e.phase)throw new RuntimeCommandJournalConflictError("agent projection intent requires effect_committed phase");const o=void 0===e.agentProjection?void 0:normalizeAgentProjectionPreparation(e.agentProjection,t);if(n&&o&&!sameProjectionPreparationBase(t.agentProjectionPreparationFailure,o))throw new RuntimeCommandJournalConflictError("agent projection preparation conflicts with committed operation");const{agentProjectionIntent:r,agentProjectionPreparationFailure:i,...a}=t,s={...void 0===o?t:a,phase:e.phase,revision:t.revision+1,updatedAt:(new Date).toISOString(),...void 0===e.result?{}:persistedResult(t.command,e.result),...void 0===e.projectionSnapshot?{}:{projectionReceipt:projectionReceipt(e.projectionSnapshot)},...void 0===o?{}:isAgentProjectionIntent(o)?{agentProjectionIntent:o}:{agentProjectionPreparationFailure:o},...void 0===e.errorCode?{}:{errorCode:e.errorCode}};return this.records.set(e.operationId,s),s}async recordAgentProjectionIntent(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision||"effect_committed"!==t.phase)throw new RuntimeCommandJournalConflictError("projection intent cannot mutate this command journal record");const n=normalizeAgentProjectionIntent(e.intent,t);if(t.agentProjectionIntent){if(canonicalizeJcs(t.agentProjectionIntent)!==canonicalizeJcs(n))throw new RuntimeCommandJournalConflictError("agent projection intent conflicts with committed operation");return t}if("pending"!==t.agentProjectionPreparationFailure?.state||!sameProjectionPreparationBase(t.agentProjectionPreparationFailure,n))throw new RuntimeCommandJournalConflictError("projection intent requires matching retryable preparation failure");const{agentProjectionPreparationFailure:o,...r}=t,i={...r,agentProjectionIntent:n,revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,i),i}async recordAgentProjectionBlocked(e){const t=this.records.get(e.operationId);if(!t||t.revision!==e.expectedRevision||"effect_committed"!==t.phase||!t.agentProjectionIntent||t.agentProjectionIntent.projectionRevision!==e.revision)throw new RuntimeCommandJournalConflictError("blocked projection cannot mutate this command journal record");if(t.agentProjectionTerminal){if("blocked"===t.agentProjectionTerminal.state&&t.agentProjectionTerminal.revision===e.revision)return t;throw new RuntimeCommandJournalConflictError("agent projection terminal conflicts with committed operation")}const n={...t,agentProjectionTerminal:agentProjectionTerminalReceipt(e.revision),revision:t.revision+1,updatedAt:(new Date).toISOString()};return this.records.set(e.operationId,n),n}async compact(){return 0}async checkpoint(){return 0}}function persistedResult(e,t){return"canonical_result"===runtimeCommandResultPersistencePolicy(e)?{result:t}:{resultReceipt:{policy:"hash_only",resultHash:runtimeCommandResultHash(t),redacted:{kind:runtimeCommandIsReplayableQuery(e)?"query_result":"mutation_result",command:e}}}}function projectionReceipt(e){if("synced"!==e.state||!/^(0|[1-9][0-9]*)$/.test(e.revision))throw new Error("invalid projection revision");if(void 0!==e.platformAgentId&&!/^[a-zA-Z0-9._:-]{1,256}$/.test(e.platformAgentId))throw new Error("invalid projection platform agent id");const t=void 0===e.mcpReceipt?void 0:normalizeMcpProjectionReceipt(e.mcpReceipt),n=Object.freeze({revision:e.revision,state:"synced",..."string"==typeof e.platformAgentId&&e.platformAgentId?{platformAgentId:e.platformAgentId}:{},...t?{mcpReceipt:t}:{}});return Object.freeze({schemaVersion:1,revision:n.revision,snapshot:n,snapshotHash:runtimeCommandResultHash(n)})}function validateRecordReceipts(e){if(void 0!==e.agentProjectionIntent&&void 0!==e.agentProjectionPreparationFailure)throw new Error("agent projection journal record is ambiguous");const t=void 0!==e.agentProjectionIntent?{...e,agentProjectionIntent:normalizeAgentProjectionIntent(e.agentProjectionIntent,e)}:void 0!==e.agentProjectionPreparationFailure?{...e,agentProjectionPreparationFailure:normalizeAgentProjectionPreparation(e.agentProjectionPreparationFailure,e)}:e,n=void 0===t.agentProjectionTerminal?t:{...t,agentProjectionTerminal:normalizeAgentProjectionTerminal(t.agentProjectionTerminal)};if(n.agentProjectionTerminal&&(!n.agentProjectionIntent||"effect_committed"!==n.phase||n.agentProjectionIntent.projectionRevision!==n.agentProjectionTerminal.revision))throw new Error("invalid agent projection terminal receipt");const o=void 0===n.mcpDesiredStateCommit?void 0:normalizeMcpDesiredStateCommit(n.mcpDesiredStateCommit);o&&assertMcpCommitBindsRecord(o,n);const r=void 0===n.mcpLiveRelease?void 0:normalizeMcpLiveRelease(n.mcpLiveRelease);if(r&&(!o||!sameMcpDesiredStateCommit(o,r)||"effect_committed"!==n.phase&&"projection_committed"!==n.phase||"service"===r.source&&!n.messageBinding))throw new Error("invalid MCP live release receipt");const i=void 0===n.mcpActiveDesiredState?void 0:normalizeMcpActiveDesiredState(n.mcpActiveDesiredState);if(i&&(!r||!sameMcpDesiredStateCommit(i,r)||i.effectRecordHash!==r.effectRecordHash||i.releasedAt!==r.releasedAt))throw new Error("invalid MCP active desired-state receipt");const a={...n,...o?{mcpDesiredStateCommit:o}:{},...r?{mcpLiveRelease:r}:{},...i?{mcpActiveDesiredState:i}:{}};if(!a.projectionReceipt)return a;const s=a.projectionReceipt,c=["revision","state",...void 0===s.snapshot?.platformAgentId?[]:["platformAgentId"],...void 0===s.snapshot?.mcpReceipt?[]:["mcpReceipt"]].sort().join(",");if("revision,schemaVersion,snapshot,snapshotHash"!==Object.keys(s).sort().join(",")||Object.keys(s.snapshot??{}).sort().join(",")!==c||"projection_committed"!==a.phase||1!==s.schemaVersion||!/^(0|[1-9][0-9]*)$/.test(s.revision)||"synced"!==s.snapshot?.state||s.snapshot.revision!==s.revision)throw new Error("invalid projection receipt");const d=projectionReceipt(s.snapshot),m=d.snapshot.mcpReceipt;if(m&&(m.operationId!==a.operationId||m.method!==a.command||m.runtimeEpoch!==a.runtimeEpoch))throw new Error("MCP projection receipt does not bind journal operation");if(d.snapshotHash!==s.snapshotHash)throw new Error("invalid projection receipt hash");return{...a,projectionReceipt:d}}function normalizeMcpDesiredStateCommit(e){if(!e||"object"!=typeof e||"desiredStateHash,desiredStateRevision,method,operationId,paramsHash,runtimeEpoch,schemaVersion,server,source"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"service"!==e.source&&"acp_local"!==e.source||!safeReceiptIdentifier(e.operationId)||"mcp.set"!==e.method&&"mcp.unset"!==e.method||!isSha256(e.paramsHash)||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||!/^(0|[1-9][0-9]*)$/.test(e.desiredStateRevision)||!isSha256(e.desiredStateHash))throw new Error("invalid MCP desired-state commit");return Object.freeze({schemaVersion:1,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:normalizeMcpReleaseServer(e.server)})}function normalizeMcpLiveRelease(e){if(!e||"object"!=typeof e)throw new Error("invalid MCP live release receipt");const t=["desiredStateHash","desiredStateRevision","method","operationId","paramsHash","releasedAt","runtimeEpoch","schemaVersion","server","source",..."service"===e.source?["effectRecordHash"]:[]].sort().join(",");if(Object.keys(e).sort().join(",")!==t||"service"===e.source&&!isSha256(e.effectRecordHash)||"acp_local"===e.source&&void 0!==e.effectRecordHash||"string"!=typeof e.releasedAt||!Number.isFinite(Date.parse(e.releasedAt)))throw new Error("invalid MCP live release receipt");const n=normalizeMcpDesiredStateCommit({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server});return Object.freeze({...n,...e.effectRecordHash?{effectRecordHash:e.effectRecordHash}:{},releasedAt:e.releasedAt})}function normalizeMcpActiveDesiredState(e){if(!e||"object"!=typeof e)throw new Error("invalid MCP active desired-state receipt");const t=["activatedAt","desiredStateHash","desiredStateRevision","method","operationId","paramsHash","releasedAt","runtimeEpoch","schemaVersion","server","source",..."service"===e.source?["effectRecordHash","terminalMessageId"]:[]].sort().join(",");if(Object.keys(e).sort().join(",")!==t||"string"!=typeof e.activatedAt||!Number.isFinite(Date.parse(e.activatedAt))||"service"===e.source&&!safeReceiptIdentifier(e.terminalMessageId))throw new Error("invalid MCP active desired-state receipt");const n=normalizeMcpLiveRelease({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server,releasedAt:e.releasedAt,...e.effectRecordHash?{effectRecordHash:e.effectRecordHash}:{}});return Object.freeze({...n,...e.terminalMessageId?{terminalMessageId:e.terminalMessageId}:{},activatedAt:e.activatedAt})}function normalizeMcpReleaseServer(e){const t=e&&"object"==typeof e?Object.keys(e).sort().join(","):"",n=!0===e?.removed;if(!(t===(n?"name,removed":void 0===e?.credentialRef?"definitionRef,definitionSha256,name,version":"credentialRef,definitionRef,definitionSha256,name,version")&&safeReceiptIdentifier(e?.name)&&(n||safeMcpReference(e?.definitionRef)&&safeMcpReference(e?.version)&&isSha256(e?.definitionSha256)&&(void 0===e?.credentialRef||safeMcpReference(e.credentialRef)))))throw new Error("invalid MCP desired-state server identity");return Object.freeze(n?{name:e.name,removed:!0}:{name:e.name,definitionRef:e.definitionRef,version:e.version,definitionSha256:e.definitionSha256,...e.credentialRef?{credentialRef:e.credentialRef}:{}})}function assertMcpCommitBindsRecord(e,t){if(e.operationId!==t.operationId||e.method!==t.command||e.paramsHash!==t.paramsHash||e.runtimeEpoch!==t.runtimeEpoch)throw new RuntimeCommandJournalConflictError("MCP desired-state commit does not bind command journal")}function releaseFromCommit(e,t,n){if(t.operationId!==e.operationId||t.source!==e.source||"service"===t.source&&!isSha256(t.effectRecordHash)||"acp_local"===t.source&&void 0!==t.effectRecordHash)throw new RuntimeCommandJournalConflictError("MCP live release proof does not bind desired state");return Object.freeze({...e,...t.effectRecordHash?{effectRecordHash:t.effectRecordHash}:{},releasedAt:n})}function activeFromRelease(e,t,n){if(!(e&&t.operationId===e.operationId&&t.source===e.source&&Number.isFinite(Date.parse(n))&&("service"!==t.source||isSha256(t.effectRecordHash)&&t.effectRecordHash===e.effectRecordHash&&safeReceiptIdentifier(t.terminalMessageId))&&("acp_local"!==t.source||void 0===t.effectRecordHash&&void 0===t.terminalMessageId)))throw new RuntimeCommandJournalConflictError("MCP active promotion does not bind terminal release proof");return Object.freeze({...e,...t.terminalMessageId?{terminalMessageId:t.terminalMessageId}:{},activatedAt:n})}function sameMcpDesiredStateCommit(e,t){if(!e||!t)return!1;const n=e=>({schemaVersion:e.schemaVersion,source:e.source,operationId:e.operationId,method:e.method,paramsHash:e.paramsHash,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:e.server});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function sameMcpLiveRelease(e,t){const n=e=>({...e,releasedAt:""});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function sameMcpActiveDesiredState(e,t){const n=e=>({...e,releasedAt:"",activatedAt:""});return canonicalizeJcs(n(e))===canonicalizeJcs(n(t))}function normalizeMcpProjectionReceipt(e){if(!e||"object"!=typeof e||"desiredStateHash,desiredStateRevision,method,operationId,runtimeEpoch,schemaVersion,server"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||!safeReceiptIdentifier(e.operationId)||"mcp.set"!==e.method&&"mcp.unset"!==e.method||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||!/^(0|[1-9][0-9]*)$/.test(e.desiredStateRevision)||!isSha256(e.desiredStateHash))throw new Error("invalid MCP projection receipt");const t=e.server;if((t&&"object"==typeof t?Object.keys(t).sort().join(","):"")!==(!0===t?.removed?"name,removed":void 0===t?.definitionRef?"name":"definitionRef,definitionSha256,name")||!safeReceiptIdentifier(t?.name)||void 0!==t?.definitionRef&&(!safeReceiptIdentifier(t.definitionRef)||!isSha256(t.definitionSha256)))throw new Error("invalid MCP projection server receipt");return Object.freeze({schemaVersion:1,operationId:e.operationId,method:e.method,runtimeEpoch:e.runtimeEpoch,desiredStateRevision:e.desiredStateRevision,desiredStateHash:e.desiredStateHash,server:Object.freeze(!0===t.removed?{name:t.name,removed:!0}:void 0===t.definitionRef?{name:t.name}:{name:t.name,definitionRef:t.definitionRef,definitionSha256:t.definitionSha256})})}function isSha256(e){return"string"==typeof e&&/^[a-f0-9]{64}$/.test(e)}function safeReceiptIdentifier(e){return"string"==typeof e&&/^[a-zA-Z0-9._:@/-]{1,512}$/.test(e)&&!/(authorization|bearer|credential|secret|token|password|private(?:_|-)?key|headers?|env)/i.test(e)}function safeMcpReference(e){return"string"==typeof e&&/^[a-zA-Z0-9._:@/-]{1,512}$/.test(e)&&!/(authorization|bearer|secret|token|password|private(?:_|-)?key|headers?|env)/i.test(e)}function agentProjectionTerminalReceipt(e){if(!/^(0|[1-9][0-9]*)$/.test(e))throw new Error("invalid agent projection terminal revision");return Object.freeze({schemaVersion:1,state:"blocked",revision:e})}function normalizeAgentProjectionTerminal(e){if("revision,schemaVersion,state"!==Object.keys(e).sort().join(",")||1!==e.schemaVersion||"blocked"!==e.state)throw new Error("invalid agent projection terminal receipt");return agentProjectionTerminalReceipt(e.revision)}function normalizeAgentProjectionIntent(e,t){const n=Object.keys(e).sort(),o=["canonicalHash","coreResultHash","method","operationId","projectionRevision","runtimeAgentUid","runtimeEpoch","runtimeLocalKey","schemaVersion","snapshotRef",...void 0===e.expectedPlatformAgentId?[]:["expectedPlatformAgentId"],...void 0===e.removedBindings?[]:["removedBindings"]].sort();if(n.length!==o.length||n.some((e,t)=>e!==o[t])||1!==e.schemaVersion||!["agent.create","agent.update","agent.delete","agent.applyTemplate"].includes(e.method)||!nonEmptyText(e.operationId)||!nonEmptyText(e.runtimeAgentUid)||!nonEmptyText(e.runtimeLocalKey)||!/^(0|[1-9][0-9]*)$/.test(e.runtimeEpoch)||e.operationId!==t.operationId||e.method!==t.command||e.runtimeEpoch!==t.runtimeEpoch||void 0!==e.expectedPlatformAgentId&&!nonEmptyText(e.expectedPlatformAgentId)||!/^[0-9a-f]{64}$/.test(e.coreResultHash)||!/^(0|[1-9][0-9]*)$/.test(e.projectionRevision)||!/^[0-9a-f]{64}$/.test(e.canonicalHash)||!nonEmptyText(e.snapshotRef)||e.snapshotRef!==projectionSnapshotRef(e.runtimeEpoch,e.projectionRevision,e.canonicalHash)||void 0!==e.removedBindings&&(!Number.isSafeInteger(e.removedBindings)||e.removedBindings<0))throw new Error("invalid agent projection intent");return Object.freeze({...e})}function normalizeAgentProjectionPreparation(e,t){if(isAgentProjectionIntent(e))return normalizeAgentProjectionIntent(e,t);const n=e,o=Object.keys(n).sort(),r=["coreResultHash","errorKind","method","operationId","runtimeAgentUid","runtimeEpoch","runtimeLocalKey","schemaVersion","state",...void 0===n.expectedPlatformAgentId?[]:["expectedPlatformAgentId"],...void 0===n.removedBindings?[]:["removedBindings"]].sort();if(o.length!==r.length||o.some((e,t)=>e!==r[t])||1!==n.schemaVersion||!["agent.create","agent.update","agent.delete","agent.applyTemplate"].includes(n.method)||n.operationId!==t.operationId||n.method!==t.command||n.runtimeEpoch!==t.runtimeEpoch||!nonEmptyText(n.runtimeAgentUid)||!nonEmptyText(n.runtimeLocalKey)||!/^(0|[1-9][0-9]*)$/.test(n.runtimeEpoch)||!/^[0-9a-f]{64}$/.test(n.coreResultHash)||"pending"!==n.state&&"blocked"!==n.state||"snapshot_retryable"!==n.errorKind&&"snapshot_nonretryable"!==n.errorKind||"pending"===n.state!=("snapshot_retryable"===n.errorKind)||void 0!==n.expectedPlatformAgentId&&!nonEmptyText(n.expectedPlatformAgentId)||void 0!==n.removedBindings&&(!Number.isSafeInteger(n.removedBindings)||n.removedBindings<0))throw new Error("invalid agent projection preparation failure");return Object.freeze({...n})}function sameProjectionPreparationBase(e,t){return e.operationId===t.operationId&&e.method===t.method&&e.runtimeEpoch===t.runtimeEpoch&&e.runtimeAgentUid===t.runtimeAgentUid&&e.runtimeLocalKey===t.runtimeLocalKey&&e.coreResultHash===t.coreResultHash&&e.expectedPlatformAgentId===t.expectedPlatformAgentId&&e.removedBindings===t.removedBindings}function isAgentProjectionIntent(e){return"projectionRevision"in e}function projectionSnapshotRef(e,t,n){return`runtime-agent-projection:v1:${e}:${t}:${n}`}function nonEmptyText(e){return"string"==typeof e&&e.length>0}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{AROUTER_PROVIDER_ID}from"../media/arouter-config.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;constructor(e,t,i){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{sessionKey:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s="verbose"===t||"detailed"===t?{verboseLevel:"full"}:"reasoning"===t||"thinking"===t?{reasoningLevel:"stream"}:"concise"===t||"default"===t?{verboseLevel:"default"}:null;if(!s)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${t}`);const n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const r=toPlatformModelId(o),a=r.indexOf("/");return{modelId:r,...a>0?{modelProvider:r.slice(0,a)}:{}}}async setModel(e,t,i,s){const n=null===t?null:toOpenClawSessionModel(t),o=s?.attempt.operationId,{key:r,materialized:a}=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s)),l=await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:r,model:n}));if(!isAuthoritativeSetModelAck(l,r,n,t))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const c=l.resolved.model,d=toPlatformModelId(c);if(null===t&&(!i||d!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(null!==t&&d!==t&&c!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");if(a){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(r))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(r)}return{...null===t?{}:{modelOverrideId:t},effectiveModelId:d,...l.resolved?.modelProvider?{modelProvider:l.resolved.modelProvider}:{}}}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}if(i){const e=await modelSwitchTimingSpan(t?.attempt.operationId,"openclaw_sessions_list_pre_patch",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(openClawSessionKeysFromList(e).has(i))return{key:i,materialized:!1}}if(!(s&&t&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i??e}`)}const n=await this.liveSessionKeys.materializeMissingModelKey(e,t);if(i&&n!==i)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:n,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}function toOpenClawSessionModel(e){return e.startsWith(`${AROUTER_PROVIDER_ID}/`)?e:`${AROUTER_PROVIDER_ID}/${e}`}function toPlatformModelId(e){const t=`${AROUTER_PROVIDER_ID}/`;return e.startsWith(t)?e.slice(t.length):e}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o.model;return"string"==typeof r&&0!==r.length&&r.trim()===r&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}
1
+ import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{AROUTER_PROVIDER_ID}from"../media/arouter-config.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;constructor(e,t,i){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{sessionKey:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s="verbose"===t||"detailed"===t?{verboseLevel:"full"}:"reasoning"===t||"thinking"===t?{reasoningLevel:"stream"}:"concise"===t||"default"===t?{verboseLevel:"default"}:null;if(!s)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${t}`);const n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const r=toPlatformModelId(o),a=r.indexOf("/");return{modelId:r,...a>0?{modelProvider:r.slice(0,a)}:{}}}async setModel(e,t,i,s){const n=null===t?null:toOpenClawSessionModel(t),o=s?.attempt.operationId;let r=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s));const a=async()=>await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:r.key,model:n,mustExist:!r.materialized}));let l;try{l=await a()}catch(t){if(r.materialized||!isMissingOpenClawStoreSession(t,r.key))throw t;r=await modelSwitchTimingSpan(o,"materialize_set_model_key",async()=>await this.materializeSetModelKey(e,r.key,s)),l=await a()}const{key:c,materialized:d}=r;if(!isAuthoritativeSetModelAck(l,c,n,t))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const m=l.resolved.model,u=toPlatformModelId(m);if(null===t&&(!i||u!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(null!==t&&u!==t&&m!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");if(d){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(c))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(c)}return{...null===t?{}:{modelOverrideId:t},effectiveModelId:u,...l.resolved?.modelProvider?{modelProvider:l.resolved.modelProvider}:{}}}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}return i?{key:i,materialized:!1}:await this.materializeSetModelKey(e,void 0,t)}async materializeSetModelKey(e,t,i){if(!("service"===i?.attempt.origin&&void 0!==i.attempt.verifiedMessageGrant&&i&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${t??e}`)}const s=await this.liveSessionKeys.materializeMissingModelKey(e,i);if(t&&s!==t)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:s,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function isMissingOpenClawStoreSession(e,t){return e instanceof Error&&e.message===`No session found: ${t}`}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}function toOpenClawSessionModel(e){return e.startsWith(`${AROUTER_PROVIDER_ID}/`)?e:`${AROUTER_PROVIDER_ID}/${e}`}function toPlatformModelId(e){const t=`${AROUTER_PROVIDER_ID}/`;return e.startsWith(t)?e.slice(t.length):e}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o.model;return"string"==typeof r&&0!==r.length&&r.trim()===r&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}
@@ -9,6 +9,9 @@
9
9
  "contracts": {
10
10
  "tools": [
11
11
  "beeos_upload_file",
12
+ "beeos_desktop_screenshot",
13
+ "beeos_list_files",
14
+ "beeos_fetch_file",
12
15
  "render_ui",
13
16
  "close_ui",
14
17
  "render_form",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.6.50",
3
+ "version": "0.6.51-staging.1",
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>",
@@ -87,5 +87,5 @@
87
87
  "fast-uri": ">=3.1.4",
88
88
  "brace-expansion": ">=5.0.9"
89
89
  },
90
- "gitHead": "d9deb49b32657c1bfefc868b9d756fa1fd7f6eb4"
90
+ "gitHead": "07fd0d127b356bf29b0f514fadadfb1b630781a5"
91
91
  }