beeos-claw 0.5.65-staging.2 → 0.5.65-staging.3

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{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 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 o=setTimeout(()=>{this.emit("transport",{event:"connect_fail",reason:"timeout",durationMs:Date.now()-n}),s.close(),t(new Error("Gateway connection timeout"))},3e4),r=t=>{clearTimeout(o),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=>{clearTimeout(o),this.authFailed=!0,this.emit("transport",{event:"connect_fail",reason:e,durationMs:Date.now()-n}),this.emit("auth-failed",{reason:e}),s.close(),t(new Error(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 o=0;const r=setInterval(()=>{o++},200),c=Date.now(),a=setInterval(()=>{const e=Date.now()-c,t=e-200*o;t>500&&this.emit("transport",{event:"eventloop_lag",lagMs:t,elapsedMs:e,probeSeq:o})},200),h=()=>{clearInterval(r),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}),r(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;r(e??h)}else{const e=h.error;!0===e?.retryable||"UNAVAILABLE"===e?.code?(clearTimeout(o),this.emit("transport",{event:"connect_fail",reason:e?.message||"Gateway unavailable; will retry",retryable:!0,retryAfterMs:e?.retryAfterMs,durationMs:Date.now()-n}),"number"==typeof e?.retryAfterMs&&e.retryAfterMs>0&&(this.nextReconnectDelayHintMs=e.retryAfterMs),s.close(),t(new Error(e?.message||"Gateway unavailable"))):c(e?.message||"Gateway connect rejected")}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(o),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(o):(this.connected||(clearTimeout(o),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;null!==this.nextReconnectDelayHintMs&&this.nextReconnectDelayHintMs>0?(t=Math.min(this.nextReconnectDelayHintMs,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:this.reconnectAttempt,delayMs:t}),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.stopped||this.connect().catch(e=>{this.emit("error",e instanceof Error?e:new Error(String(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.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"],o={id:this.deviceIdentity.deviceId,publicKey:this.deviceIdentity.publicKeyB64url,signedAt:n};if(e){const t=Date.now(),r=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();o.nonce=e,o.signature=signDeviceAuth(this.deviceIdentity,r);const h=Date.now()-a;this.emit("transport",{event:"handshake.sign_done",buildPayloadMs:c,signMs:h})}const r=randomUUID(),c={type:"req",id:r,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:o}},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}),r}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,o)=>{const r=setTimeout(()=>{this.pending.delete(s),o(new Error(`Gateway request timeout: ${e}`))},n);this.pending.set(s,{resolve:t,reject:o,timer:r}),this.ws.send(JSON.stringify(i))})}async agentSend(e,t,n,s,i){const o={agentId:n,sessionKey:e,message:t,deliver:!1,idempotencyKey:randomUUID()};s?.length&&(o.attachments=s),i&&(o.inputProvenance=i);return await this.request("agent",o,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 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;null!==this.nextReconnectDelayHintMs&&this.nextReconnectDelayHintMs>0?(t=Math.min(this.nextReconnectDelayHintMs,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: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}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.65-staging.2",
3
+ "version": "0.5.65-staging.3",
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>",
@@ -28,6 +28,26 @@ export async function withinDeadline(run, deadline) {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * Start a Gateway exactly once within the worker's existing deadline.
33
+ *
34
+ * GatewayClient deliberately rejects its first `start()` call when OpenClaw
35
+ * answers the handshake with the typed startup-only UNAVAILABLE response. It
36
+ * concurrently schedules its own reconnect. The runtime worker must keep
37
+ * that client alive long enough to observe the resulting `connected` event;
38
+ * invoking start() again would create competing reconnect loops.
39
+ */
40
+ export async function startGatewayWithinDeadline(gateway, deadline) {
41
+ try {
42
+ await withinDeadline(() => gateway.start(), deadline);
43
+ return;
44
+ } catch (error) {
45
+ if (!isRetryableGatewayHandshake(error)) throw error;
46
+ }
47
+
48
+ await withinDeadline((signal) => waitForGatewayConnection(gateway, signal), deadline);
49
+ }
50
+
31
51
  export async function closeRuntimeResources({ consumer, gateway }, cleanupTimeoutMs = 5_000) {
32
52
  let cleanupError;
33
53
  try {
@@ -91,6 +111,50 @@ export function createRuntimeBlackboxCompletionGate() {
91
111
  };
92
112
  }
93
113
 
114
+ function isRetryableGatewayHandshake(error) {
115
+ return typeof error === "object" && error !== null &&
116
+ ((error.retryable === true) || error.code === "UNAVAILABLE");
117
+ }
118
+
119
+ function waitForGatewayConnection(gateway, signal) {
120
+ return new Promise((resolve, reject) => {
121
+ let settled = false;
122
+ const cleanup = () => {
123
+ gateway.off("connected", onConnected);
124
+ gateway.off("auth-failed", onAuthFailed);
125
+ gateway.off("error", onError);
126
+ signal.removeEventListener("abort", onAbort);
127
+ };
128
+ const settle = (callback, value) => {
129
+ if (settled) return;
130
+ settled = true;
131
+ cleanup();
132
+ callback(value);
133
+ };
134
+ const onConnected = () => settle(resolve);
135
+ const onAuthFailed = (failure) => {
136
+ const reason = failure && typeof failure.reason === "string"
137
+ ? failure.reason
138
+ : "Gateway authentication failed";
139
+ settle(reject, new Error(reason));
140
+ };
141
+ // GatewayClient emits `error` for each failed reconnect before scheduling
142
+ // the next one. Keep an error listener while waiting so EventEmitter does
143
+ // not crash the worker; auth-failed remains the immediate terminal signal.
144
+ const onError = () => {};
145
+ const onAbort = () => settle(reject, signal.reason ?? new Error("Gateway connection aborted"));
146
+
147
+ if (signal.aborted) {
148
+ onAbort();
149
+ return;
150
+ }
151
+ gateway.on("connected", onConnected);
152
+ gateway.on("auth-failed", onAuthFailed);
153
+ gateway.on("error", onError);
154
+ signal.addEventListener("abort", onAbort, { once: true });
155
+ });
156
+ }
157
+
94
158
  function workerTimeoutError() {
95
159
  return new Error(WORKER_TIMEOUT_MESSAGE);
96
160
  }
@@ -19,7 +19,7 @@ import { AgentGatewayGrantJwksReadinessAdapter, RuntimeServiceTransportReadiness
19
19
  "../dist/runtime/runtime-service-production-candidate.js";
20
20
  import { createRuntimeBlackboxReceipt } from "./runtime-security.mjs";
21
21
  import { runRuntimePoisonBlackbox } from "./runtime-poison-blackbox-lib.mjs";
22
- import { closeRuntimeResources, createRuntimeBlackboxCompletionGate, selectRuntimeError, withinDeadline } from
22
+ import { closeRuntimeResources, createRuntimeBlackboxCompletionGate, selectRuntimeError, startGatewayWithinDeadline, withinDeadline } from
23
23
  "./runtime-blackbox-worker-lifecycle.mjs";
24
24
 
25
25
  export async function main() {
@@ -108,7 +108,10 @@ export async function main() {
108
108
  }
109
109
 
110
110
  const gateway = new GatewayClient({ url: openclawUrl, token: openclawToken,
111
- clientId: `runtime-blackbox-${process.pid}`, retry: { baseMs: 100, maxMs: 500, maxAttempts: 1 } });
111
+ clientId: `runtime-blackbox-${process.pid}`,
112
+ // GatewayClient owns reconnect scheduling. Sixty attempts at this capped
113
+ // 500ms backoff remain bounded by the worker's existing 30s deadline.
114
+ retry: { baseMs: 100, maxMs: 500, maxAttempts: 60 } });
112
115
  let consumer;
113
116
  let primaryError;
114
117
  const runtimeLease = () => ({ instanceId, leaseId: nonempty(authority.leaseId, "authority.leaseId"),
@@ -117,7 +120,7 @@ export async function main() {
117
120
  runtimeLeaseCredential: nonempty(authority.runtimeLeaseCredential, "authority.runtimeLeaseCredential"),
118
121
  leaseExpiresAt: new Date(Number(leaseClaims.exp) * 1_000).toISOString() });
119
122
  try {
120
- await withinDeadline(() => gateway.start(), deadline);
123
+ await startGatewayWithinDeadline(gateway, deadline);
121
124
  const identities = new FileRuntimeAgentIdentityRegistry(`${statePrefix}-identities.json`);
122
125
  const dispatcher = new OpenClawRuntimeCommandDispatcher({ getGateway: () => gateway,
123
126
  resources: rejectingResources(), identities,