beeos-claw 0.6.8 → 0.6.10

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{TERMINAL_MAX_FRAME_BYTES}from"@beeos-ai/beeos-types/runtime";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5;export class RuntimeTerminalOutboundRegistration{options;service;authorityAdmission;serviceGeneration;rings=new Map;ringBytes=new Map;droppedThrough=new Map;pending=[];pendingBytes=0;activeSend;activeFlush;flushes=new Set;connected=!1;draining=!1;stopped=!1;eventGeneration=0;transportEpoch=0;drainPromise;closePromise;replayExpiryTimers=new Map;constructor(e){this.options=e,this.service=e.service??getOrCreateRuntimeTerminalService(e.manager),this.authorityAdmission=new TerminalAuthorityAdmission(e.authorityJournal,e.authorityClock),this.serviceGeneration=this.service.setOutbound(this);const t=this.eventGeneration;e.client.on("connected",()=>{this.acceptsEvent(t)&&(this.connected||(this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this),this.connected=!0,this.publishAvailability(!0),this.kickFlush(),e.logger?.info?.("[terminal-service] outbound registration connected")))}),e.client.on("disconnected",()=>{this.acceptsEvent(t)&&this.invalidateConnection(!1)}),e.client.on("message",(...e)=>{if(!this.acceptsEvent(t))return;const i=this.serviceGeneration,n=this.transportEpoch;if(!i||!this.acceptsTransport(n,i))return;const s=e[0],r="string"==typeof s?s:Buffer.from(s).toString("utf8");this.handleWire(r,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced"))}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch fenced"))})}async start(){if(this.stopped)throw new Error("terminal outbound registration is stopped");await this.options.client.connect()}isAvailable(){return this.connected&&!this.draining&&!this.stopped}send(e){if(this.stopped)return!1;const t=JSON.stringify(e),i=Buffer.byteLength(t);return this.remember(e,t,i),!this.connected||!(this.pendingBytes+i>(this.options.maxPendingSendBytes??1048576))&&(this.pending.push({clientId:e.clientId,terminalId:e.terminalId,wire:t,bytes:i}),this.pendingBytes+=i,this.kickFlush(),!0)}resume(e,t,i){const n=this.transportEpoch;if(!this.acceptsConnection(n))throw new Error("terminal outbound registration is disconnected");const s=BigInt(i),r=s<(this.droppedThrough.get(t)??0n),o=[];let a=0,h=-1n;for(const i of this.rings.get(t)??[]){if(i.frame.clientId!==e)throw new Error("terminal resume ownership mismatch");const n=BigInt(i.frame.sequence);if(n<=h)throw new Error("terminal replay sequence is not monotonic");h=n,n<=s||(o.push({clientId:e,terminalId:t,wire:i.wire,bytes:i.bytes}),a+=i.bytes)}if(this.pendingBytes+a>(this.options.maxPendingSendBytes??1048576))throw new Error("terminal resume backpressure limit");if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");return this.pending.push(...o),this.pendingBytes+=a,this.acknowledgeReplay(t,s),this.kickFlush(),{gap:r}}forgetTerminal(e,t){this.prepareForgetTerminal(e,t)()}prepareForgetTerminal(e,t){const i=this.rings.get(t),n=new Set(this.pending.filter(e=>e.terminalId===t)),s=this.activeSend?.item.terminalId===t&&this.activeSend.counted?this.activeSend:void 0;if(i?.some(t=>t.frame.clientId!==e)||[...n].some(t=>t.clientId!==e)||s&&s.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const r=this.replayExpiryTimers.get(t);let o=!1;return()=>{if(o)return;o=!0,r&&this.replayExpiryTimers.get(t)===r&&(clearTimeout(r),this.replayExpiryTimers.delete(t)),i&&this.rings.get(t)===i&&(this.rings.delete(t),this.ringBytes.delete(t),this.droppedThrough.delete(t));let e=0;for(const t of this.pending)n.has(t)?this.pendingBytes-=t.bytes:this.pending[e++]=t;this.pending.length=e,s&&this.activeSend===s&&s.counted&&(s.counted=!1,this.pendingBytes-=s.item.bytes),this.pendingBytes=Math.max(0,this.pendingBytes)}}startTerminalReplayExpiry(e,t){if((this.rings.get(t)??[]).some(t=>t.frame.clientId!==e))throw new Error("terminal replay expiry ownership mismatch");this.scheduleReplayExpiry(e,t)}drain(e){return this.drainPromise??=this.drainOnce(e),this.drainPromise}async drainOnce(e){this.draining=!0,this.stopped=!0,this.eventGeneration++;const t=[...this.flushes];this.invalidateConnection(!0);const i=await waitBounded(this.options.manager.drain(e.timeoutMs,e.terminateAfterTimeout),e.timeoutMs,e.signal),n=i===interrupted?"terminated":i;return i!==interrupted&&"terminated"===n&&e.terminateAfterTimeout&&await waitBounded(this.options.manager.drain(Math.min(1e3,Math.max(50,e.timeoutMs)),!1),Math.min(1e3,Math.max(50,e.timeoutMs)),e.signal),await waitBounded(Promise.allSettled(t),e.timeoutMs,e.signal),await waitBounded(this.closeClientOnce(),e.timeoutMs,e.signal),this.clearReplayState(),n}acceptsEvent(e){return!this.stopped&&e===this.eventGeneration}async handleWire(e,t,i){if(!this.acceptsTransport(i,t))return;if(Buffer.byteLength(e)>TERMINAL_MAX_FRAME_BYTES)return void this.options.logger?.warn?.("[terminal-service] oversized client frame rejected");let n;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{const e=await this.authorityAdmission.admit(n);await this.service.handleClientFrame(e.frame,t,e.admission)}catch(e){this.acceptsTransport(i,t)&&this.options.logger?.warn?.("[terminal-service] client frame rejected",e)}}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let s=(this.ringBytes.get(e.terminalId)??0)+i;const r=this.options.maxBufferedFramesPerTerminal??512,o=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>r||s>o;){const t=n.shift();if(!t)break;s-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,s),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}async flush(e){for(;this.acceptsConnection(e)&&this.pending.length>0;){const t=this.pending.shift(),i={item:t,counted:!0};this.activeSend=i;try{await this.options.client.send(t.wire)}catch{return void(this.transportEpoch===e&&this.invalidateConnection(!1))}if(!this.acceptsConnection(e))return;this.activeSend===i&&(i.counted&&(this.pendingBytes-=t.bytes),this.activeSend=void 0)}}kickFlush(){if(this.activeFlush||!this.connected||0===this.pending.length)return;const e=this.transportEpoch,t=this.flush(e).finally(()=>{this.flushes.delete(t),this.activeFlush?.promise===t&&(this.activeFlush=void 0,this.acceptsConnection(e)&&this.kickFlush())});this.activeFlush={epoch:e,promise:t},this.flushes.add(t)}acceptsConnection(e){return this.connected&&!this.stopped&&e===this.transportEpoch}acceptsTransport(e,t){return this.acceptsEvent(this.eventGeneration)&&this.acceptsConnection(e)&&this.serviceGeneration===t}invalidateConnection(e){const t=this.connected;if(this.transportEpoch++,this.connected=!1,t&&this.publishAvailability(!1),this.activeFlush=void 0,this.activeSend=void 0,this.flushes.clear(),this.pending.length=0,this.pendingBytes=0,!e)return;const i=this.serviceGeneration;i&&(this.service.fenceServiceSessions("runtime_lease_lost",i),this.service.clearOutbound(this,i),this.serviceGeneration===i&&(this.serviceGeneration=void 0)),this.clearReplayState()}publishAvailability(e){try{this.options.onAvailabilityChange?.(e)}catch{}}clearReplayState(){for(const e of this.replayExpiryTimers.values())clearTimeout(e);this.replayExpiryTimers.clear(),this.rings.clear(),this.ringBytes.clear(),this.droppedThrough.clear()}acknowledgeReplay(e,t){const i=this.rings.get(e);if(!i||0===i.length)return;let n=0,s=this.droppedThrough.get(e)??0n,r=0;for(;r<i.length;){const e=i[r],o=BigInt(e.frame.sequence);if(o>t)break;n+=e.bytes,o>s&&(s=o),r++}0!==r&&(i.splice(0,r),this.ringBytes.set(e,Math.max(0,(this.ringBytes.get(e)??0)-n)),this.droppedThrough.set(e,s))}scheduleReplayExpiry(e,t){this.clearReplayExpiry(t);const i=this.serviceGeneration;if(!i)return;const n=setTimeout(()=>{if(this.replayExpiryTimers.get(t)===n){this.replayExpiryTimers.delete(t);try{this.service.expireServiceTerminalReplay(e,t,i)}catch(e){this.options.logger?.warn?.("[terminal-service] replay expiry failed",e)}}},Math.max(0,this.options.replayRetentionMs??3e5));n.unref?.(),this.replayExpiryTimers.set(t,n)}clearReplayExpiry(e){const t=this.replayExpiryTimers.get(e);t&&clearTimeout(t),this.replayExpiryTimers.delete(e)}closeClientOnce(){return this.closePromise??=this.options.client.close(!0),this.closePromise}}const interrupted=Symbol("interrupted");async function waitBounded(e,t,i){return i?.aborted?(e.catch(()=>{}),interrupted):await new Promise((n,s)=>{let r=!1;const o=e=>{r||(r=!0,clearTimeout(h),i?.removeEventListener("abort",a),n(e))},a=()=>o(interrupted),h=setTimeout(()=>o(interrupted),Math.max(0,t));h.unref?.(),i?.addEventListener("abort",a,{once:!0}),e.then(e=>o(e),e=>{r||(r=!0,clearTimeout(h),i?.removeEventListener("abort",a),s(e))})})}
1
+ import{TERMINAL_MAX_FRAME_BYTES}from"@beeos-ai/beeos-types/runtime";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5;export class RuntimeTerminalOutboundRegistration{options;service;authorityAdmission;serviceGeneration;rings=new Map;ringBytes=new Map;droppedThrough=new Map;pending=[];pendingBytes=0;activeSend;activeFlush;flushes=new Set;connected=!1;draining=!1;stopped=!1;eventGeneration=0;transportEpoch=0;drainPromise;closePromise;replayExpiryTimers=new Map;constructor(e){this.options=e,this.service=e.service??getOrCreateRuntimeTerminalService(e.manager),this.authorityAdmission=new TerminalAuthorityAdmission(e.authorityJournal,e.authorityClock),this.serviceGeneration=this.service.setOutbound(this);const t=this.eventGeneration;e.client.on("connected",()=>{this.acceptsEvent(t)&&(this.connected||(this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this),this.connected=!0,this.publishAvailability(!0),this.kickFlush(),e.logger?.info?.("[terminal-service] outbound registration connected")))}),e.client.on("disconnected",()=>{this.acceptsEvent(t)&&this.invalidateConnection(!1)}),e.client.on("message",(...e)=>{if(!this.acceptsEvent(t))return;const i=this.serviceGeneration,n=this.transportEpoch;if(!i||!this.acceptsTransport(n,i))return;const s=e[0],r="string"==typeof s?s:Buffer.from(s).toString("utf8");this.handleWire(r,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced"))}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection(!0),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch fenced"))})}async start(){if(this.stopped)throw new Error("terminal outbound registration is stopped");await this.options.client.connect()}isAvailable(){return this.connected&&!this.draining&&!this.stopped}send(e){if(this.stopped)return!1;const t=JSON.stringify(e),i=Buffer.byteLength(t);return this.remember(e,t,i),!this.connected||!(this.pendingBytes+i>(this.options.maxPendingSendBytes??1048576))&&(this.pending.push({clientId:e.clientId,terminalId:e.terminalId,wire:t,bytes:i}),this.pendingBytes+=i,this.kickFlush(),!0)}resume(e,t,i){const n=this.transportEpoch;if(!this.acceptsConnection(n))throw new Error("terminal outbound registration is disconnected");const s=BigInt(i),r=s<(this.droppedThrough.get(t)??0n),o=[];let a=0,h=-1n;for(const i of this.rings.get(t)??[]){if(i.frame.clientId!==e)throw new Error("terminal resume ownership mismatch");const n=BigInt(i.frame.sequence);if(n<=h)throw new Error("terminal replay sequence is not monotonic");h=n,n<=s||(o.push({clientId:e,terminalId:t,wire:i.wire,bytes:i.bytes}),a+=i.bytes)}if(this.pendingBytes+a>(this.options.maxPendingSendBytes??1048576))throw new Error("terminal resume backpressure limit");if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");return this.pending.push(...o),this.pendingBytes+=a,this.acknowledgeReplay(t,s),this.kickFlush(),{gap:r}}rebindResumeClient(e,t,i){if(e===t)return;const n=this.rings.get(i);if(n?.some(t=>t.frame.clientId!==e))throw new Error("terminal resume ownership mismatch");if(n){let e=0;const s=n.map(i=>{const n=Object.freeze({...i.frame,clientId:t}),s=JSON.stringify(n),r=Buffer.byteLength(s);return e+=r,{frame:n,wire:s,bytes:r}});this.rings.set(i,s),this.ringBytes.set(i,e)}for(let n=0;n<this.pending.length;n++){const s=this.pending[n];if(s.terminalId!==i||s.clientId!==e)continue;const r={...JSON.parse(s.wire),clientId:t},o=JSON.stringify(r),a=Buffer.byteLength(o);this.pendingBytes+=a-s.bytes,this.pending[n]={clientId:t,terminalId:i,wire:o,bytes:a}}if(this.activeSend?.item.terminalId===i&&this.activeSend.item.clientId===e){const e=this.activeSend.item,n={...JSON.parse(e.wire),clientId:t},s=JSON.stringify(n),r=Buffer.byteLength(s);this.activeSend.counted&&(this.pendingBytes+=r-e.bytes),this.activeSend={item:{clientId:t,terminalId:i,wire:s,bytes:r},counted:this.activeSend.counted}}}forgetTerminal(e,t){this.prepareForgetTerminal(e,t)()}prepareForgetTerminal(e,t){const i=this.rings.get(t),n=new Set(this.pending.filter(e=>e.terminalId===t)),s=this.activeSend?.item.terminalId===t&&this.activeSend.counted?this.activeSend:void 0;if(i?.some(t=>t.frame.clientId!==e)||[...n].some(t=>t.clientId!==e)||s&&s.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const r=this.replayExpiryTimers.get(t);let o=!1;return()=>{if(o)return;o=!0,r&&this.replayExpiryTimers.get(t)===r&&(clearTimeout(r),this.replayExpiryTimers.delete(t)),i&&this.rings.get(t)===i&&(this.rings.delete(t),this.ringBytes.delete(t),this.droppedThrough.delete(t));let e=0;for(const t of this.pending)n.has(t)?this.pendingBytes-=t.bytes:this.pending[e++]=t;this.pending.length=e,s&&this.activeSend===s&&s.counted&&(s.counted=!1,this.pendingBytes-=s.item.bytes),this.pendingBytes=Math.max(0,this.pendingBytes)}}startTerminalReplayExpiry(e,t){if((this.rings.get(t)??[]).some(t=>t.frame.clientId!==e))throw new Error("terminal replay expiry ownership mismatch");this.scheduleReplayExpiry(e,t)}drain(e){return this.drainPromise??=this.drainOnce(e),this.drainPromise}async drainOnce(e){this.draining=!0,this.stopped=!0,this.eventGeneration++;const t=[...this.flushes];this.invalidateConnection(!0);const i=await waitBounded(this.options.manager.drain(e.timeoutMs,e.terminateAfterTimeout),e.timeoutMs,e.signal),n=i===interrupted?"terminated":i;return i!==interrupted&&"terminated"===n&&e.terminateAfterTimeout&&await waitBounded(this.options.manager.drain(Math.min(1e3,Math.max(50,e.timeoutMs)),!1),Math.min(1e3,Math.max(50,e.timeoutMs)),e.signal),await waitBounded(Promise.allSettled(t),e.timeoutMs,e.signal),await waitBounded(this.closeClientOnce(),e.timeoutMs,e.signal),this.clearReplayState(),n}acceptsEvent(e){return!this.stopped&&e===this.eventGeneration}async handleWire(e,t,i){if(!this.acceptsTransport(i,t))return;if(Buffer.byteLength(e)>TERMINAL_MAX_FRAME_BYTES)return void this.options.logger?.warn?.("[terminal-service] oversized client frame rejected");let n;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{const e=await this.authorityAdmission.admit(n);await this.service.handleClientFrame(e.frame,t,e.admission)}catch(e){if(this.acceptsTransport(i,t)){const t=e instanceof Error?e.message:String(e);this.options.logger?.warn?.(`[terminal-service] client frame rejected: ${t}`,e)}}}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let s=(this.ringBytes.get(e.terminalId)??0)+i;const r=this.options.maxBufferedFramesPerTerminal??512,o=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>r||s>o;){const t=n.shift();if(!t)break;s-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,s),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}async flush(e){for(;this.acceptsConnection(e)&&this.pending.length>0;){const t=this.pending.shift(),i={item:t,counted:!0};this.activeSend=i;try{await this.options.client.send(t.wire)}catch{return void(this.transportEpoch===e&&this.invalidateConnection(!1))}if(!this.acceptsConnection(e))return;this.activeSend===i&&(i.counted&&(this.pendingBytes-=t.bytes),this.activeSend=void 0)}}kickFlush(){if(this.activeFlush||!this.connected||0===this.pending.length)return;const e=this.transportEpoch,t=this.flush(e).finally(()=>{this.flushes.delete(t),this.activeFlush?.promise===t&&(this.activeFlush=void 0,this.acceptsConnection(e)&&this.kickFlush())});this.activeFlush={epoch:e,promise:t},this.flushes.add(t)}acceptsConnection(e){return this.connected&&!this.stopped&&e===this.transportEpoch}acceptsTransport(e,t){return this.acceptsEvent(this.eventGeneration)&&this.acceptsConnection(e)&&this.serviceGeneration===t}invalidateConnection(e){const t=this.connected;if(this.transportEpoch++,this.connected=!1,t&&this.publishAvailability(!1),this.activeFlush=void 0,this.activeSend=void 0,this.flushes.clear(),this.pending.length=0,this.pendingBytes=0,!e)return;const i=this.serviceGeneration;i&&(this.service.fenceServiceSessions("runtime_lease_lost",i),this.service.clearOutbound(this,i),this.serviceGeneration===i&&(this.serviceGeneration=void 0)),this.clearReplayState()}publishAvailability(e){try{this.options.onAvailabilityChange?.(e)}catch{}}clearReplayState(){for(const e of this.replayExpiryTimers.values())clearTimeout(e);this.replayExpiryTimers.clear(),this.rings.clear(),this.ringBytes.clear(),this.droppedThrough.clear()}acknowledgeReplay(e,t){const i=this.rings.get(e);if(!i||0===i.length)return;let n=0,s=this.droppedThrough.get(e)??0n,r=0;for(;r<i.length;){const e=i[r],o=BigInt(e.frame.sequence);if(o>t)break;n+=e.bytes,o>s&&(s=o),r++}0!==r&&(i.splice(0,r),this.ringBytes.set(e,Math.max(0,(this.ringBytes.get(e)??0)-n)),this.droppedThrough.set(e,s))}scheduleReplayExpiry(e,t){this.clearReplayExpiry(t);const i=this.serviceGeneration;if(!i)return;const n=setTimeout(()=>{if(this.replayExpiryTimers.get(t)===n){this.replayExpiryTimers.delete(t);try{this.service.expireServiceTerminalReplay(e,t,i)}catch(e){this.options.logger?.warn?.("[terminal-service] replay expiry failed",e)}}},Math.max(0,this.options.replayRetentionMs??3e5));n.unref?.(),this.replayExpiryTimers.set(t,n)}clearReplayExpiry(e){const t=this.replayExpiryTimers.get(e);t&&clearTimeout(t),this.replayExpiryTimers.delete(e)}closeClientOnce(){return this.closePromise??=this.options.client.close(!0),this.closePromise}}const interrupted=Symbol("interrupted");async function waitBounded(e,t,i){return i?.aborted?(e.catch(()=>{}),interrupted):await new Promise((n,s)=>{let r=!1;const o=e=>{r||(r=!0,clearTimeout(h),i?.removeEventListener("abort",a),n(e))},a=()=>o(interrupted),h=setTimeout(()=>o(interrupted),Math.max(0,t));h.unref?.(),i?.addEventListener("abort",a,{once:!0}),e.then(e=>o(e),e=>{r||(r=!0,clearTimeout(h),i?.removeEventListener("abort",a),s(e))})})}
@@ -1 +1 @@
1
- import{TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";export class RuntimeTerminalService{manager;clients=new Map;clientSequences=new Map;openingFrames=new Map;outboundGeneration;constructor(e,n){this.manager=e,n&&this.setOutbound(n),e.addListeners({onOutput:e=>{const n=this.clients.get(e.terminalId);"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),stream:e.stream,data:e.data.toString("base64"),encoding:"base64"},n)},onExit:e=>{const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)===n&&(this.clients.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,n.incarnation))},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.exit",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),...null===e.code?{}:{exitCode:e.code},...null===e.signal?{}:{signal:e.signal}},n)},onStateChange:e=>{if("closing"!==e.to)return;const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)!==n||this.manager.isCurrentSession(e.terminalId,e.incarnation)||this.clients.delete(e.terminalId)},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.isOwnedTerminal(n)&&n.generation.outbound.startTerminalReplayExpiry(n.clientId,e.terminalId)}})}setOutbound(e){if(this.outboundGeneration?.outbound===e)return this.outboundGeneration;if(this.outboundGeneration)throw new Error("Terminal outbound sink is already configured");const n=Object.freeze({outbound:e});return this.outboundGeneration=n,n}clearOutbound(e,n){const t=n??this.outboundGeneration;t&&t.outbound===e&&this.outboundGeneration===t&&(this.outboundGeneration=void 0,this.clearGenerationOwnership(t))}fenceServiceSessions(e="runtime_lease_lost",n=this.outboundGeneration){for(const[t,i]of this.clients)"service"!==i.kind||n&&i.generation!==n||this.manager.closeSessionIfCurrent(t,i.incarnation,e);if(n)this.clearGenerationOwnership(n);else{for(const[e,n]of this.clients)"service"===n.kind&&this.clients.delete(e);this.clientSequences.clear();for(const[e,n]of this.openingFrames)"service"===n.owner.kind&&this.openingFrames.delete(e)}}async handleClientFrame(e,n=this.outboundGeneration,t){if(!n||!this.isGenerationActive(n))throw new Error("terminal outbound generation is inactive");assertTerminalClientFrame(e);const i=BigInt(e.clientSequence),r=this.clientSequences.get(e.clientId);if(i<=(r?.generation===n?r.sequence:0n))throw new Error("non-monotonic terminal client sequence");if(this.clientSequences.set(e.clientId,{generation:n,sequence:i}),"terminal.open"===e.type||"terminal.create"===e.type){if(this.clients.has(e.terminalId)||this.manager.getSession(e.terminalId)){const i=this.clients.get(e.terminalId);if(t?.recovered&&"service"===i?.kind&&i.clientId===e.clientId&&i.generation===n&&this.isOwnedTerminal(i)){const n=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(i),resumed:!0,columns:n.cols,rows:n.rows},i)}throw new Error("terminal already exists; use terminal.resume")}const i=this.manager.createSessionIncarnation(e.terminalId),r={kind:"service",clientId:e.clientId,generation:n,incarnation:i};this.clients.set(e.terminalId,r),this.openingFrames.set(e.terminalId,{owner:r,frames:[]});try{const n=this.manager.openSession({terminalId:e.terminalId,sessionId:e.clientId,command:e.command,args:e.args,env:e.env,cwd:e.cwd,cols:"terminal.open"===e.type?e.columns??80:80,rows:"terminal.open"===e.type?e.rows??24:24,outputLimitBytes:e.outputLimitBytes,initialServerSequence:2,incarnation:i});if(!this.isOwnedTerminal(r))return void this.manager.releaseSessionIfCurrent(e.terminalId,i);this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:n.terminalId,sequence:"1",resumed:!0===t?.recovered,columns:n.cols,rows:n.rows},r);const a=this.openingFrames.get(e.terminalId);a?.owner===r&&this.openingFrames.delete(e.terminalId);for(const e of a?.owner===r?a.frames:[])this.sendOrClose(e,r)}catch(n){throw this.clients.get(e.terminalId)===r&&this.clients.delete(e.terminalId),this.openingFrames.get(e.terminalId)?.owner===r&&this.openingFrames.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,i),n}return}const a=this.assertOwner(e.clientId,e.terminalId,n);switch(e.type){case"terminal.resume":{const t=n.outbound.resume(e.clientId,e.terminalId,e.lastServerSequence);if(!this.isOwnedTerminal(a))return;if(t.gap&&this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),stream:"stdout",data:"",encoding:"base64",truncated:!0},a),!this.isOwnedTerminal(a))return;const i=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),resumed:!0,columns:i.cols,rows:i.rows},a)}case"terminal.input":return void this.manager.writeToSessionIfCurrent(e.terminalId,a.incarnation,Buffer.from(e.data,"base64"===e.encoding?"base64":"utf8"));case"terminal.resize":return void this.manager.resizeSessionIfCurrent(e.terminalId,a.incarnation,e.columns,e.rows);case"terminal.close":if(!this.manager.closeSessionIfCurrent(e.terminalId,a.incarnation,e.reason))throw new TerminalNotFoundError(e.terminalId);return;case"terminal.ping":if(this.manager.touchSessionIfCurrent(e.terminalId,a.incarnation),!this.isOwnedTerminal(a))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.pong",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),nonce:e.nonce},a);case"terminal.wait":{let n;try{n=await this.manager.waitForExitIfCurrent(e.terminalId,a.incarnation,e.timeoutMs)}catch(e){if(!this.isOwnedTerminal(a))return;throw e}if(!this.isOwnedTerminal(a))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.wait_result",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(a),exitCode:n.exitCode,output:n.output,state:n.state},a)}case"terminal.release":if(!this.manager.isCurrentSession(e.terminalId,a.incarnation))throw new TerminalNotFoundError(e.terminalId);const t=n.outbound.prepareForgetTerminal(e.clientId,e.terminalId);if(!this.manager.detachSessionIfCurrent(e.terminalId,a.incarnation))throw new TerminalNotFoundError(e.terminalId);return this.clients.get(e.terminalId)===a&&this.clients.delete(e.terminalId),void t();case"terminal.kill":if(!this.manager.killSessionIfCurrent(e.terminalId,a.incarnation,normalizeSignal(e.signal)))throw new TerminalNotFoundError(e.terminalId);return}}async execute(e,n){const t=this.outboundGeneration,i=t?.outbound;if(i?.isAvailable&&!i.isAvailable())throw new Error("terminal Service transport is unavailable; legacy fallback is forbidden");const r=e,a=r.context.terminal.terminalId,s=r.context.terminal.clientId;switch(r.name){case"terminal.open":{const e=r,n=this.manager.createSessionIncarnation(a),t=this.manager.openSession({terminalId:a,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:e.params.columns,rows:e.params.rows,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command??t.shell,shell:t.shell,cwd:t.cwd,columns:t.cols,rows:t.rows,state:t.state}}case"terminal.input":{const e=r,n=this.assertAcpOwner(s,a);return this.manager.writeToSessionIfCurrent(a,n.incarnation,Buffer.from(e.params.data,"base64"===e.params.encoding?"base64":"utf8")),{accepted:!0}}case"terminal.resize":{const e=r,n=this.assertAcpOwner(s,a);return this.manager.resizeSessionIfCurrent(a,n.incarnation,e.params.columns,e.params.rows),{resized:!0}}case"terminal.close":{const e=this.assertAcpOwner(s,a);if(!this.manager.closeSessionIfCurrent(a,e.incarnation,r.params.reason))throw new TerminalNotFoundError(a);return{closed:!0}}case"terminal.ping":{const e=this.assertAcpOwner(s,a);return this.manager.touchSessionIfCurrent(a,e.incarnation),{terminalId:a,status:"ok"}}case"terminal.create":{const e=r,n=this.manager.createSessionIncarnation(a),t=this.manager.openSession({terminalId:a,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:80,rows:24,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command}}case"terminal.wait":{const e=r,n=this.assertAcpOwner(s,a),t=await this.manager.waitForExitIfCurrent(a,n.incarnation,e.params.timeoutMs);if(!this.isOwnedAcpTerminal(n))throw new TerminalNotFoundError(a);return t}case"terminal.release":{const e=this.assertAcpOwner(s,a);if(!this.manager.detachSessionIfCurrent(a,e.incarnation))throw new TerminalNotFoundError(a);return this.clients.get(a)===e&&this.clients.delete(a),{released:!0}}case"terminal.kill":{const e=this.assertAcpOwner(s,a);if(!this.manager.killSessionIfCurrent(a,e.incarnation,normalizeSignal(r.params.signal)))throw new TerminalNotFoundError(a);return{killed:!0}}}throw new Error(`unsupported terminal command ${r.name}`)}nextSequence(e){return String(this.manager.nextServerSequence(e.incarnation.terminalId,e.incarnation))}sendOrClose(e,n){this.isOwnedTerminal(n)&&!n.generation.outbound.send(e)&&this.isOwnedTerminal(n)&&this.manager.closeSessionIfCurrent(e.terminalId,n.incarnation,"backpressure_limit")}emitFrame(e,n){if(!this.isOwnedTerminal(n))return;const t=this.openingFrames.get(e.terminalId);t?.owner===n?t.frames.push(e):this.sendOrClose(e,n)}assertOwner(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))throw new TerminalNotFoundError(n);return i}assertAcpOwner(e,n){const t=this.clients.get(n);if("acp"!==t?.kind||t.clientId!==e||!this.isOwnedAcpTerminal(t))throw new TerminalNotFoundError(n);return t}isGenerationActive(e){return this.outboundGeneration===e}isOwnedTerminal(e){return this.outboundGeneration===e.generation&&this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}isOwnedAcpTerminal(e){return this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}expireServiceTerminalReplay(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))return;const r=t.outbound.prepareForgetTerminal(e,n);if(!("closed"===this.manager.getSession(n)?.state?this.manager.releaseSessionIfCurrent(n,i.incarnation):this.manager.detachSessionIfCurrent(n,i.incarnation)))throw new TerminalNotFoundError(n);this.clients.get(n)===i&&this.clients.delete(n),this.openingFrames.get(n)?.owner===i&&this.openingFrames.delete(n),r()}clearGenerationOwnership(e){for(const[n,t]of this.clients)"service"===t.kind&&t.generation===e&&this.clients.delete(n);for(const[n,t]of this.clientSequences)t.generation===e&&this.clientSequences.delete(n);for(const[n,t]of this.openingFrames)t.owner.generation===e&&this.openingFrames.delete(n)}}const runtimeServices=new WeakMap;export function getOrCreateRuntimeTerminalService(e){let n=runtimeServices.get(e);return n||(n=new RuntimeTerminalService(e),runtimeServices.set(e,n)),n}export function assertTerminalClientFrame(e){const n=e;if(!(n&&CLIENT_FRAME_TYPES.has(String(n.type))&&n.protocolVersion===TERMINAL_SERVICE_PROTOCOL_VERSION&&isUuid(n.clientId)&&isUuid(n.terminalId)&&isPositiveRuntimeSequence(n.clientSequence)))throw new Error("invalid terminal client frame");switch(e.type){case"terminal.open":assertOptionalCommand(e.command),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalDimension(e.columns),assertOptionalDimension(e.rows),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.create":("string"!=typeof e.command||e.command.length<1||e.command.length>4096)&&invalidFrame(),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.resume":isRuntimeSequence(e.lastServerSequence)||invalidFrame();break;case"terminal.input":("string"!=typeof e.data||"utf8"!==e.encoding&&"base64"!==e.encoding)&&invalidFrame();break;case"terminal.resize":assertDimension(e.columns),assertDimension(e.rows);break;case"terminal.close":assertOptionalString(e.reason);break;case"terminal.ping":"string"!=typeof e.nonce&&invalidFrame();break;case"terminal.wait":void 0!==e.timeoutMs&&(!Number.isInteger(e.timeoutMs)||e.timeoutMs<0||e.timeoutMs>864e5)&&invalidFrame();break;case"terminal.kill":assertOptionalString(e.signal)}}const CLIENT_FRAME_TYPES=new Set(["terminal.open","terminal.resume","terminal.input","terminal.resize","terminal.close","terminal.ping","terminal.create","terminal.wait","terminal.release","terminal.kill"]);function normalizeSignal(e){return e&&e.startsWith("SIG")?e:"SIGTERM"}const UUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,UINT64_MAX=18446744073709551615n;function isUuid(e){return"string"==typeof e&&UUID_PATTERN.test(e)}function isRuntimeSequence(e){return"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)<=UINT64_MAX}function isPositiveRuntimeSequence(e){return isRuntimeSequence(e)&&"0"!==e}function invalidFrame(){throw new Error("invalid terminal client frame")}function assertOptionalString(e){void 0!==e&&"string"!=typeof e&&invalidFrame()}function assertOptionalCommand(e){void 0!==e&&("string"!=typeof e||e.length<1||e.length>4096)&&invalidFrame()}function assertOptionalArgs(e){void 0===e||Array.isArray(e)&&e.every(e=>"string"==typeof e)||invalidFrame()}function assertOptionalEnv(e){void 0===e||e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>"string"==typeof e)||invalidFrame()}function assertDimension(e){(!Number.isInteger(e)||e<1||e>1e3)&&invalidFrame()}function assertOptionalDimension(e){void 0!==e&&assertDimension(e)}function assertOptionalOutputLimit(e){void 0!==e&&(!Number.isInteger(e)||e<1||e>16777216)&&invalidFrame()}
1
+ import{TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";export class RuntimeTerminalService{manager;clients=new Map;clientSequences=new Map;openingFrames=new Map;outboundGeneration;constructor(e,n){this.manager=e,n&&this.setOutbound(n),e.addListeners({onOutput:e=>{const n=this.clients.get(e.terminalId);"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),stream:e.stream,data:e.data.toString("base64"),encoding:"base64"},n)},onExit:e=>{const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)===n&&(this.clients.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,n.incarnation))},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.manager.isCurrentSession(e.terminalId,n.incarnation)&&this.emitFrame({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.exit",clientId:n.clientId,terminalId:e.terminalId,sequence:String(e.seq),...null===e.code?{}:{exitCode:e.code},...null===e.signal?{}:{signal:e.signal}},n)},onStateChange:e=>{if("closing"!==e.to)return;const n=this.clients.get(e.terminalId);if("acp"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation){const t=setTimeout(()=>{this.clients.get(e.terminalId)!==n||this.manager.isCurrentSession(e.terminalId,e.incarnation)||this.clients.delete(e.terminalId)},0);return void t.unref?.()}"service"===n?.kind&&n.clientId===e.sessionId&&n.incarnation===e.incarnation&&this.isOwnedTerminal(n)&&n.generation.outbound.startTerminalReplayExpiry(n.clientId,e.terminalId)}})}setOutbound(e){if(this.outboundGeneration?.outbound===e)return this.outboundGeneration;if(this.outboundGeneration)throw new Error("Terminal outbound sink is already configured");const n=Object.freeze({outbound:e});return this.outboundGeneration=n,n}clearOutbound(e,n){const t=n??this.outboundGeneration;t&&t.outbound===e&&this.outboundGeneration===t&&(this.outboundGeneration=void 0,this.clearGenerationOwnership(t))}fenceServiceSessions(e="runtime_lease_lost",n=this.outboundGeneration){for(const[t,i]of this.clients)"service"!==i.kind||n&&i.generation!==n||this.manager.closeSessionIfCurrent(t,i.incarnation,e);if(n)this.clearGenerationOwnership(n);else{for(const[e,n]of this.clients)"service"===n.kind&&this.clients.delete(e);this.clientSequences.clear();for(const[e,n]of this.openingFrames)"service"===n.owner.kind&&this.openingFrames.delete(e)}}async handleClientFrame(e,n=this.outboundGeneration,t){if(!n||!this.isGenerationActive(n))throw new Error("terminal outbound generation is inactive");assertTerminalClientFrame(e);const i=BigInt(e.clientSequence),r=this.clientSequences.get(e.clientId);if(i<=(r?.generation===n?r.sequence:0n))throw new Error("non-monotonic terminal client sequence");if(this.clientSequences.set(e.clientId,{generation:n,sequence:i}),"terminal.open"===e.type||"terminal.create"===e.type){if(this.clients.has(e.terminalId)||this.manager.getSession(e.terminalId)){const i=this.clients.get(e.terminalId);if(t?.recovered&&"service"===i?.kind&&i.clientId===e.clientId&&i.generation===n&&this.isOwnedTerminal(i)){const n=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(i),resumed:!0,columns:n.cols,rows:n.rows},i)}throw new Error("terminal already exists; use terminal.resume")}const i=this.manager.createSessionIncarnation(e.terminalId),r={kind:"service",clientId:e.clientId,generation:n,incarnation:i};this.clients.set(e.terminalId,r),this.openingFrames.set(e.terminalId,{owner:r,frames:[]});try{const n=this.manager.openSession({terminalId:e.terminalId,sessionId:e.clientId,command:e.command,args:e.args,env:e.env,cwd:e.cwd,cols:"terminal.open"===e.type?e.columns??80:80,rows:"terminal.open"===e.type?e.rows??24:24,outputLimitBytes:e.outputLimitBytes,initialServerSequence:2,incarnation:i});if(!this.isOwnedTerminal(r))return void this.manager.releaseSessionIfCurrent(e.terminalId,i);this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:n.terminalId,sequence:"1",resumed:!0===t?.recovered,columns:n.cols,rows:n.rows},r);const s=this.openingFrames.get(e.terminalId);s?.owner===r&&this.openingFrames.delete(e.terminalId);for(const e of s?.owner===r?s.frames:[])this.sendOrClose(e,r)}catch(n){throw this.clients.get(e.terminalId)===r&&this.clients.delete(e.terminalId),this.openingFrames.get(e.terminalId)?.owner===r&&this.openingFrames.delete(e.terminalId),this.manager.releaseSessionIfCurrent(e.terminalId,i),n}return}"terminal.resume"===e.type&&this.rebindServiceOwnerForResume(e.clientId,e.terminalId,n);const s=this.assertOwner(e.clientId,e.terminalId,n);switch(e.type){case"terminal.resume":{const t=n.outbound.resume(e.clientId,e.terminalId,e.lastServerSequence);if(!this.isOwnedTerminal(s))return;if(t.gap&&this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.output",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),stream:"stdout",data:"",encoding:"base64",truncated:!0},s),!this.isOwnedTerminal(s))return;const i=this.manager.getSession(e.terminalId);return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.opened",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),resumed:!0,columns:i.cols,rows:i.rows},s)}case"terminal.input":return void this.manager.writeToSessionIfCurrent(e.terminalId,s.incarnation,Buffer.from(e.data,"base64"===e.encoding?"base64":"utf8"));case"terminal.resize":return void this.manager.resizeSessionIfCurrent(e.terminalId,s.incarnation,e.columns,e.rows);case"terminal.close":if(!this.manager.closeSessionIfCurrent(e.terminalId,s.incarnation,e.reason))throw new TerminalNotFoundError(e.terminalId);return;case"terminal.ping":if(this.manager.touchSessionIfCurrent(e.terminalId,s.incarnation),!this.isOwnedTerminal(s))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.pong",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),nonce:e.nonce},s);case"terminal.wait":{let n;try{n=await this.manager.waitForExitIfCurrent(e.terminalId,s.incarnation,e.timeoutMs)}catch(e){if(!this.isOwnedTerminal(s))return;throw e}if(!this.isOwnedTerminal(s))return;return void this.sendOrClose({protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.wait_result",clientId:e.clientId,terminalId:e.terminalId,sequence:this.nextSequence(s),exitCode:n.exitCode,output:n.output,state:n.state},s)}case"terminal.release":if(!this.manager.isCurrentSession(e.terminalId,s.incarnation))throw new TerminalNotFoundError(e.terminalId);const t=n.outbound.prepareForgetTerminal(e.clientId,e.terminalId);if(!this.manager.detachSessionIfCurrent(e.terminalId,s.incarnation))throw new TerminalNotFoundError(e.terminalId);return this.clients.get(e.terminalId)===s&&this.clients.delete(e.terminalId),void t();case"terminal.kill":if(!this.manager.killSessionIfCurrent(e.terminalId,s.incarnation,normalizeSignal(e.signal)))throw new TerminalNotFoundError(e.terminalId);return}}async execute(e,n){const t=this.outboundGeneration,i=t?.outbound;if(i?.isAvailable&&!i.isAvailable())throw new Error("terminal Service transport is unavailable; legacy fallback is forbidden");const r=e,s=r.context.terminal.terminalId,a=r.context.terminal.clientId;switch(r.name){case"terminal.open":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:e.params.columns,rows:e.params.rows,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command??t.shell,shell:t.shell,cwd:t.cwd,columns:t.cols,rows:t.rows,state:t.state}}case"terminal.input":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.writeToSessionIfCurrent(s,n.incarnation,Buffer.from(e.params.data,"base64"===e.params.encoding?"base64":"utf8")),{accepted:!0}}case"terminal.resize":{const e=r,n=this.assertAcpOwner(a,s);return this.manager.resizeSessionIfCurrent(s,n.incarnation,e.params.columns,e.params.rows),{resized:!0}}case"terminal.close":{const e=this.assertAcpOwner(a,s);if(!this.manager.closeSessionIfCurrent(s,e.incarnation,r.params.reason))throw new TerminalNotFoundError(s);return{closed:!0}}case"terminal.ping":{const e=this.assertAcpOwner(a,s);return this.manager.touchSessionIfCurrent(s,e.incarnation),{terminalId:s,status:"ok"}}case"terminal.create":{const e=r,n=this.manager.createSessionIncarnation(s),t=this.manager.openSession({terminalId:s,sessionId:e.context.terminal.clientId,command:e.params.command,args:e.params.args,env:e.params.env,cwd:e.params.cwd,cols:80,rows:24,outputLimitBytes:e.params.outputLimitBytes,incarnation:n});return this.clients.set(t.terminalId,{kind:"acp",clientId:e.context.terminal.clientId,incarnation:n}),{terminalId:t.terminalId,command:e.params.command}}case"terminal.wait":{const e=r,n=this.assertAcpOwner(a,s),t=await this.manager.waitForExitIfCurrent(s,n.incarnation,e.params.timeoutMs);if(!this.isOwnedAcpTerminal(n))throw new TerminalNotFoundError(s);return t}case"terminal.release":{const e=this.assertAcpOwner(a,s);if(!this.manager.detachSessionIfCurrent(s,e.incarnation))throw new TerminalNotFoundError(s);return this.clients.get(s)===e&&this.clients.delete(s),{released:!0}}case"terminal.kill":{const e=this.assertAcpOwner(a,s);if(!this.manager.killSessionIfCurrent(s,e.incarnation,normalizeSignal(r.params.signal)))throw new TerminalNotFoundError(s);return{killed:!0}}}throw new Error(`unsupported terminal command ${r.name}`)}nextSequence(e){return String(this.manager.nextServerSequence(e.incarnation.terminalId,e.incarnation))}sendOrClose(e,n){this.isOwnedTerminal(n)&&!n.generation.outbound.send(e)&&this.isOwnedTerminal(n)&&this.manager.closeSessionIfCurrent(e.terminalId,n.incarnation,"backpressure_limit")}emitFrame(e,n){if(!this.isOwnedTerminal(n))return;const t=this.openingFrames.get(e.terminalId);t?.owner===n?t.frames.push(e):this.sendOrClose(e,n)}assertOwner(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))throw new TerminalNotFoundError(n);return i}rebindServiceOwnerForResume(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.generation!==t||!this.isOwnedTerminal(i))return;if(i.clientId===e)return;if(!this.manager.rebindSessionId(n,i.incarnation,e))throw new TerminalNotFoundError(n);t.outbound.rebindResumeClient(i.clientId,e,n),this.clientSequences.delete(i.clientId);const r={kind:"service",clientId:e,generation:i.generation,incarnation:i.incarnation};this.clients.set(n,r)}assertAcpOwner(e,n){const t=this.clients.get(n);if("acp"!==t?.kind||t.clientId!==e||!this.isOwnedAcpTerminal(t))throw new TerminalNotFoundError(n);return t}isGenerationActive(e){return this.outboundGeneration===e}isOwnedTerminal(e){return this.outboundGeneration===e.generation&&this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}isOwnedAcpTerminal(e){return this.clients.get(e.incarnation.terminalId)===e&&this.manager.isCurrentSession(e.incarnation.terminalId,e.incarnation)}expireServiceTerminalReplay(e,n,t){const i=this.clients.get(n);if("service"!==i?.kind||i.clientId!==e||i.generation!==t||!this.isOwnedTerminal(i))return;const r=t.outbound.prepareForgetTerminal(e,n);if(!("closed"===this.manager.getSession(n)?.state?this.manager.releaseSessionIfCurrent(n,i.incarnation):this.manager.detachSessionIfCurrent(n,i.incarnation)))throw new TerminalNotFoundError(n);this.clients.get(n)===i&&this.clients.delete(n),this.openingFrames.get(n)?.owner===i&&this.openingFrames.delete(n),r()}clearGenerationOwnership(e){for(const[n,t]of this.clients)"service"===t.kind&&t.generation===e&&this.clients.delete(n);for(const[n,t]of this.clientSequences)t.generation===e&&this.clientSequences.delete(n);for(const[n,t]of this.openingFrames)t.owner.generation===e&&this.openingFrames.delete(n)}}const runtimeServices=new WeakMap;export function getOrCreateRuntimeTerminalService(e){let n=runtimeServices.get(e);return n||(n=new RuntimeTerminalService(e),runtimeServices.set(e,n)),n}export function assertTerminalClientFrame(e){const n=e;if(!(n&&CLIENT_FRAME_TYPES.has(String(n.type))&&n.protocolVersion===TERMINAL_SERVICE_PROTOCOL_VERSION&&isUuid(n.clientId)&&isUuid(n.terminalId)&&isPositiveRuntimeSequence(n.clientSequence)))throw new Error("invalid terminal client frame");switch(e.type){case"terminal.open":assertOptionalCommand(e.command),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalDimension(e.columns),assertOptionalDimension(e.rows),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.create":("string"!=typeof e.command||e.command.length<1||e.command.length>4096)&&invalidFrame(),assertOptionalArgs(e.args),assertOptionalEnv(e.env),assertOptionalString(e.cwd),assertOptionalOutputLimit(e.outputLimitBytes);break;case"terminal.resume":isRuntimeSequence(e.lastServerSequence)||invalidFrame();break;case"terminal.input":("string"!=typeof e.data||"utf8"!==e.encoding&&"base64"!==e.encoding)&&invalidFrame();break;case"terminal.resize":assertDimension(e.columns),assertDimension(e.rows);break;case"terminal.close":assertOptionalString(e.reason);break;case"terminal.ping":"string"!=typeof e.nonce&&invalidFrame();break;case"terminal.wait":void 0!==e.timeoutMs&&(!Number.isInteger(e.timeoutMs)||e.timeoutMs<0||e.timeoutMs>864e5)&&invalidFrame();break;case"terminal.kill":assertOptionalString(e.signal)}}const CLIENT_FRAME_TYPES=new Set(["terminal.open","terminal.resume","terminal.input","terminal.resize","terminal.close","terminal.ping","terminal.create","terminal.wait","terminal.release","terminal.kill"]);function normalizeSignal(e){return e&&e.startsWith("SIG")?e:"SIGTERM"}const UUID_PATTERN=/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,UINT64_MAX=18446744073709551615n;function isUuid(e){return"string"==typeof e&&UUID_PATTERN.test(e)}function isRuntimeSequence(e){return"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e)&&BigInt(e)<=UINT64_MAX}function isPositiveRuntimeSequence(e){return isRuntimeSequence(e)&&"0"!==e}function invalidFrame(){throw new Error("invalid terminal client frame")}function assertOptionalString(e){void 0!==e&&"string"!=typeof e&&invalidFrame()}function assertOptionalCommand(e){void 0!==e&&("string"!=typeof e||e.length<1||e.length>4096)&&invalidFrame()}function assertOptionalArgs(e){void 0===e||Array.isArray(e)&&e.every(e=>"string"==typeof e)||invalidFrame()}function assertOptionalEnv(e){void 0===e||e&&"object"==typeof e&&!Array.isArray(e)&&Object.values(e).every(e=>"string"==typeof e)||invalidFrame()}function assertDimension(e){(!Number.isInteger(e)||e<1||e>1e3)&&invalidFrame()}function assertOptionalDimension(e){void 0!==e&&assertDimension(e)}function assertOptionalOutputLimit(e){void 0!==e&&(!Number.isInteger(e)||e<1||e>16777216)&&invalidFrame()}
@@ -1 +1 @@
1
- import{spawn}from"node:child_process";import{createRequire}from"node:module";import{randomUUID}from"node:crypto";import{realpathSync}from"node:fs";import{dirname,resolve}from"node:path";export const TERMINAL_TIMEOUT_REASONS={idle:"idle_timeout",maxDuration:"max_duration_timeout"};const ALLOWED_STATE_TRANSITIONS={opening:["running","closing","closed"],running:["closing","closed"],closing:["closed"],closed:[]},require=createRequire(import.meta.url);let cachedNodePty;function resolveOpenclawNodeModulesDir(){const e=process.argv[1];if(e)try{const t=realpathSync(e);if(/openclaw/i.test(t))return dirname(t)}catch{}const t=["/opt/homebrew/lib/node_modules/openclaw","/usr/local/lib/node_modules/openclaw","/usr/lib/node_modules/openclaw"];for(const e of t)try{return realpathSync(resolve(e,"package.json")),e}catch{}return null}function getNodePty(){if(void 0!==cachedNodePty)return cachedNodePty;const e=["node-pty","@lydell/node-pty"];for(const t of e)try{return cachedNodePty=require(t),cachedNodePty}catch{}const t=resolveOpenclawNodeModulesDir();if(t){const s=createRequire(resolve(t,"package.json"));for(const t of e)try{return cachedNodePty=s(t),cachedNodePty}catch{}}return cachedNodePty=null,null}function createPtyProcess(e){const t=getNodePty();if(!t)throw new Error("node-pty not available");const s={...process.env,...e.env,TERM:e.env?.TERM||"xterm-256color",LANG:e.env?.LANG||process.env.LANG||"C.UTF-8",LC_ALL:e.env?.LC_ALL||process.env.LC_ALL||"C.UTF-8"},i=t.spawn(e.shell,[...e.args],{name:"xterm-256color",cwd:e.cwd,env:s,cols:e.cols,rows:e.rows});let n=!1;return i.onData(t=>{e.onData(Buffer.from(t,"utf8"),"stdout")}),i.onExit(t=>{var s;s={code:Number.isFinite(t.exitCode)?t.exitCode:null,signal:null!=t.signal?String(t.signal):null},n||(n=!0,e.onExit(s))}),{write(e){const t=e.toString("utf8");t&&i.write(t)},resize(e,t){i.resize(e,t)},close(){try{i.kill()}catch{}}}}function createPipeProcess(e){const t=spawn(e.shell,[...e.args],{shell:!1,cwd:e.cwd,stdio:["pipe","pipe","pipe"],env:{...process.env,...e.env,TERM:e.env?.TERM||"xterm-256color",LANG:e.env?.LANG||process.env.LANG||"C.UTF-8",LC_ALL:e.env?.LC_ALL||process.env.LC_ALL||"C.UTF-8"}});let s=!1;const i=t=>{s||(s=!0,e.onExit(t))};return t.stdout?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t),"stdout")),t.stderr?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t),"stderr")),t.once("error",e=>i({code:null,signal:null})),t.once("exit",(e,t)=>i({code:e??null,signal:t??null})),{write(e){t.stdin?.destroyed||t.stdin?.write(e)},resize(e,t){},close(){t.killed||t.kill()}}}function createDefaultProcess(e){try{return createPtyProcess(e)}catch{return createPipeProcess(e)}}const DEFAULT_MAX_CONCURRENT=10,DEFAULT_IDLE_TIMEOUT_MS=432e5,DEFAULT_MAX_DURATION_MS=432e5;export class TerminalNotFoundError extends Error{terminalId;constructor(e){super(`terminal not found: ${e}`),this.name="TerminalNotFoundError",this.terminalId=e}}export class TerminalClosedError extends Error{terminalId;state;constructor(e,t){super(`terminal is not writable in state=${t}: ${e}`),this.name="TerminalClosedError",this.terminalId=e,this.state=t}}function chainHandler(e,t){return t?e?s=>{e(s),t(s)}:t:e}export class TerminalManager{sessions=new Map;maxConcurrent;idleTimeoutMs;maxDurationMs;onOutput;onExit;onStateChange;constructor(e){this.maxConcurrent=e?.maxConcurrent??10,this.idleTimeoutMs=e?.idleTimeoutMs??432e5,this.maxDurationMs=e?.maxDurationMs??432e5}addListeners(e){this.onOutput=chainHandler(this.onOutput,e.onOutput),this.onExit=chainHandler(this.onExit,e.onExit),this.onStateChange=chainHandler(this.onStateChange,e.onStateChange)}openSession(e){if(void 0!==e.outputLimitBytes&&(!Number.isInteger(e.outputLimitBytes)||e.outputLimitBytes<1||e.outputLimitBytes>16777216))throw new Error("terminal output limit is outside the contract range");if(this.sessions.size>=this.maxConcurrent&&this.evictOldest(),this.sessions.size>=this.maxConcurrent)throw new Error("terminal quota exceeded");if(e.incarnation&&e.terminalId!==e.incarnation.terminalId)throw new Error("terminal incarnation does not match terminalId");if(e.incarnation&&e.terminalId&&this.sessions.has(e.terminalId))throw new Error(`terminal already exists: ${e.terminalId}`);const t=e.terminalId&&!this.sessions.has(e.terminalId)?e.terminalId:`term_${randomUUID().slice(0,12)}`,s=e.sessionId??t,i=e.command||process.env.SHELL||"/bin/bash",n=e.cwd||process.cwd(),r=Date.now(),o={terminalId:t,sessionId:s,shell:i,cwd:n,startedAt:r,lastActiveAt:r,cols:e.cols,rows:e.rows,state:"opening",nextServerSeq:Math.max(1,e.initialServerSequence??1),output:[],outputBytes:0,outputLimitBytes:Math.max(1,e.outputLimitBytes??1048576),clientExitWaiters:new Set,drainExitWaiters:new Set,process:null,incarnation:e.incarnation??this.createSessionIncarnation(t)};this.sessions.set(t,o);try{o.process=createDefaultProcess({shell:i,args:e.args??[],env:e.env,cwd:n,cols:e.cols,rows:e.rows,onData:(e,t)=>this.handleProcessData(o,e,t),onExit:e=>this.handleProcessExit(o,e)}),this.transitionState(o,"running"),this.scheduleIdleTimeout(o),this.scheduleMaxDuration(o)}catch(e){throw this.clearTimers(o),this.transitionState(o,"closed"),this.sessions.delete(t),e}return this.snapshot(o)}writeToSession(e,t){const s=this.requireSession(e);this.assertRunning(s),s.process?.write(t),this.markActivity(s)}writeToSessionIfCurrent(e,t,s){this.requireCurrentSession(e,t),this.writeToSession(e,s)}resizeSession(e,t,s){const i=this.requireSession(e);this.assertRunning(i),i.cols=t,i.rows=s,i.process?.resize(t,s),this.markActivity(i)}resizeSessionIfCurrent(e,t,s,i){this.requireCurrentSession(e,t),this.resizeSession(e,s,i)}closeSession(e,t){const s=this.sessions.get(e);s&&(t&&!s.timeoutReason&&(s.timeoutReason=t),"closed"!==s.state?(this.clearTimers(s),"closing"!==s.state&&(this.transitionState(s,"closing"),s.process?.close())):this.clearTimers(s))}touchSession(e){const t=this.requireSession(e);this.assertRunning(t),this.markActivity(t)}touchSessionIfCurrent(e,t){this.requireCurrentSession(e,t),this.touchSession(e)}nextServerSequence(e,t){const s=this.requireSession(e);if(t&&s.incarnation!==t)throw new TerminalNotFoundError(e);return s.nextServerSeq++}createSessionIncarnation(e){return Object.freeze({terminalId:e,token:Symbol(e)})}getSessionIncarnation(e){const t=this.sessions.get(e);return t&&!t.detached?t.incarnation:void 0}isCurrentSession(e,t){return this.sessions.get(e)?.incarnation===t}closeSessionIfCurrent(e,t,s){return!!this.isCurrentSession(e,t)&&(this.closeSession(e,s),!0)}releaseSessionIfCurrent(e,t){return!!this.isCurrentSession(e,t)&&(this.release(e),!0)}detachSessionIfCurrent(e,t){return!!this.isCurrentSession(e,t)&&(this.detach(e),!0)}killSessionIfCurrent(e,t,s="SIGTERM"){return!!this.isCurrentSession(e,t)&&(this.kill(e,s),!0)}getSession(e){const t=this.sessions.get(e);return t&&!t.detached?this.snapshot(t):void 0}async waitForExit(e,t=3e4){const s=this.requireSession(e);return"closed"!==s.state&&await new Promise(e=>{let i=!1;const n=()=>{i||(i=!0,clearTimeout(r),s.clientExitWaiters.delete(n),e())},r=setTimeout(n,Math.max(0,t));r.unref?.(),s.clientExitWaiters.add(n)}),{terminalId:e,exitCode:s.exitCode??null,output:s.output.join(""),state:s.state}}async waitForExitIfCurrent(e,t,s=3e4){this.requireCurrentSession(e,t);const i=await this.waitForExit(e,s);return this.requireCurrentSession(e,t),i}create(e){const t=this.openSession({command:e,cols:80,rows:24}),s=this.sessions.get(t.terminalId);return{id:s.terminalId,command:s.shell,state:s.state,output:s.output,exitCode:s.exitCode,seq:s.nextServerSeq-1}}get(e){const t=this.sessions.get(e);if(t&&!t.detached)return{id:t.terminalId,state:t.state,output:t.output,exitCode:t.exitCode,seq:t.nextServerSeq-1}}write(e,t){try{return this.writeToSession(e,Buffer.from(t,"utf8")),!0}catch{return!1}}resize(e,t,s){try{return this.resizeSession(e,t,s),!0}catch{return!1}}kill(e,t="SIGTERM"){const s=this.sessions.get(e);s&&s.process&&("closing"!==s.state&&this.transitionState(s,"closing"),s.process.close())}release(e){const t=this.sessions.get(e);if(t){!t.process||"running"!==t.state&&"opening"!==t.state||this.kill(e),this.clearTimers(t),this.sessions.delete(e);for(const e of[...t.clientExitWaiters])e()}}detach(e){const t=this.requireSession(e);t.detached=!0,t.output=[],t.outputBytes=0;for(const e of[...t.clientExitWaiters])e();"closed"===t.state&&this.sessions.delete(e)}listSessions(){return[...this.sessions.values()].map(e=>this.snapshot(e))}async drain(e,t){const s=[...this.sessions.values()].filter(e=>"closed"!==e.state);if(0===s.length)return"drained";let i;const n=[],r=Promise.all(s.map(e=>new Promise(t=>{const s=()=>{e.drainExitWaiters.delete(s),t()};n.push({session:e,finish:s}),e.drainExitWaiters.add(s)}))).then(()=>!0),o=await Promise.race([r,new Promise(t=>{i=setTimeout(()=>t(!1),Math.max(0,e))})]);if(i&&clearTimeout(i),o)return"drained";for(const e of n)e.session.drainExitWaiters.delete(e.finish);if(t)for(const e of s)this.closeOwnedSession(e,"worker_drain_timeout");return"terminated"}get sessionCount(){return this.sessions.size}get maxConcurrentLimit(){return this.maxConcurrent}destroy(){for(const e of[...this.sessions.keys()])this.release(e)}snapshot(e){return{terminalId:e.terminalId,sessionId:e.sessionId,shell:e.shell,cwd:e.cwd,startedAt:e.startedAt,lastActiveAt:e.lastActiveAt,cols:e.cols,rows:e.rows,state:e.state}}requireSession(e){const t=this.sessions.get(e);if(!t||t.detached)throw new TerminalNotFoundError(e);return t}requireCurrentSession(e,t){const s=this.requireSession(e);if(s.incarnation!==t)throw new TerminalNotFoundError(e);return s}assertRunning(e){if("running"!==e.state)throw new TerminalClosedError(e.terminalId,e.state)}transitionState(e,t){const s=e.state;s!==t&&ALLOWED_STATE_TRANSITIONS[s].includes(t)&&(e.state=t,this.onStateChange?.({terminalId:e.terminalId,sessionId:e.sessionId,from:s,to:t,incarnation:e.incarnation}))}handleProcessData(e,t,s){if(this.sessions.get(e.terminalId)!==e||"closed"===e.state)return;this.markActivity(e);const i=e.nextServerSeq++;appendOutput(e,t),this.onOutput?.({terminalId:e.terminalId,sessionId:e.sessionId,data:t,seq:i,stream:s,incarnation:e.incarnation})}handleProcessExit(e,t){if(this.sessions.get(e.terminalId)===e){this.clearTimers(e),e.exitCode=t.code??void 0;try{if("closing"!==e.state&&this.transitionState(e,"closing"),this.sessions.get(e.terminalId)!==e)return;if(this.transitionState(e,"closed"),this.sessions.get(e.terminalId)!==e)return;this.onExit?.({terminalId:e.terminalId,sessionId:e.sessionId,code:t.code,signal:t.signal,...e.timeoutReason?{reason:e.timeoutReason}:{},seq:e.nextServerSeq++,incarnation:e.incarnation})}finally{for(const t of[...e.clientExitWaiters])t();for(const t of[...e.drainExitWaiters])t();e.detached&&this.sessions.get(e.terminalId)===e&&this.sessions.delete(e.terminalId)}}}closeOwnedSession(e,t){this.sessions.get(e.terminalId)===e&&this.closeSession(e.terminalId,t)}markActivity(e){e.lastActiveAt=Date.now(),"running"===e.state&&this.refreshIdleTimer(e)}evictOldest(){let e=null;for(const t of this.sessions.values()){if("closed"===t.state){e=t;break}(!e||t.lastActiveAt<e.lastActiveAt||t.lastActiveAt===e.lastActiveAt&&t.startedAt<e.startedAt)&&(e=t)}e&&this.release(e.terminalId)}refreshIdleTimer(e){e.idleTimer&&clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{"running"===e.state&&this.closeSessionIfCurrent(e.terminalId,e.incarnation,TERMINAL_TIMEOUT_REASONS.idle)},this.idleTimeoutMs),"object"==typeof e.idleTimer&&"unref"in e.idleTimer&&e.idleTimer.unref()}scheduleIdleTimeout(e){this.refreshIdleTimer(e)}scheduleMaxDuration(e){e.maxDurationTimer=setTimeout(()=>{"running"===e.state&&this.closeSessionIfCurrent(e.terminalId,e.incarnation,TERMINAL_TIMEOUT_REASONS.maxDuration)},this.maxDurationMs),"object"==typeof e.maxDurationTimer&&"unref"in e.maxDurationTimer&&e.maxDurationTimer.unref()}clearTimers(e){e.idleTimer&&(clearTimeout(e.idleTimer),e.idleTimer=void 0),e.maxDurationTimer&&(clearTimeout(e.maxDurationTimer),e.maxDurationTimer=void 0)}}function appendOutput(e,t){const s=t.toString("utf8");for(e.output.push(s),e.outputBytes+=Buffer.byteLength(s);e.outputBytes>e.outputLimitBytes&&e.output.length>0;){const t=e.outputBytes-e.outputLimitBytes,s=e.output[0],i=Buffer.from(s);if(i.length<=t){e.output.shift(),e.outputBytes-=i.length;continue}let n=t;for(;n<i.length&&128==(192&i[n]);)n++;e.output[0]=i.subarray(n).toString("utf8"),e.outputBytes-=n}}
1
+ import{spawn}from"node:child_process";import{createRequire}from"node:module";import{randomUUID}from"node:crypto";import{realpathSync}from"node:fs";import{dirname,resolve}from"node:path";export const TERMINAL_TIMEOUT_REASONS={idle:"idle_timeout",maxDuration:"max_duration_timeout"};const ALLOWED_STATE_TRANSITIONS={opening:["running","closing","closed"],running:["closing","closed"],closing:["closed"],closed:[]},require=createRequire(import.meta.url);let cachedNodePty;function resolveOpenclawNodeModulesDir(){const e=process.argv[1];if(e)try{const t=realpathSync(e);if(/openclaw/i.test(t))return dirname(t)}catch{}const t=["/opt/homebrew/lib/node_modules/openclaw","/usr/local/lib/node_modules/openclaw","/usr/lib/node_modules/openclaw"];for(const e of t)try{return realpathSync(resolve(e,"package.json")),e}catch{}return null}function getNodePty(){if(void 0!==cachedNodePty)return cachedNodePty;const e=["node-pty","@lydell/node-pty"];for(const t of e)try{return cachedNodePty=require(t),cachedNodePty}catch{}const t=resolveOpenclawNodeModulesDir();if(t){const s=createRequire(resolve(t,"package.json"));for(const t of e)try{return cachedNodePty=s(t),cachedNodePty}catch{}}return cachedNodePty=null,null}function createPtyProcess(e){const t=getNodePty();if(!t)throw new Error("node-pty not available");const s={...process.env,...e.env,TERM:e.env?.TERM||"xterm-256color",LANG:e.env?.LANG||process.env.LANG||"C.UTF-8",LC_ALL:e.env?.LC_ALL||process.env.LC_ALL||"C.UTF-8"},i=t.spawn(e.shell,[...e.args],{name:"xterm-256color",cwd:e.cwd,env:s,cols:e.cols,rows:e.rows});let n=!1;return i.onData(t=>{e.onData(Buffer.from(t,"utf8"),"stdout")}),i.onExit(t=>{var s;s={code:Number.isFinite(t.exitCode)?t.exitCode:null,signal:null!=t.signal?String(t.signal):null},n||(n=!0,e.onExit(s))}),{write(e){const t=e.toString("utf8");t&&i.write(t)},resize(e,t){i.resize(e,t)},close(){try{i.kill()}catch{}}}}function createPipeProcess(e){const t=spawn(e.shell,[...e.args],{shell:!1,cwd:e.cwd,stdio:["pipe","pipe","pipe"],env:{...process.env,...e.env,TERM:e.env?.TERM||"xterm-256color",LANG:e.env?.LANG||process.env.LANG||"C.UTF-8",LC_ALL:e.env?.LC_ALL||process.env.LC_ALL||"C.UTF-8"}});let s=!1;const i=t=>{s||(s=!0,e.onExit(t))};return t.stdout?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t),"stdout")),t.stderr?.on("data",t=>e.onData(Buffer.isBuffer(t)?t:Buffer.from(t),"stderr")),t.once("error",e=>i({code:null,signal:null})),t.once("exit",(e,t)=>i({code:e??null,signal:t??null})),{write(e){t.stdin?.destroyed||t.stdin?.write(e)},resize(e,t){},close(){t.killed||t.kill()}}}function createDefaultProcess(e){try{return createPtyProcess(e)}catch{return createPipeProcess(e)}}const DEFAULT_MAX_CONCURRENT=10,DEFAULT_IDLE_TIMEOUT_MS=432e5,DEFAULT_MAX_DURATION_MS=432e5;export class TerminalNotFoundError extends Error{terminalId;constructor(e){super(`terminal not found: ${e}`),this.name="TerminalNotFoundError",this.terminalId=e}}export class TerminalClosedError extends Error{terminalId;state;constructor(e,t){super(`terminal is not writable in state=${t}: ${e}`),this.name="TerminalClosedError",this.terminalId=e,this.state=t}}function chainHandler(e,t){return t?e?s=>{e(s),t(s)}:t:e}export class TerminalManager{sessions=new Map;maxConcurrent;idleTimeoutMs;maxDurationMs;onOutput;onExit;onStateChange;constructor(e){this.maxConcurrent=e?.maxConcurrent??10,this.idleTimeoutMs=e?.idleTimeoutMs??432e5,this.maxDurationMs=e?.maxDurationMs??432e5}addListeners(e){this.onOutput=chainHandler(this.onOutput,e.onOutput),this.onExit=chainHandler(this.onExit,e.onExit),this.onStateChange=chainHandler(this.onStateChange,e.onStateChange)}openSession(e){if(void 0!==e.outputLimitBytes&&(!Number.isInteger(e.outputLimitBytes)||e.outputLimitBytes<1||e.outputLimitBytes>16777216))throw new Error("terminal output limit is outside the contract range");if(this.sessions.size>=this.maxConcurrent&&this.evictOldest(),this.sessions.size>=this.maxConcurrent)throw new Error("terminal quota exceeded");if(e.incarnation&&e.terminalId!==e.incarnation.terminalId)throw new Error("terminal incarnation does not match terminalId");if(e.incarnation&&e.terminalId&&this.sessions.has(e.terminalId))throw new Error(`terminal already exists: ${e.terminalId}`);const t=e.terminalId&&!this.sessions.has(e.terminalId)?e.terminalId:`term_${randomUUID().slice(0,12)}`,s=e.sessionId??t,i=e.command||process.env.SHELL||"/bin/bash",n=e.cwd||process.cwd(),r=Date.now(),o={terminalId:t,sessionId:s,shell:i,cwd:n,startedAt:r,lastActiveAt:r,cols:e.cols,rows:e.rows,state:"opening",nextServerSeq:Math.max(1,e.initialServerSequence??1),output:[],outputBytes:0,outputLimitBytes:Math.max(1,e.outputLimitBytes??1048576),clientExitWaiters:new Set,drainExitWaiters:new Set,process:null,incarnation:e.incarnation??this.createSessionIncarnation(t)};this.sessions.set(t,o);try{o.process=createDefaultProcess({shell:i,args:e.args??[],env:e.env,cwd:n,cols:e.cols,rows:e.rows,onData:(e,t)=>this.handleProcessData(o,e,t),onExit:e=>this.handleProcessExit(o,e)}),this.transitionState(o,"running"),this.scheduleIdleTimeout(o),this.scheduleMaxDuration(o)}catch(e){throw this.clearTimers(o),this.transitionState(o,"closed"),this.sessions.delete(t),e}return this.snapshot(o)}writeToSession(e,t){const s=this.requireSession(e);this.assertRunning(s),s.process?.write(t),this.markActivity(s)}writeToSessionIfCurrent(e,t,s){this.requireCurrentSession(e,t),this.writeToSession(e,s)}resizeSession(e,t,s){const i=this.requireSession(e);this.assertRunning(i),i.cols=t,i.rows=s,i.process?.resize(t,s),this.markActivity(i)}resizeSessionIfCurrent(e,t,s,i){this.requireCurrentSession(e,t),this.resizeSession(e,s,i)}closeSession(e,t){const s=this.sessions.get(e);s&&(t&&!s.timeoutReason&&(s.timeoutReason=t),"closed"!==s.state?(this.clearTimers(s),"closing"!==s.state&&(this.transitionState(s,"closing"),s.process?.close())):this.clearTimers(s))}touchSession(e){const t=this.requireSession(e);this.assertRunning(t),this.markActivity(t)}touchSessionIfCurrent(e,t){this.requireCurrentSession(e,t),this.touchSession(e)}nextServerSequence(e,t){const s=this.requireSession(e);if(t&&s.incarnation!==t)throw new TerminalNotFoundError(e);return s.nextServerSeq++}createSessionIncarnation(e){return Object.freeze({terminalId:e,token:Symbol(e)})}getSessionIncarnation(e){const t=this.sessions.get(e);return t&&!t.detached?t.incarnation:void 0}isCurrentSession(e,t){return this.sessions.get(e)?.incarnation===t}rebindSessionId(e,t,s){const i=this.sessions.get(e);return!(!i||i.detached||i.incarnation!==t)&&("string"==typeof s&&0!==s.length&&(i.sessionId=s,!0))}closeSessionIfCurrent(e,t,s){return!!this.isCurrentSession(e,t)&&(this.closeSession(e,s),!0)}releaseSessionIfCurrent(e,t){return!!this.isCurrentSession(e,t)&&(this.release(e),!0)}detachSessionIfCurrent(e,t){return!!this.isCurrentSession(e,t)&&(this.detach(e),!0)}killSessionIfCurrent(e,t,s="SIGTERM"){return!!this.isCurrentSession(e,t)&&(this.kill(e,s),!0)}getSession(e){const t=this.sessions.get(e);return t&&!t.detached?this.snapshot(t):void 0}async waitForExit(e,t=3e4){const s=this.requireSession(e);return"closed"!==s.state&&await new Promise(e=>{let i=!1;const n=()=>{i||(i=!0,clearTimeout(r),s.clientExitWaiters.delete(n),e())},r=setTimeout(n,Math.max(0,t));r.unref?.(),s.clientExitWaiters.add(n)}),{terminalId:e,exitCode:s.exitCode??null,output:s.output.join(""),state:s.state}}async waitForExitIfCurrent(e,t,s=3e4){this.requireCurrentSession(e,t);const i=await this.waitForExit(e,s);return this.requireCurrentSession(e,t),i}create(e){const t=this.openSession({command:e,cols:80,rows:24}),s=this.sessions.get(t.terminalId);return{id:s.terminalId,command:s.shell,state:s.state,output:s.output,exitCode:s.exitCode,seq:s.nextServerSeq-1}}get(e){const t=this.sessions.get(e);if(t&&!t.detached)return{id:t.terminalId,state:t.state,output:t.output,exitCode:t.exitCode,seq:t.nextServerSeq-1}}write(e,t){try{return this.writeToSession(e,Buffer.from(t,"utf8")),!0}catch{return!1}}resize(e,t,s){try{return this.resizeSession(e,t,s),!0}catch{return!1}}kill(e,t="SIGTERM"){const s=this.sessions.get(e);s&&s.process&&("closing"!==s.state&&this.transitionState(s,"closing"),s.process.close())}release(e){const t=this.sessions.get(e);if(t){!t.process||"running"!==t.state&&"opening"!==t.state||this.kill(e),this.clearTimers(t),this.sessions.delete(e);for(const e of[...t.clientExitWaiters])e()}}detach(e){const t=this.requireSession(e);t.detached=!0,t.output=[],t.outputBytes=0;for(const e of[...t.clientExitWaiters])e();"closed"===t.state&&this.sessions.delete(e)}listSessions(){return[...this.sessions.values()].map(e=>this.snapshot(e))}async drain(e,t){const s=[...this.sessions.values()].filter(e=>"closed"!==e.state);if(0===s.length)return"drained";let i;const n=[],r=Promise.all(s.map(e=>new Promise(t=>{const s=()=>{e.drainExitWaiters.delete(s),t()};n.push({session:e,finish:s}),e.drainExitWaiters.add(s)}))).then(()=>!0),o=await Promise.race([r,new Promise(t=>{i=setTimeout(()=>t(!1),Math.max(0,e))})]);if(i&&clearTimeout(i),o)return"drained";for(const e of n)e.session.drainExitWaiters.delete(e.finish);if(t)for(const e of s)this.closeOwnedSession(e,"worker_drain_timeout");return"terminated"}get sessionCount(){return this.sessions.size}get maxConcurrentLimit(){return this.maxConcurrent}destroy(){for(const e of[...this.sessions.keys()])this.release(e)}snapshot(e){return{terminalId:e.terminalId,sessionId:e.sessionId,shell:e.shell,cwd:e.cwd,startedAt:e.startedAt,lastActiveAt:e.lastActiveAt,cols:e.cols,rows:e.rows,state:e.state}}requireSession(e){const t=this.sessions.get(e);if(!t||t.detached)throw new TerminalNotFoundError(e);return t}requireCurrentSession(e,t){const s=this.requireSession(e);if(s.incarnation!==t)throw new TerminalNotFoundError(e);return s}assertRunning(e){if("running"!==e.state)throw new TerminalClosedError(e.terminalId,e.state)}transitionState(e,t){const s=e.state;s!==t&&ALLOWED_STATE_TRANSITIONS[s].includes(t)&&(e.state=t,this.onStateChange?.({terminalId:e.terminalId,sessionId:e.sessionId,from:s,to:t,incarnation:e.incarnation}))}handleProcessData(e,t,s){if(this.sessions.get(e.terminalId)!==e||"closed"===e.state)return;this.markActivity(e);const i=e.nextServerSeq++;appendOutput(e,t),this.onOutput?.({terminalId:e.terminalId,sessionId:e.sessionId,data:t,seq:i,stream:s,incarnation:e.incarnation})}handleProcessExit(e,t){if(this.sessions.get(e.terminalId)===e){this.clearTimers(e),e.exitCode=t.code??void 0;try{if("closing"!==e.state&&this.transitionState(e,"closing"),this.sessions.get(e.terminalId)!==e)return;if(this.transitionState(e,"closed"),this.sessions.get(e.terminalId)!==e)return;this.onExit?.({terminalId:e.terminalId,sessionId:e.sessionId,code:t.code,signal:t.signal,...e.timeoutReason?{reason:e.timeoutReason}:{},seq:e.nextServerSeq++,incarnation:e.incarnation})}finally{for(const t of[...e.clientExitWaiters])t();for(const t of[...e.drainExitWaiters])t();e.detached&&this.sessions.get(e.terminalId)===e&&this.sessions.delete(e.terminalId)}}}closeOwnedSession(e,t){this.sessions.get(e.terminalId)===e&&this.closeSession(e.terminalId,t)}markActivity(e){e.lastActiveAt=Date.now(),"running"===e.state&&this.refreshIdleTimer(e)}evictOldest(){let e=null;for(const t of this.sessions.values()){if("closed"===t.state){e=t;break}(!e||t.lastActiveAt<e.lastActiveAt||t.lastActiveAt===e.lastActiveAt&&t.startedAt<e.startedAt)&&(e=t)}e&&this.release(e.terminalId)}refreshIdleTimer(e){e.idleTimer&&clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{"running"===e.state&&this.closeSessionIfCurrent(e.terminalId,e.incarnation,TERMINAL_TIMEOUT_REASONS.idle)},this.idleTimeoutMs),"object"==typeof e.idleTimer&&"unref"in e.idleTimer&&e.idleTimer.unref()}scheduleIdleTimeout(e){this.refreshIdleTimer(e)}scheduleMaxDuration(e){e.maxDurationTimer=setTimeout(()=>{"running"===e.state&&this.closeSessionIfCurrent(e.terminalId,e.incarnation,TERMINAL_TIMEOUT_REASONS.maxDuration)},this.maxDurationMs),"object"==typeof e.maxDurationTimer&&"unref"in e.maxDurationTimer&&e.maxDurationTimer.unref()}clearTimers(e){e.idleTimer&&(clearTimeout(e.idleTimer),e.idleTimer=void 0),e.maxDurationTimer&&(clearTimeout(e.maxDurationTimer),e.maxDurationTimer=void 0)}}function appendOutput(e,t){const s=t.toString("utf8");for(e.output.push(s),e.outputBytes+=Buffer.byteLength(s);e.outputBytes>e.outputLimitBytes&&e.output.length>0;){const t=e.outputBytes-e.outputLimitBytes,s=e.output[0],i=Buffer.from(s);if(i.length<=t){e.output.shift(),e.outputBytes-=i.length;continue}let n=t;for(;n<i.length&&128==(192&i[n]);)n++;e.output[0]=i.subarray(n).toString("utf8"),e.outputBytes-=n}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.6.8",
3
+ "version": "0.6.10",
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>",