beeos-claw 0.6.66-cloud.5 → 0.6.66-cloud.7

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{toMessagePart}from"../message-client-types.js";import{isUploadArtifactToolName,resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{sessionWorkspaceDir,takeFinalizeAttachments}from"../upload/delivery-ledger.js";import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";import{extractSearchSources,mergeSearchSource,searchSourceProvider}from"./search-source-extractor.js";import{logChatLatency}from"../chat-latency.js";const STREAMING_SNAPSHOT_PUBLISH_INTERVAL_MS=200,isNonEmptyString=e=>"string"==typeof e&&e.length>0,isPlainRecord=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function pickRawResultFromPayload(e){if(void 0!==e.rawResult)return e.rawResult;if(void 0!==e.result)return e.result;if(void 0!==e.details)return e.details;if(void 0!==e.toolResult)return e.toolResult;if(isPlainRecord(e.channelData)){const t=e.channelData;if(void 0!==t.rawResult)return t.rawResult;if(void 0!==t.result)return t.result;if(void 0!==t.details)return t.details}}function asToolParams(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e}const summarizeObservedToolError=e=>{if(null==e)return;if("string"==typeof e)return e.slice(0,180);if("object"!=typeof e)return String(e).slice(0,180);const t=e;for(const e of["error","message","blockReason","details","text"]){const r=t[e];if("string"==typeof r&&r.trim())return r.slice(0,180);if(isPlainRecord(r)&&"string"==typeof r.message&&r.message.trim())return r.message.slice(0,180)}try{const t=JSON.stringify(e);return t?t.slice(0,180):void 0}catch{return}},observedToolResultText=e=>{if(!isPlainRecord(e)||!Array.isArray(e.content))return;const t=e.content.filter(e=>isPlainRecord(e)&&"text"===e.type&&"string"==typeof e.text).map(e=>e.text.trim()).filter(Boolean);return t.length>0?t.join("\n"):void 0};export function createImReplySink(e){const t=e.startedAtMs??Date.now();let r=!1;const s=new Map,n=new Set,o={text:""};let a="",i="",l=!1,d=!1;const u=new Set;let c;const m={receivedSnapshotCount:0,streamingPublishCount:0,terminalPublishCount:0},g=[];let p;const f=new Map,h=new Map,v=[],b=[],y=new Map,I=new Map;let _=0,S=1;const x=(e,t={})=>({schema_version:1,seq:e,...t}),k=new Set,$=new Set,q=t=>{t!==o.text&&(t.startsWith(o.text)?e.stream.appendBody(t.slice(o.text.length)):e.stream.setBody(t),o.text=t)},w=e=>{void 0===e.publishTimer&&(e.publishTimer=setTimeout(()=>{e.publishTimer=void 0,de("body_preview_flush",()=>{c===e&&(e=>{o.text!==e.targetBody&&(q(e.targetBody),e.streamingPublishCount+=1)})(e)})},200))},C=e=>{const t=c;t?(void 0!==t.publishTimer&&(clearTimeout(t.publishTimer),t.publishTimer=void 0),t.targetBody=e,o.text!==e&&(q(e),t.terminalPublishCount+=1),m.receivedSnapshotCount+=t.receivedSnapshotCount,m.streamingPublishCount+=t.streamingPublishCount,m.terminalPublishCount+=t.terminalPublishCount,c=void 0):q(e)},E=t=>{if(void 0!==t.partIndex)return;const r=_,s=S;e.stream.addPart(toMessagePart({type:"text",text:t.text,state:"done",_seq:s,data:x(s)})),t.partIndex=r,_+=1,S+=1},T=()=>{if(!d){d=!0;for(const e of g)E(e)}},P=e=>{const t={text:e};g.push(t),d&&E(t)},R=(t,r)=>{for(const s of t){const t=I.get(s.url);if(t){const r=mergeSearchSource(t,s);if(r.title===t.title&&r.snippet===t.snippet)continue;const n={...r,partIndex:t.partIndex,displaySeq:t.displaySeq,toolUseId:t.toolUseId};e.stream.replacePart(t.partIndex,toMessagePart({type:"source",url:n.url,...n.title?{title:n.title}:{},...n.snippet?{snippet:n.snippet}:{},tool_use_id:n.toolUseId,_seq:n.displaySeq,data:x(n.displaySeq)})),I.set(s.url,n);continue}const n={...s,partIndex:_,displaySeq:S,toolUseId:r};e.stream.addPart(toMessagePart({type:"source",url:s.url,...s.title?{title:s.title}:{},...s.snippet?{snippet:s.snippet}:{},tool_use_id:r,_seq:n.displaySeq,data:x(n.displaySeq)})),I.set(s.url,n),_+=1,S+=1,ae.source+=1}},M=(e,t,r,s)=>toMessagePart({type:"thinking",text:t,state:r,_seq:e.displaySeq,_started_at_ms:e.startedAtMs,...void 0!==s?{_duration_ms:s}:{},data:x(e.displaySeq,{started_at_ms:e.startedAtMs,...void 0!==s?{duration_ms:s}:{}})}),O=t=>{T();const r={partIndex:_,displaySeq:S,text:t,publishedText:t,receivedSnapshotCount:1,streamingPublishCount:1,startedAtMs:Date.now(),startedAtMonotonicMs:performance.now()};return e.stream.addPart(M(r,t,"streaming")),_+=1,S+=1,p=r,r},A=t=>{void 0===t.publishTimer&&(t.publishTimer=setTimeout(()=>{t.publishTimer=void 0,de("reasoning_snapshot_flush",()=>{p===t&&(t=>{t.publishedText!==t.text&&(e.stream.replacePart(t.partIndex,M(t,t.text,"streaming")),t.publishedText=t.text,t.streamingPublishCount+=1)})(t)})},200))},F=()=>{const r=p;if(!r)return;void 0!==r.publishTimer&&(clearTimeout(r.publishTimer),r.publishTimer=void 0);const s=Math.max(0,Math.round(performance.now()-r.startedAtMonotonicMs));e.stream.replacePart(r.partIndex,M(r,r.text,"done",s)),p=void 0,logChatLatency(e.logger,"claw.reasoning.settled",e.requestId,t,{reasoning_duration_ms:s,reasoning_chars:r.text.length,reasoning_snapshot_count:r.receivedSnapshotCount,reasoning_streaming_publish_count:r.streamingPublishCount,reasoning_coalesced_snapshot_count:Math.max(0,r.receivedSnapshotCount-r.streamingPublishCount),reasoning_publish_interval_ms:200})},N=e=>{const t=v.indexOf(e);t>=0&&v.splice(t,1)},U=e=>{const t=e?v.filter(t=>f.get(t)?.name===e):v;return 1===t.length?t[0]:void 0},j=e=>{const t=b.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:r,index:s}]=t;return b.splice(s,1),r},L=(e,t)=>{if(h.set(e,t),e===t)return;const r=y.get(e);if(!r)return;const s=D(t);W(s,r),y.delete(e);const n=f.get(t);n&&(Y(n,s),y.delete(t))},B=e=>h.get(e)??e,z=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),D=e=>{let t=y.get(e);return t||(t={},y.set(e,t)),t},Q=(e,t)=>{if(!e?.length&&!t?.length)return;const r=[],s=new Map;for(const n of[...e??[],...t??[]]){const e=s.get(n.url);void 0===e?(s.set(n.url,r.length),r.push(n)):r[e]=mergeSearchSource(r[e],n)}return r},W=(e,t)=>{e.name??=t.name,e.arguments??=t.arguments,t.toolStartObserved&&(e.toolStartObserved=!0),t.itemEventObserved&&(e.itemEventObserved=!0),t.progress?.length&&(e.progress=[...e.progress??[],...t.progress]),e.observedResult??=t.observedResult,e.sources=Q(e.sources,t.sources)},J=(t,r,s)=>{const n="tool"===t.name&&"tool"!==r?r:t.name,o=!z(t.arguments)&&z(s)?s:t.arguments;n===t.name&&o===t.arguments||(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:n,arguments:o,state:t.state,_seq:t.displaySeq,data:x(t.displaySeq)})),t.name=n,t.arguments=o)},H=(t,r,s,n)=>{const o=f.get(t);if(o)return J(o,r,s),n&&!v.includes(t)&&"streaming"===o.state&&v.push(t),o;T();const a=y.get(t),i="tool"!==r?r:a?.name&&"tool"!==a.name?a.name:r,l=z(s)?s:a?.arguments??s,d=((t,r,s)=>{const n=_,o=S;return e.stream.addPart(toMessagePart({type:"tool_use",id:t,name:r,arguments:s,state:"streaming",_seq:o,data:x(o)})),_+=1,S+=1,{partIndex:n,displaySeq:o}})(t,i,l),u={id:t,name:i,arguments:l,partIndex:d.partIndex,displaySeq:d.displaySeq,state:"streaming",itemEventObserved:!0===a?.itemEventObserved,toolStartObserved:!0===a?.toolStartObserved};return f.set(t,u),(n||u.toolStartObserved||u.itemEventObserved)&&(v.includes(t)||"streaming"!==u.state||v.push(t)),a&&Y(u,a),y.delete(t),u},K=(e,t)=>{const r=(e=>{const t=h.get(e);return t?f.get(t):f.get(e)})(e);if(r)return void 0===t.name&&void 0===t.arguments||J(r,t.name??r.name,t.arguments??r.arguments),t.toolStartObserved&&(r.toolStartObserved=!0),t.itemEventObserved&&(r.itemEventObserved=!0),(t.pending||t.toolStartObserved||t.itemEventObserved)&&!v.includes(r.id)&&"streaming"===r.state&&v.push(r.id),r;const s=D(e);void 0!==t.name&&(s.name=t.name),void 0!==t.arguments&&(s.arguments=t.arguments),t.toolStartObserved&&(s.toolStartObserved=!0),t.itemEventObserved&&(s.itemEventObserved=!0)},G=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done",_seq:t.displaySeq,data:x(t.displaySeq)})),t.state="done")},V=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(V).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${V(t)}`).join(",")}}`},X=(t,r,s)=>{const n=`${s?"1":"0"}:${V(r)}`;if(t.progressSignature===n)return;const o=toMessagePart({type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:r,...s?{is_error:!0}:{}}});if(void 0===t.progressPartIndex){const r=_;return e.stream.addPart(o),t.progressPartIndex=r,t.progressSignature=n,void(_+=1)}e.stream.replacePart(t.progressPartIndex,o),t.progressSignature=n},Y=(e,t)=>{if(t.progress?.length)for(const r of t.progress)X(e,r.content,r.isError);t.observedResult&&(X(e,t.observedResult.content,t.observedResult.isError),e.observedResult=t.observedResult),(t.progress?.length||t.observedResult)&&(G(e),N(e.id)),t.sources?.length&&R(t.sources,e.id)},Z=e=>[...new Set([e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e)).map(e=>e.trim()).filter(Boolean))],ee=(e,t=[])=>{const r=[...new Set([...Z(e),...t])];return isNonEmptyString(e.text)&&r.length>0?{text:e.text,media_urls:r}:isNonEmptyString(e.text)?e.text:1===r.length?r[0]:r.length>1?{media_urls:r}:!0===e.isError?"Tool execution failed.":""},te=t=>{for(const r of t)e.stream.addPart(toMessagePart({type:"resource_link",uri:r.uri,name:r.name,...r.mimeType?{mimeType:r.mimeType}:{}})),_+=1},re=t=>{k.add(t);const r=()=>{k.delete(t)};e.onCallbackFailure?t.then(r,r):t.finally(r)},se=(t,r)=>{if(t.durableUploadFilesEmitted)return;e.logger.info?.(`[im-sink] schedule tool artifact enrichment requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`);const s=pickRawResultFromPayload(r),n=(async()=>{let n=[];try{n=await resolveToolArtifactLinks({toolCallId:t.id,toolName:t.name,rawResult:s,rawText:"string"==typeof r.text?r.text:void 0,params:asToolParams(t.arguments),cwd:sessionWorkspaceDir(e.sessionKey),payloadMediaUrls:Z(r)})}catch(r){const s=r instanceof Error?r.message:String(r);if(e.logger.warn?.(`[im-sink] resolveToolArtifactLinks failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure)throw r;return}0!==n.length?(e.logger.info?.(`[im-sink] tool artifact enrichment resolved ${n.length} link(s) requestId=${e.requestId} toolUseId=${t.id}`),await new Promise((s,o)=>{!1===e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;const s=n.map(e=>e.uri);t.artifactMediaUrls=s;const o=t.observedResult;X(t,ee(o?{text:o.content,isError:o.isError}:r,s),o?.isError??!0===r.isError),((t,r,s)=>{0===r.length||t.durableUploadFilesEmitted||(t.durableUploadFilesEmitted=!0,e.stream.appendToolResult(t.id,r.map(e=>({type:"resource_link",uri:e.uri,name:e.name,...e.mimeType?{mimeType:e.mimeType}:{}})),s),_+=1,te(r))})(t,n,o?.isError??!0===r.isError)}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment apply failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure&&o(r)}finally{s()}})&&e.onCallbackFailure&&e.callbackQueue.waitForIdle().then(s,o)})):e.logger.debug?.(`[im-sink] tool artifact enrichment found no links requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`)})().catch(r=>{const s=r instanceof Error?r.message:String(r);if(e.logger.warn?.(`[im-sink] tool artifact enrichment failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure)throw r});re(n)},ne=(e,t,r)=>{const s=h.get(e);if(s)return K(s,{name:t,arguments:r});const n=K(e,{name:t,arguments:r});if(n)return n;const o=U(t);if(o)return L(e,o),K(o,{name:t,arguments:r});K(e,{name:t,arguments:r})},oe=(e,t)=>{const r=h.get(e);if(r)return K(r,{name:t,itemEventObserved:!0,pending:!0});const s=K(e,{name:t,itemEventObserved:!0,pending:!0});if(s)return s;const n=(e=>{const t=v.filter(t=>{const r=f.get(t);return!(void 0===r||e&&r.name!==e||r.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(n)return L(e,n),K(n,{name:t,itemEventObserved:!0,pending:!0});const o=j(t);K(e,{name:t,arguments:o?.arguments,itemEventObserved:!0,pending:!0})},ae={tool:0,block:0,final:0,source:0};let ie,le=!1;const de=(t,r)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!le||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{r()}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${s}`),e.onCallbackFailure?.(r)}})};return{dispatcher:{sendToolResult:t=>(de("tool_progress",()=>{const r=resolveRuntimeToolCallId(t),s=resolveRuntimeToolName(t),n=resolveRuntimeToolArguments(t),o=ee(t),a=!0===t.isError;let i;if(r){if(i=ne(r,s,n),!i){const e=D(r);return s&&(e.name=s),z(n)&&(e.arguments=n),e.progress=[...e.progress??[],{content:o,isError:a}],void(ae.tool+=1)}}else{const t=s?U(s):U();if(!t)return j(s),void e.logger.debug?.(`[im-sink] unattributed tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${s??"unknown"} pending=${v.length}`);i=K(t,{name:s,arguments:n})}i&&(ae.tool+=1,i.observedResult||X(i,o,a),G(i),N(i.id),se(i,t))}),!0),sendBlockReply:e=>(de("block_reply",()=>{ae.block+=1,isNonEmptyString(e.text)&&(ie=!0!==e.isError?void 0:e.text)}),!0),sendFinalReply:t=>(de("final_reply",()=>{var r;(ae.final+=1,!0!==t.isError)?((t=>{const r=Z(t).filter(e=>!$.has(e));if(0===r.length)return;for(const e of r)$.add(e);const s=(async()=>{let t=[];try{t=await resolveToolArtifactLinks({toolCallId:`final:${e.runId??e.requestId}`,toolName:"final_reply_media",payloadMediaUrls:r})}catch(t){for(const e of r)$.delete(e);if(e.logger.warn?.(`[im-sink] final media enrichment failed requestId=${e.requestId} error=${t instanceof Error?t.message:String(t)}`),e.onCallbackFailure?.(t),e.onCallbackFailure)throw t;return}if(0!==t.length)await new Promise((r,s)=>{!1===e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;te(t)}catch(t){e.logger.warn?.(`[im-sink] final media enrichment apply failed requestId=${e.requestId} error=${t instanceof Error?t.message:String(t)}`),e.onCallbackFailure?.(t),e.onCallbackFailure&&s(t)}finally{r()}})&&e.onCallbackFailure&&e.callbackQueue.waitForIdle().then(r,s)});else for(const e of r)$.delete(e)})();re(s)})(t),isNonEmptyString(t.text)&&(ie=void 0,l||(r=t.text,i="",a+=r,C(a),P(r),l=!0))):ie=t.text}),!0),waitForIdle:async()=>{for(let t=0;(e.onCallbackFailure||t<16)&&(await e.callbackQueue.waitForIdle(),0!==k.size);t++)e.onCallbackFailure?await Promise.all([...k]):await Promise.allSettled([...k]);await e.callbackQueue.waitForIdle();const t=takeFinalizeAttachments(e.sessionKey);t.length>0&&!e.abortSignal.aborted&&!e.stream.isTerminated&&te(t)},getQueuedCounts:()=>({tool:ae.tool,block:ae.block,final:ae.final}),getAccumulatedBody:()=>o.text,getTerminalErrorText:()=>ie,markComplete:r=>{F(),(b.length>0||y.size>0)&&e.logger.debug?.(`[im-sink] dropping uncommitted lifecycle tool signals requestId=${e.requestId} legacy_hints=${b.length} buffered=${y.size}`),i="",C(a),m.receivedSnapshotCount>0&&logChatLatency(e.logger,"claw.text_preview.settled",e.requestId,t,{text_preview_snapshot_count:m.receivedSnapshotCount,text_preview_streaming_publish_count:m.streamingPublishCount,text_preview_terminal_publish_count:m.terminalPublishCount,text_preview_coalesced_snapshot_count:Math.max(0,m.receivedSnapshotCount-m.streamingPublishCount-m.terminalPublishCount),text_preview_publish_interval_ms:200});for(const t of f.values())try{"success"===r&&isUploadArtifactToolName(t.name)&&!t.durableUploadFilesEmitted&&se(t,{toolCallId:t.id,toolName:t.name}),"success"===r&&G(t),N(t.id)}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`)}b.length=0,y.clear(),le=!0}},replyOptions:{...e.runId?{runId:e.runId}:{},abortSignal:e.abortSignal,disableBlockStreaming:!0,onAgentRunStart:r=>{e.onAgentRunStart?.(r),logChatLatency(e.logger,"claw.runtime.run_started",e.requestId,t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${r} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:s=>{de("partial",()=>{"string"==typeof s.text&&(!r&&s.text.length>0&&(r=!0,logChatLatency(e.logger,"claw.model.first_delta",e.requestId,t)),i=s.text,(e=>{const t=c;if(!t){if(o.text===e)return;return q(e),void(c={targetBody:e,receivedSnapshotCount:1,streamingPublishCount:1,terminalPublishCount:0})}t.targetBody!==e&&(t.targetBody=e,t.receivedSnapshotCount+=1,w(t))})(a+i))})},onReasoningStream:e=>{de("reasoning",()=>{if(!isNonEmptyString(e.text))return;const t=e.text,r=p??O(t);t!==r.text&&(r.text.startsWith(t)||(t.startsWith(r.text)?(r.text=t,r.receivedSnapshotCount+=1,A(r)):(F(),O(t))))})},onReasoningEnd:()=>{de("reasoning_end",()=>{F()})},onAssistantMessageStart:()=>{de("assistant_message_start",()=>{i="",l=!1,C(a),e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)})},onToolStart:r=>{de("tool_start",()=>{const n="string"==typeof r.phase?r.phase:"start",o=resolveRuntimeToolName(r)??"tool",a=resolveRuntimeToolCallId(r),i=resolveRuntimeToolArguments(r);if("update"===n)return void(a&&K(B(a),{name:o,arguments:i,pending:!0}));if(a&&s.set(a,Date.now()),logChatLatency(e.logger,"claw.tool.start",e.requestId,t,{tool:o}),a){K(a,{name:o,arguments:i,toolStartObserved:!0,pending:!0});const e=U(o);return void(e&&e!==a&&!h.has(a)&&(L(a,e),K(e,{name:o,arguments:i,toolStartObserved:!0,pending:!0})))}const l=(d=o,v.map(e=>f.get(e)).filter(e=>void 0!==e&&(!d||e.name===d)&&e.itemEventObserved));var d;if(1===l.length)return void K(l[0].id,{name:o,arguments:i,toolStartObserved:!0,pending:!0});if(l.length>1)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${o} candidates=${l.length}`);const u=[...y.entries()].filter(([,e])=>!e.name||e.name===o||"tool"===e.name);1!==u.length?b.push({name:o,arguments:i}):K(u[0][0],{name:o,arguments:i,toolStartObserved:!0,pending:!0})})},onItemEvent:e=>{de("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const r="string"==typeof e.phase?e.phase.toLowerCase():"update",s=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),n=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);s&&n&&("update"!==r?oe(s,n):K(B(s),{name:n,itemEventObserved:!0,pending:!0}))})}},commitAssistantMessage:e=>{de("assistant_message_commit",()=>{if(u.has(e.key))return;u.add(e.key),l=!0,ie=void 0,i="";const t=e.segments.filter(e=>"text"===e.type).map(e=>e.text).join("");a+=t,C(a);for(const t of e.segments)"text"!==t.type?H(t.id,t.name,t.arguments,!0):P(t.text)})},observeToolResult:r=>{de("tool_result_observation",()=>{const o=r.toolCallId.trim(),a=r.toolName.trim();if(!o||!a)return;const i=!n.has(o);if(i){n.add(o);const i=s.get(o);logChatLatency(e.logger,"claw.tool.result",e.requestId,t,{tool:a,tool_elapsed_ms:void 0===i?void 0:Math.max(0,Date.now()-i),outcome:r.isError?"error":"success",...r.isError?{error:summarizeObservedToolError(r.rawResult)??"error"}:{}})}if(e.persistObservedToolResults&&i){const e=observedToolResultText(r.rawResult);if(void 0!==e){const t=ne(o,a);if(t)X(t,ee({text:e,isError:r.isError},t.artifactMediaUrls),r.isError),t.observedResult={content:e,isError:r.isError},G(t),N(t.id);else{const t=D(B(o));t.name??=a,t.observedResult={content:e,isError:r.isError}}}}const l=searchSourceProvider(r.rawResult)??"unknown";let d=0;try{const t=extractSearchSources({toolName:a,rawResult:r.rawResult,isError:r.isError});if(d=t.length,0===d)return;const s=ne(o,a);if(s)R(t,s.id);else{const e=D(B(o));e.name??=a,e.sources=Q(e.sources,t)}e.logger.debug?.(`[im-sink] search source projection requestId=${e.requestId} tool=${a} provider=${l} source_count=${d}`)}catch{e.logger.warn?.(`[im-sink] search source projection ignored requestId=${e.requestId} tool=${a} provider=${l} source_count=${d}`)}})},getSourceCount:()=>ae.source}}
1
+ import{toMessagePart}from"../message-client-types.js";import{isUploadArtifactToolName,resolveToolArtifactLinks}from"../upload/tool-artifact-enrichment.js";import{sessionWorkspaceDir,takeFinalizeAttachments}from"../upload/delivery-ledger.js";import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";import{extractSearchSources,mergeSearchSource,searchSourceProvider}from"./search-source-extractor.js";import{logChatLatency}from"../chat-latency.js";const STREAMING_SNAPSHOT_PUBLISH_INTERVAL_MS=200,isNonEmptyString=e=>"string"==typeof e&&e.length>0,isPlainRecord=e=>"object"==typeof e&&null!==e&&!Array.isArray(e);function pickRawResultFromPayload(e){if(void 0!==e.rawResult)return e.rawResult;if(void 0!==e.result)return e.result;if(void 0!==e.details)return e.details;if(void 0!==e.toolResult)return e.toolResult;if(isPlainRecord(e.channelData)){const t=e.channelData;if(void 0!==t.rawResult)return t.rawResult;if(void 0!==t.result)return t.result;if(void 0!==t.details)return t.details}}function asToolParams(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e}const summarizeObservedToolError=e=>{if(null==e)return;if("string"==typeof e)return e.slice(0,180);if("object"!=typeof e)return String(e).slice(0,180);const t=e;for(const e of["error","message","blockReason","details","text"]){const r=t[e];if("string"==typeof r&&r.trim())return r.slice(0,180);if(isPlainRecord(r)&&"string"==typeof r.message&&r.message.trim())return r.message.slice(0,180)}try{const t=JSON.stringify(e);return t?t.slice(0,180):void 0}catch{return}},observedToolResultText=e=>{if(!isPlainRecord(e)||!Array.isArray(e.content))return;const t=e.content.filter(e=>isPlainRecord(e)&&"text"===e.type&&"string"==typeof e.text).map(e=>e.text.trim()).filter(Boolean);return t.length>0?t.join("\n"):void 0};export function createImReplySink(e){const t=e.startedAtMs??Date.now();let r=!1;const s=new Map,n=new Set,o={text:""};let a="",i="",l=!1,d=!1;const u=new Set;let m;const c={receivedSnapshotCount:0,streamingPublishCount:0,terminalPublishCount:0},g=[];let p;const f=new Map,h=new Map,v=[],b=[],y=new Map,I=new Map;let _=0,S=1;const x=(e,t={})=>({schema_version:1,seq:e,...t}),k=new Set,$=new Set,q=t=>{t!==o.text&&(t.startsWith(o.text)?e.stream.appendBody(t.slice(o.text.length)):e.stream.setBody(t),o.text=t)},w=e=>{void 0===e.publishTimer&&(e.publishTimer=setTimeout(()=>{e.publishTimer=void 0,de("body_preview_flush",()=>{m===e&&(e=>{o.text!==e.targetBody&&(q(e.targetBody),e.streamingPublishCount+=1)})(e)})},200))},T=e=>{const t=m;t?(void 0!==t.publishTimer&&(clearTimeout(t.publishTimer),t.publishTimer=void 0),t.targetBody=e,o.text!==e&&(q(e),t.terminalPublishCount+=1),c.receivedSnapshotCount+=t.receivedSnapshotCount,c.streamingPublishCount+=t.streamingPublishCount,c.terminalPublishCount+=t.terminalPublishCount,m=void 0):q(e)},C=t=>{if(void 0!==t.partIndex)return;const r=_,s=S;e.stream.addPart(toMessagePart({type:"text",text:t.text,state:"done",_seq:s,data:x(s)})),t.partIndex=r,_+=1,S+=1},E=()=>{if(!d){d=!0;for(const e of g)C(e)}},P=e=>{const t={text:e};g.push(t),d&&C(t)},R=(t,r)=>{for(const s of t){const t=I.get(s.url);if(t){const r=mergeSearchSource(t,s);if(r.title===t.title&&r.snippet===t.snippet)continue;const n={...r,partIndex:t.partIndex,displaySeq:t.displaySeq,toolUseId:t.toolUseId};e.stream.replacePart(t.partIndex,toMessagePart({type:"source",url:n.url,...n.title?{title:n.title}:{},...n.snippet?{snippet:n.snippet}:{},tool_use_id:n.toolUseId,_seq:n.displaySeq,data:x(n.displaySeq)})),I.set(s.url,n);continue}const n={...s,partIndex:_,displaySeq:S,toolUseId:r};e.stream.addPart(toMessagePart({type:"source",url:s.url,...s.title?{title:s.title}:{},...s.snippet?{snippet:s.snippet}:{},tool_use_id:r,_seq:n.displaySeq,data:x(n.displaySeq)})),I.set(s.url,n),_+=1,S+=1,ae.source+=1}},M=(e,t,r,s)=>toMessagePart({type:"thinking",text:t,state:r,_seq:e.displaySeq,_started_at_ms:e.startedAtMs,...void 0!==s?{_duration_ms:s}:{},data:x(e.displaySeq,{started_at_ms:e.startedAtMs,...void 0!==s?{duration_ms:s}:{}})}),O=t=>{E();const r={partIndex:_,displaySeq:S,text:t,publishedText:t,receivedSnapshotCount:1,streamingPublishCount:1,startedAtMs:Date.now(),startedAtMonotonicMs:performance.now()};return e.stream.addPart(M(r,t,"streaming")),_+=1,S+=1,p=r,r},A=t=>{void 0===t.publishTimer&&(t.publishTimer=setTimeout(()=>{t.publishTimer=void 0,de("reasoning_snapshot_flush",()=>{p===t&&(t=>{t.publishedText!==t.text&&(e.stream.replacePart(t.partIndex,M(t,t.text,"streaming")),t.publishedText=t.text,t.streamingPublishCount+=1)})(t)})},200))},F=()=>{const r=p;if(!r)return;void 0!==r.publishTimer&&(clearTimeout(r.publishTimer),r.publishTimer=void 0);const s=Math.max(0,Math.round(performance.now()-r.startedAtMonotonicMs));e.stream.replacePart(r.partIndex,M(r,r.text,"done",s)),p=void 0,logChatLatency(e.logger,"claw.reasoning.settled",e.requestId,t,{reasoning_duration_ms:s,reasoning_chars:r.text.length,reasoning_snapshot_count:r.receivedSnapshotCount,reasoning_streaming_publish_count:r.streamingPublishCount,reasoning_coalesced_snapshot_count:Math.max(0,r.receivedSnapshotCount-r.streamingPublishCount),reasoning_publish_interval_ms:200})},N=e=>{const t=v.indexOf(e);t>=0&&v.splice(t,1)},U=e=>{const t=e?v.filter(t=>f.get(t)?.name===e):v;return 1===t.length?t[0]:void 0},j=e=>{const t=b.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:r,index:s}]=t;return b.splice(s,1),r},L=(e,t)=>{if(h.set(e,t),e===t)return;const r=y.get(e);if(!r)return;const s=D(t);W(s,r),y.delete(e);const n=f.get(t);n&&(Y(n,s),y.delete(t))},B=e=>h.get(e)??e,z=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),D=e=>{let t=y.get(e);return t||(t={},y.set(e,t)),t},Q=(e,t)=>{if(!e?.length&&!t?.length)return;const r=[],s=new Map;for(const n of[...e??[],...t??[]]){const e=s.get(n.url);void 0===e?(s.set(n.url,r.length),r.push(n)):r[e]=mergeSearchSource(r[e],n)}return r},W=(e,t)=>{e.name??=t.name,e.arguments??=t.arguments,t.toolStartObserved&&(e.toolStartObserved=!0),t.itemEventObserved&&(e.itemEventObserved=!0),t.progress?.length&&(e.progress=[...e.progress??[],...t.progress]),e.observedResult??=t.observedResult,e.sources=Q(e.sources,t.sources)},J=(t,r,s)=>{const n="tool"===t.name&&"tool"!==r?r:t.name,o=!z(t.arguments)&&z(s)?s:t.arguments;n===t.name&&o===t.arguments||(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:n,arguments:o,state:t.state,_seq:t.displaySeq,data:x(t.displaySeq)})),t.name=n,t.arguments=o)},H=(t,r,s,n)=>{const o=f.get(t);if(o)return J(o,r,s),n&&!v.includes(t)&&"streaming"===o.state&&v.push(t),o;E();const a=y.get(t),i="tool"!==r?r:a?.name&&"tool"!==a.name?a.name:r,l=z(s)?s:a?.arguments??s,d=((t,r,s)=>{const n=_,o=S;return e.stream.addPart(toMessagePart({type:"tool_use",id:t,name:r,arguments:s,state:"streaming",_seq:o,data:x(o)})),_+=1,S+=1,{partIndex:n,displaySeq:o}})(t,i,l),u={id:t,name:i,arguments:l,partIndex:d.partIndex,displaySeq:d.displaySeq,state:"streaming",itemEventObserved:!0===a?.itemEventObserved,toolStartObserved:!0===a?.toolStartObserved};return f.set(t,u),(n||u.toolStartObserved||u.itemEventObserved)&&(v.includes(t)||"streaming"!==u.state||v.push(t)),a&&Y(u,a),y.delete(t),u},K=(e,t)=>{const r=(e=>{const t=h.get(e);return t?f.get(t):f.get(e)})(e);if(r)return void 0===t.name&&void 0===t.arguments||J(r,t.name??r.name,t.arguments??r.arguments),t.toolStartObserved&&(r.toolStartObserved=!0),t.itemEventObserved&&(r.itemEventObserved=!0),(t.pending||t.toolStartObserved||t.itemEventObserved)&&!v.includes(r.id)&&"streaming"===r.state&&v.push(r.id),r;const s=D(e);void 0!==t.name&&(s.name=t.name),void 0!==t.arguments&&(s.arguments=t.arguments),t.toolStartObserved&&(s.toolStartObserved=!0),t.itemEventObserved&&(s.itemEventObserved=!0)},G=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,toMessagePart({type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done",_seq:t.displaySeq,data:x(t.displaySeq)})),t.state="done")},V=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(V).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${V(t)}`).join(",")}}`},X=(t,r,s)=>{const n=`${s?"1":"0"}:${V(r)}`;if(t.progressSignature===n)return;const o=toMessagePart({type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:r,...s?{is_error:!0}:{}}});if(void 0===t.progressPartIndex){const r=_;return e.stream.addPart(o),t.progressPartIndex=r,t.progressSignature=n,void(_+=1)}e.stream.replacePart(t.progressPartIndex,o),t.progressSignature=n},Y=(e,t)=>{if(t.progress?.length)for(const r of t.progress)X(e,r.content,r.isError);t.observedResult&&(X(e,t.observedResult.content,t.observedResult.isError),e.observedResult=t.observedResult),(t.progress?.length||t.observedResult)&&(G(e),N(e.id)),t.sources?.length&&R(t.sources,e.id)},Z=e=>[...new Set([e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e)).map(e=>e.trim()).filter(Boolean))],ee=(e,t=[])=>{const r=[...new Set([...Z(e),...t])];return isNonEmptyString(e.text)&&r.length>0?{text:e.text,media_urls:r}:isNonEmptyString(e.text)?e.text:1===r.length?r[0]:r.length>1?{media_urls:r}:!0===e.isError?"Tool execution failed.":""},te=t=>{for(const r of t)e.stream.addPart(toMessagePart({type:"resource_link",uri:r.uri,name:r.name,...r.mimeType?{mimeType:r.mimeType}:{}})),_+=1},re=t=>{k.add(t);const r=()=>{k.delete(t)};e.onCallbackFailure?t.then(r,r):t.finally(r)},se=(t,r)=>{if(t.durableUploadFilesEmitted)return;e.logger.info?.(`[im-sink] schedule tool artifact enrichment requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`);const s=pickRawResultFromPayload(r),n=(async()=>{let n=[];try{n=await resolveToolArtifactLinks({toolCallId:t.id,toolName:t.name,rawResult:s,rawText:"string"==typeof r.text?r.text:void 0,params:asToolParams(t.arguments),cwd:sessionWorkspaceDir(e.sessionKey),payloadMediaUrls:Z(r)})}catch(r){const s=r instanceof Error?r.message:String(r);if(e.logger.warn?.(`[im-sink] resolveToolArtifactLinks failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure)throw r;return}0!==n.length?(e.logger.info?.(`[im-sink] tool artifact enrichment resolved ${n.length} link(s) requestId=${e.requestId} toolUseId=${t.id}`),await new Promise((s,o)=>{!1===e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;const s=n.map(e=>e.uri);t.artifactMediaUrls=s;const o=t.observedResult;X(t,ee(o?{text:o.content,isError:o.isError}:r,s),o?.isError??!0===r.isError),((t,r,s)=>{0===r.length||t.durableUploadFilesEmitted||(t.durableUploadFilesEmitted=!0,e.stream.appendToolResult(t.id,r.map(e=>({type:"resource_link",uri:e.uri,name:e.name,...e.mimeType?{mimeType:e.mimeType}:{}})),s),_+=1,te(r))})(t,n,o?.isError??!0===r.isError)}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool artifact enrichment apply failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure&&o(r)}finally{s()}})&&e.onCallbackFailure&&e.callbackQueue.waitForIdle().then(s,o)})):e.logger.debug?.(`[im-sink] tool artifact enrichment found no links requestId=${e.requestId} toolUseId=${t.id} name=${t.name}`)})().catch(r=>{const s=r instanceof Error?r.message:String(r);if(e.logger.warn?.(`[im-sink] tool artifact enrichment failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`),e.onCallbackFailure?.(r),e.onCallbackFailure)throw r});re(n)},ne=(e,t,r)=>{const s=h.get(e);if(s)return K(s,{name:t,arguments:r});const n=K(e,{name:t,arguments:r});if(n)return n;const o=U(t);if(o)return L(e,o),K(o,{name:t,arguments:r});K(e,{name:t,arguments:r})},oe=(e,t)=>{const r=h.get(e);if(r)return K(r,{name:t,itemEventObserved:!0,pending:!0});const s=K(e,{name:t,itemEventObserved:!0,pending:!0});if(s)return s;const n=(e=>{const t=v.filter(t=>{const r=f.get(t);return!(void 0===r||e&&r.name!==e||r.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(n)return L(e,n),K(n,{name:t,itemEventObserved:!0,pending:!0});const o=j(t);K(e,{name:t,arguments:o?.arguments,itemEventObserved:!0,pending:!0})},ae={tool:0,block:0,final:0,source:0};let ie,le=!1;const de=(t,r)=>{e.callbackQueue.enqueueSync(()=>{if((t=>e.abortSignal.aborted?(e.logger.debug?.(`[im-sink] ${t} ignored after abort requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):e.stream.isTerminated?(e.logger.debug?.(`[im-sink] ${t} ignored after stream_terminated requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1):!le||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{r()}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${s}`),e.onCallbackFailure?.(r)}})};return{dispatcher:{sendToolResult:t=>(de("tool_progress",()=>{const r=resolveRuntimeToolCallId(t),s=resolveRuntimeToolName(t),n=resolveRuntimeToolArguments(t),o=ee(t),a=!0===t.isError;let i;if(r){if(i=ne(r,s,n),!i){const e=D(r);return s&&(e.name=s),z(n)&&(e.arguments=n),e.progress=[...e.progress??[],{content:o,isError:a}],void(ae.tool+=1)}}else{const t=s?U(s):U();if(!t)return j(s),void e.logger.debug?.(`[im-sink] unattributed tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${s??"unknown"} pending=${v.length}`);i=K(t,{name:s,arguments:n})}i&&(ae.tool+=1,i.observedResult||X(i,o,a),G(i),N(i.id),se(i,t))}),!0),sendBlockReply:e=>(de("block_reply",()=>{ae.block+=1,isNonEmptyString(e.text)&&(ie=!0!==e.isError?void 0:e.text)}),!0),sendFinalReply:t=>(de("final_reply",()=>{var r;(ae.final+=1,!0!==t.isError)?((t=>{const r=Z(t).filter(e=>!$.has(e));if(0===r.length)return;for(const e of r)$.add(e);const s=(async()=>{let t=[];try{t=await resolveToolArtifactLinks({toolCallId:`final:${e.runId??e.requestId}`,toolName:"final_reply_media",payloadMediaUrls:r})}catch(t){for(const e of r)$.delete(e);if(e.logger.warn?.(`[im-sink] final media enrichment failed requestId=${e.requestId} error=${t instanceof Error?t.message:String(t)}`),e.onCallbackFailure?.(t),e.onCallbackFailure)throw t;return}if(0!==t.length)await new Promise((r,s)=>{!1===e.callbackQueue.enqueueSync(()=>{try{if(e.abortSignal.aborted||e.stream.isTerminated)return;te(t)}catch(t){e.logger.warn?.(`[im-sink] final media enrichment apply failed requestId=${e.requestId} error=${t instanceof Error?t.message:String(t)}`),e.onCallbackFailure?.(t),e.onCallbackFailure&&s(t)}finally{r()}})&&e.onCallbackFailure&&e.callbackQueue.waitForIdle().then(r,s)});else for(const e of r)$.delete(e)})();re(s)})(t),isNonEmptyString(t.text)&&(ie=void 0,l||(r=t.text,i="",a+=r,T(a),P(r),l=!0))):ie=t.text}),!0),waitForIdle:async()=>{for(let t=0;(e.onCallbackFailure||t<16)&&(await e.callbackQueue.waitForIdle(),0!==k.size);t++)e.onCallbackFailure?await Promise.all([...k]):await Promise.allSettled([...k]);await e.callbackQueue.waitForIdle();const t=takeFinalizeAttachments(e.sessionKey);t.length>0&&!e.abortSignal.aborted&&!e.stream.isTerminated&&te(t)},getQueuedCounts:()=>({tool:ae.tool,block:ae.block,final:ae.final}),getAccumulatedBody:()=>o.text,getTerminalErrorText:()=>ie,markComplete:r=>{F(),(b.length>0||y.size>0)&&e.logger.debug?.(`[im-sink] dropping uncommitted lifecycle tool signals requestId=${e.requestId} legacy_hints=${b.length} buffered=${y.size}`),i="",T(a),c.receivedSnapshotCount>0&&logChatLatency(e.logger,"claw.text_preview.settled",e.requestId,t,{text_preview_snapshot_count:c.receivedSnapshotCount,text_preview_streaming_publish_count:c.streamingPublishCount,text_preview_terminal_publish_count:c.terminalPublishCount,text_preview_coalesced_snapshot_count:Math.max(0,c.receivedSnapshotCount-c.streamingPublishCount-c.terminalPublishCount),text_preview_publish_interval_ms:200});for(const t of f.values())try{"success"===r&&isUploadArtifactToolName(t.name)&&!t.durableUploadFilesEmitted&&se(t,{toolCallId:t.id,toolName:t.name}),"success"===r&&G(t),N(t.id)}catch(r){const s=r instanceof Error?r.message:String(r);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${t.id} error=${s}`)}b.length=0,y.clear(),le=!0}},replyOptions:{...e.runId?{runId:e.runId}:{},abortSignal:e.abortSignal,disableBlockStreaming:!0,onAgentRunStart:r=>{e.onAgentRunStart?.(r),logChatLatency(e.logger,"claw.runtime.run_started",e.requestId,t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${r} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:s=>{de("partial",()=>{"string"==typeof s.text&&(!r&&s.text.length>0&&(r=!0,logChatLatency(e.logger,"claw.model.first_delta",e.requestId,t)),i=s.text,(e=>{const t=m;if(!t){if(o.text===e)return;return q(e),void(m={targetBody:e,receivedSnapshotCount:1,streamingPublishCount:1,terminalPublishCount:0})}t.targetBody!==e&&(t.targetBody=e,t.receivedSnapshotCount+=1,w(t))})(a+i))})},onReasoningStream:e=>{de("reasoning",()=>{if(!isNonEmptyString(e.text))return;const t=e.text,r=p??O(t);t!==r.text&&(r.text.startsWith(t)||(t.startsWith(r.text)?(r.text=t,r.receivedSnapshotCount+=1,A(r)):(F(),O(t))))})},onReasoningEnd:()=>{de("reasoning_end",()=>{F()})},onAssistantMessageStart:()=>{de("assistant_message_start",()=>{i="",l=!1,T(a),e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)})},onToolStart:r=>{de("tool_start",()=>{const n="string"==typeof r.phase?r.phase:"start",o=resolveRuntimeToolName(r)??"tool",a=resolveRuntimeToolCallId(r),i=resolveRuntimeToolArguments(r);if("update"===n)return void(a&&K(B(a),{name:o,arguments:i,pending:!0}));if(a&&s.set(a,Date.now()),logChatLatency(e.logger,"claw.tool.start",e.requestId,t,{tool:o}),a){K(a,{name:o,arguments:i,toolStartObserved:!0,pending:!0});const e=U(o);return void(e&&e!==a&&!h.has(a)&&(L(a,e),K(e,{name:o,arguments:i,toolStartObserved:!0,pending:!0})))}const l=(d=o,v.map(e=>f.get(e)).filter(e=>void 0!==e&&(!d||e.name===d)&&e.itemEventObserved));var d;if(1===l.length)return void K(l[0].id,{name:o,arguments:i,toolStartObserved:!0,pending:!0});if(l.length>1)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${o} candidates=${l.length}`);const u=[...y.entries()].filter(([,e])=>!e.name||e.name===o||"tool"===e.name);1!==u.length?b.push({name:o,arguments:i}):K(u[0][0],{name:o,arguments:i,toolStartObserved:!0,pending:!0})})},onItemEvent:e=>{de("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const r="string"==typeof e.phase?e.phase.toLowerCase():"update",s=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),n=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);s&&n&&("update"!==r?oe(s,n):K(B(s),{name:n,itemEventObserved:!0,pending:!0}))})}},commitAssistantMessage:e=>{de("assistant_message_commit",()=>{if(u.has(e.key))return;u.add(e.key),l=!0,ie=void 0,i="";const t=e.segments.filter(e=>"text"===e.type).map(e=>e.text).join("");a+=t,T(a);for(const t of e.segments)"text"!==t.type?H(t.id,t.name,t.arguments,!0):P(t.text)})},observeToolResult:r=>{de("tool_result_observation",()=>{const o=r.toolCallId.trim(),a=r.toolName.trim();if(!o||!a)return;const i=!n.has(o);if(i){n.add(o);const i=s.get(o);logChatLatency(e.logger,"claw.tool.result",e.requestId,t,{tool:a,tool_elapsed_ms:void 0===i?void 0:Math.max(0,Date.now()-i),outcome:r.isError?"error":"success",...r.isError?{error:summarizeObservedToolError(r.rawResult)??"error"}:{}})}if(e.persistObservedToolResults&&i){const e=observedToolResultText(r.rawResult);if(void 0!==e){const t=ne(o,a);if(t)X(t,ee({text:e,isError:r.isError},t.artifactMediaUrls),r.isError),t.observedResult={content:e,isError:r.isError},G(t),N(t.id);else{const t=D(B(o));t.name??=a,t.observedResult={content:e,isError:r.isError}}}}const l=searchSourceProvider(r.rawResult)??"unknown";let d=0;try{const t=extractSearchSources({toolName:a,rawResult:r.rawResult,isError:r.isError});if(d=t.length,0===d)return;const s=ne(o,a);if(s)R(t,s.id);else{const e=D(B(o));e.name??=a,e.sources=Q(e.sources,t)}e.logger.debug?.(`[im-sink] search source projection requestId=${e.requestId} tool=${a} provider=${l} source_count=${d}`)}catch{e.logger.warn?.(`[im-sink] search source projection ignored requestId=${e.requestId} tool=${a} provider=${l} source_count=${d}`)}})},getSourceCount:()=>ae.source,approvalParts:{add(t){if(e.stream.isTerminated)return;const r=_;return e.stream.addPart(t),_+=1,r},replace(t,r){e.stream.isTerminated||e.stream.replacePart(t,r)}}}}
@@ -1 +1 @@
1
- import{isCloudRuntimeSubject,RUNTIME_HASH_DOMAINS,runtimeCloudCallerIntentPayload,runtimeCloudIdempotencyFingerprintPayload}from"@beeos-ai/beeos-types/runtime";import{runtimeMessageHash,semanticEqual}from"./runtime-message-channel-contract.js";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{isDeepStrictEqual}from"node:util";const verifiedPrompts=new WeakMap;export function retainedVerifiedNativePrompt(e){if(!e||"object"!=typeof e)return null;const t=verifiedPrompts.get(e);return t&&isDeepStrictEqual(e,t)?structuredClone(t):null}const object=e=>!!e&&"object"==typeof e&&!Array.isArray(e),nonempty=e=>"string"==typeof e&&!!e,digest=e=>"string"==typeof e&&/^[0-9a-f]{64}$/.test(e),decimal=e=>"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e),exact=(e,t)=>Object.keys(e).sort().join("\0")===t.sort().join("\0"),fail=()=>{throw new RuntimeCommandError("INVALID_PARAMS","native prompt signed binding invalid")},validReasoning=e=>object(e)&&exact(e,["modelId","reasoningOverrideId"])&&nonempty(e.modelId)&&(null===e.reasoningOverrideId||nonempty(e.reasoningOverrideId));export function verifyNativePromptAuthority(e){const t=e.request,a=e.decoded?.claims;if(!(object(t)&&object(a)&&object(t.request)&&object(t.command)&&object(t.command.context)&&object(t.command.params)&&object(t.request.params)))return fail();const n=t.request.params,r=t.command.context,o=n.runtimeLease;if(!(exact(t,["schemaVersion","authorityRealm","cloudSubject","operationId","requestMessageId","requestHash","paramsHash","commandHash","idempotencyKeyHash","idempotencyFingerprint","callerIntentHash","instanceId","leaseId","runtimeHandlerIdentity","dispatchEpoch","executionGrantHash","claimDeadlineAt","executionDeadlineAt","createdAt","method","request","command","callerExternalUserId"])&&exact(t.request,["jsonrpc","id","method","params"])&&exact(t.command,["context","name","params"])&&exact(n,["runtimeRunId","parts","conversationId","platformAgentId","taskId","taskRequestMessageId","replyMessageId","inputFingerprint","runtimeLease",...["mentions","mode","modelOverrideId","reasoning"].filter(e=>void 0!==n[e])])&&object(o)&&exact(o,["runtimeEpoch","leaseId","journalStoreId","journalGeneration"])&&2===t.schemaVersion&&"cloud"===t.authorityRealm&&"session/prompt"===t.method&&t.request.method===t.method&&"2.0"===t.request.jsonrpc&&("string"==typeof t.request.id||"number"==typeof t.request.id)&&"session.prompt"===t.command.name&&isCloudRuntimeSubject(t.cloudSubject)&&isCloudRuntimeSubject(a.cloudSubject)&&semanticEqual(t.cloudSubject,a.cloudSubject)&&t.cloudSubject.instanceId===e.lease.instanceId&&t.instanceId===e.lease.instanceId&&t.runtimeHandlerIdentity===e.lease.handlerIdentity&&t.leaseId===e.lease.leaseId&&decimal(t.dispatchEpoch)&&decimal(o.runtimeEpoch)&&decimal(o.journalGeneration)&&o.runtimeEpoch===e.lease.runtimeEpoch&&o.leaseId===e.lease.leaseId&&o.journalStoreId===e.lineage.journalStoreId&&o.journalGeneration===e.lineage.journalGeneration&&nonempty(t.callerExternalUserId)&&nonempty(t.requestMessageId)&&t.requestMessageId!==n.taskRequestMessageId&&n.replyMessageId!==n.taskRequestMessageId&&[n.conversationId,n.platformAgentId,n.taskRequestMessageId,n.replyMessageId].every(nonempty)&&digest(n.inputFingerprint)&&n.taskId===t.operationId&&n.runtimeRunId===t.operationId&&"string"==typeof n.taskId&&/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(n.taskId)&&Array.isArray(n.parts)&&(void 0===n.mode||"string"==typeof n.mode)&&(void 0===n.modelOverrideId||"string"==typeof n.modelOverrideId)&&(void 0===n.reasoning||validReasoning(n.reasoning))&&(void 0===n.mentions||Array.isArray(n.mentions))&&"conversation"===r.scope&&r.operationId===t.operationId&&r.dispatchEpoch===t.dispatchEpoch&&r.runtimeInstanceUid===t.instanceId&&r.paramsHash===t.paramsHash&&r.idempotencyKeyHash===t.idempotencyKeyHash&&r.claimDeadline===t.claimDeadlineAt&&r.executionDeadline===t.executionDeadlineAt&&decimal(r.historyGeneration)&&object(r.conversation)&&r.conversation.runtimeConversationUid===n.conversationId&&r.conversation.runtimeSessionKey===n.conversationId&&[t.requestHash,t.paramsHash,t.commandHash,t.callerIntentHash,t.idempotencyFingerprint,t.idempotencyKeyHash,t.executionGrantHash].every(digest)))return fail();const i={runtimeRunId:n.runtimeRunId,parts:n.parts,...void 0===n.mentions?{}:{mentions:n.mentions},...void 0===n.mode?{}:{mode:n.mode},...void 0===n.modelOverrideId?{}:{modelOverrideId:n.modelOverrideId},...void 0===n.reasoning?{}:{reasoning:n.reasoning}},s=e.now??Date.now();if(![t.createdAt,t.claimDeadlineAt,t.executionDeadlineAt].every(e=>"string"==typeof e&&Number.isFinite(Date.parse(e)))||Date.parse(t.createdAt)>Date.parse(t.claimDeadlineAt)||Date.parse(t.claimDeadlineAt)>Date.parse(t.executionDeadlineAt)||!e.allowExpiredReadOnly&&Date.parse(t.claimDeadlineAt)<=s||!e.allowExpiredReadOnly&&Date.parse(t.executionDeadlineAt)<=s||!semanticEqual(i,t.command.params)||runtimeMessageHash(RUNTIME_HASH_DOMAINS.request,t.request)!==t.requestHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.params,{method:t.method,params:n})!==t.paramsHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.command,{command:t.command})!==t.commandHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.cloudCallerIntent,runtimeCloudCallerIntentPayload(t.cloudSubject,"session/prompt",n,t.callerExternalUserId))!==t.callerIntentHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.cloudIdempotencyFingerprint,runtimeCloudIdempotencyFingerprintPayload(t.cloudSubject,"session/prompt",t.callerIntentHash))!==t.idempotencyFingerprint||e.decoded?.compactHash!==t.executionGrantHash)return fail();const d=Math.floor(s/1e3);if(!exact(a,["iss","aud","purpose","sub","jti","iat","exp","authorityRealm","cloudSubject","operationId","requestMessageId","instanceId","leaseId","handlerIdentity","dispatchEpoch","requestHash","paramsHash","commandHash","idempotencyKeyHash","executionMode","method","command","scope","callerExternalUserId","platformAgentId","runtimeAgentUid","platformConversationId","historyGeneration"])||"beeos-cloud-gateway"!==a.iss||"beeos-cloud-runtime-command"!==a.aud||"cloud-runtime-execution-grant"!==a.purpose||"cloud"!==a.authorityRealm||"start"!==a.executionMode||"conversation"!==a.scope||!nonempty(a.jti)||!nonempty(a.runtimeAgentUid)||!Number.isSafeInteger(a.iat)||!Number.isSafeInteger(a.exp)||a.iat>d||!e.allowExpiredReadOnly&&a.exp<=d||a.exp<=a.iat||a.exp-a.iat>60||a.sub!==e.lease.handlerIdentity||a.handlerIdentity!==e.lease.handlerIdentity||a.platformAgentId!==n.platformAgentId||a.platformConversationId!==n.conversationId||a.historyGeneration!==r.historyGeneration||!semanticEqual(a.command,t.command)||!semanticEqual(a.cloudSubject,t.cloudSubject)||["operationId","requestMessageId","instanceId","leaseId","dispatchEpoch","requestHash","paramsHash","commandHash","idempotencyKeyHash","callerExternalUserId","method"].some(e=>a[e]!==t[e]))return fail();const m=structuredClone(t),c={historyGeneration:r.historyGeneration,runtimeAgentUid:a.runtimeAgentUid,request:m,params:m.request.params,command:m.command};return verifiedPrompts.set(c,structuredClone(c)),c}
1
+ import{isCloudRuntimeSubject,RUNTIME_HASH_DOMAINS,runtimeCloudCallerIntentPayload,runtimeCloudIdempotencyFingerprintPayload}from"@beeos-ai/beeos-types/runtime";import{runtimeMessageHash,semanticEqual}from"./runtime-message-channel-contract.js";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{isDeepStrictEqual}from"node:util";const verifiedPrompts=new WeakMap;export function retainedVerifiedNativePrompt(e){if(!e||"object"!=typeof e)return null;const t=verifiedPrompts.get(e);return t&&isDeepStrictEqual(e,t)?structuredClone(t):null}const object=e=>!!e&&"object"==typeof e&&!Array.isArray(e),nonempty=e=>"string"==typeof e&&!!e,digest=e=>"string"==typeof e&&/^[0-9a-f]{64}$/.test(e),decimal=e=>"string"==typeof e&&/^(0|[1-9][0-9]*)$/.test(e),exact=(e,t)=>Object.keys(e).sort().join("\0")===t.sort().join("\0"),fail=()=>{throw new RuntimeCommandError("INVALID_PARAMS","native prompt signed binding invalid")},validReasoning=e=>object(e)&&exact(e,["modelId","reasoningOverrideId"])&&nonempty(e.modelId)&&(null===e.reasoningOverrideId||nonempty(e.reasoningOverrideId));export function verifyNativePromptAuthority(e){const t=e.request,a=e.decoded?.claims;if(!(object(t)&&object(a)&&object(t.request)&&object(t.command)&&object(t.command.context)&&object(t.command.params)&&object(t.request.params)))return fail();const n=t.request.params,o=t.command.context,r=n.runtimeLease;if(!(exact(t,["schemaVersion","authorityRealm","cloudSubject","operationId","requestMessageId","requestHash","paramsHash","commandHash","idempotencyKeyHash","idempotencyFingerprint","callerIntentHash","instanceId","leaseId","runtimeHandlerIdentity","dispatchEpoch","executionGrantHash","claimDeadlineAt","executionDeadlineAt","createdAt","method","request","command","callerExternalUserId"])&&exact(t.request,["jsonrpc","id","method","params"])&&exact(t.command,["context","name","params"])&&exact(n,["runtimeRunId","parts","conversationId","platformAgentId","taskId","taskRequestMessageId","replyMessageId","inputFingerprint","runtimeLease",...["mentions","mode","modelOverrideId","reasoning","approvalMode"].filter(e=>void 0!==n[e])])&&object(r)&&exact(r,["runtimeEpoch","leaseId","journalStoreId","journalGeneration"])&&2===t.schemaVersion&&"cloud"===t.authorityRealm&&"session/prompt"===t.method&&t.request.method===t.method&&"2.0"===t.request.jsonrpc&&("string"==typeof t.request.id||"number"==typeof t.request.id)&&"session.prompt"===t.command.name&&isCloudRuntimeSubject(t.cloudSubject)&&isCloudRuntimeSubject(a.cloudSubject)&&semanticEqual(t.cloudSubject,a.cloudSubject)&&t.cloudSubject.instanceId===e.lease.instanceId&&t.instanceId===e.lease.instanceId&&t.runtimeHandlerIdentity===e.lease.handlerIdentity&&t.leaseId===e.lease.leaseId&&decimal(t.dispatchEpoch)&&decimal(r.runtimeEpoch)&&decimal(r.journalGeneration)&&r.runtimeEpoch===e.lease.runtimeEpoch&&r.leaseId===e.lease.leaseId&&r.journalStoreId===e.lineage.journalStoreId&&r.journalGeneration===e.lineage.journalGeneration&&nonempty(t.callerExternalUserId)&&nonempty(t.requestMessageId)&&t.requestMessageId!==n.taskRequestMessageId&&n.replyMessageId!==n.taskRequestMessageId&&[n.conversationId,n.platformAgentId,n.taskRequestMessageId,n.replyMessageId].every(nonempty)&&digest(n.inputFingerprint)&&n.taskId===t.operationId&&n.runtimeRunId===t.operationId&&"string"==typeof n.taskId&&/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(n.taskId)&&Array.isArray(n.parts)&&(void 0===n.mode||"string"==typeof n.mode)&&(void 0===n.modelOverrideId||"string"==typeof n.modelOverrideId)&&(void 0===n.reasoning||validReasoning(n.reasoning))&&(void 0===n.approvalMode||"string"==typeof n.approvalMode)&&(void 0===n.mentions||Array.isArray(n.mentions))&&"conversation"===o.scope&&o.operationId===t.operationId&&o.dispatchEpoch===t.dispatchEpoch&&o.runtimeInstanceUid===t.instanceId&&o.paramsHash===t.paramsHash&&o.idempotencyKeyHash===t.idempotencyKeyHash&&o.claimDeadline===t.claimDeadlineAt&&o.executionDeadline===t.executionDeadlineAt&&decimal(o.historyGeneration)&&object(o.conversation)&&o.conversation.runtimeConversationUid===n.conversationId&&o.conversation.runtimeSessionKey===n.conversationId&&[t.requestHash,t.paramsHash,t.commandHash,t.callerIntentHash,t.idempotencyFingerprint,t.idempotencyKeyHash,t.executionGrantHash].every(digest)))return fail();const i={runtimeRunId:n.runtimeRunId,parts:n.parts,...void 0===n.mentions?{}:{mentions:n.mentions},...void 0===n.mode?{}:{mode:n.mode},...void 0===n.modelOverrideId?{}:{modelOverrideId:n.modelOverrideId},...void 0===n.reasoning?{}:{reasoning:n.reasoning},...void 0===n.approvalMode?{}:{approvalMode:n.approvalMode}},s=e.now??Date.now();if(![t.createdAt,t.claimDeadlineAt,t.executionDeadlineAt].every(e=>"string"==typeof e&&Number.isFinite(Date.parse(e)))||Date.parse(t.createdAt)>Date.parse(t.claimDeadlineAt)||Date.parse(t.claimDeadlineAt)>Date.parse(t.executionDeadlineAt)||!e.allowExpiredReadOnly&&Date.parse(t.claimDeadlineAt)<=s||!e.allowExpiredReadOnly&&Date.parse(t.executionDeadlineAt)<=s||!semanticEqual(i,t.command.params)||runtimeMessageHash(RUNTIME_HASH_DOMAINS.request,t.request)!==t.requestHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.params,{method:t.method,params:n})!==t.paramsHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.command,{command:t.command})!==t.commandHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.cloudCallerIntent,runtimeCloudCallerIntentPayload(t.cloudSubject,"session/prompt",n,t.callerExternalUserId))!==t.callerIntentHash||runtimeMessageHash(RUNTIME_HASH_DOMAINS.cloudIdempotencyFingerprint,runtimeCloudIdempotencyFingerprintPayload(t.cloudSubject,"session/prompt",t.callerIntentHash))!==t.idempotencyFingerprint||e.decoded?.compactHash!==t.executionGrantHash)return fail();const d=Math.floor(s/1e3);if(!exact(a,["iss","aud","purpose","sub","jti","iat","exp","authorityRealm","cloudSubject","operationId","requestMessageId","instanceId","leaseId","handlerIdentity","dispatchEpoch","requestHash","paramsHash","commandHash","idempotencyKeyHash","executionMode","method","command","scope","callerExternalUserId","platformAgentId","runtimeAgentUid","platformConversationId","historyGeneration"])||"beeos-cloud-gateway"!==a.iss||"beeos-cloud-runtime-command"!==a.aud||"cloud-runtime-execution-grant"!==a.purpose||"cloud"!==a.authorityRealm||"start"!==a.executionMode||"conversation"!==a.scope||!nonempty(a.jti)||!nonempty(a.runtimeAgentUid)||!Number.isSafeInteger(a.iat)||!Number.isSafeInteger(a.exp)||a.iat>d||!e.allowExpiredReadOnly&&a.exp<=d||a.exp<=a.iat||a.exp-a.iat>60||a.sub!==e.lease.handlerIdentity||a.handlerIdentity!==e.lease.handlerIdentity||a.platformAgentId!==n.platformAgentId||a.platformConversationId!==n.conversationId||a.historyGeneration!==o.historyGeneration||!semanticEqual(a.command,t.command)||!semanticEqual(a.cloudSubject,t.cloudSubject)||["operationId","requestMessageId","instanceId","leaseId","dispatchEpoch","requestHash","paramsHash","commandHash","idempotencyKeyHash","callerExternalUserId","method"].some(e=>a[e]!==t[e]))return fail();const m=structuredClone(t),c={historyGeneration:o.historyGeneration,runtimeAgentUid:a.runtimeAgentUid,request:m,params:m.request.params,command:m.command};return verifiedPrompts.set(c,structuredClone(c)),c}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{createSessionModelRefs}from"./openclaw-session-model.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;modelMutations;constructor(e,t,i,s){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i,this.modelMutations=s}modelRefs(){return createSessionModelRefs()}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{sessionKey:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s=openClawSessionModePatch(t),n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const a=this.modelRefs().toPlatform(o),r=a.indexOf("/");return{modelId:a,...r>0?{modelProvider:a.slice(0,r)}:{}}}async setModel(e,t,i,s){const n=null===t?null:this.modelRefs().toOpenClaw(t),o=s?.attempt.operationId;let a=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s));const r=await(this.modelMutations?.acquireInternal({sessionKey:a.key,label:"session model mutation"}));try{if(r){const t=await this.resolveSetModelKey(e,s);if(t.key!==r.sessionKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model target changed while waiting for its physical session gate");a=t}const l=async()=>await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:a.key,model:n,mustExist:!a.materialized}));let d;try{d=await l()}catch(t){if(a.materialized||!isMissingOpenClawStoreSession(t,a.key))throw t;a=await modelSwitchTimingSpan(o,"materialize_set_model_key",async()=>await this.materializeSetModelKey(e,a.key,s)),d=await l()}const{key:c,materialized:m}=a,u=resolveModelPatch(d,c,t,this.modelRefs()),h=u.effectiveModelId;if(null===t&&(!i||h!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(m){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(c))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(c)}return u}finally{this.modelMutations?.releaseBeforeRuntime(r)}}async prepareNativeModel(e,t,i,s){await i();const n=void 0===t?null:this.modelRefs().toOpenClaw(t),o=void 0===s?void 0:openClawSessionModePatch(s),a=await this.gateway().request("sessions.patch",{key:e,model:n,...o});await i();const r=a;if(!0!==r?.ok||r.key!==e)throw new RuntimeCommandError("OUTCOME_UNKNOWN","native model patch did not acknowledge the exact qualified session");if(o&&!isAuthoritativeSetModeAck(a,e,o))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return resolveModelPatch(a,e,t??null,this.modelRefs())}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}return i?{key:i,materialized:!1}:await this.materializeSetModelKey(e,void 0,t)}async materializeSetModelKey(e,t,i){if(!("service"===i?.attempt.origin&&void 0!==i.attempt.verifiedMessageGrant&&i&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${t??e}`)}const s=await this.liveSessionKeys.materializeMissingModelKey(e,i);if(t&&s!==t)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:s,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function isMissingOpenClawStoreSession(e,t){return e instanceof Error&&e.message===`No session found: ${t}`}function resolveModelPatch(e,t,i,s){const n=null===i?null:s.toOpenClaw(i);if(!isAuthoritativeSetModelAck(e,t,n,i))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const o=e.resolved.model,a=s.toPlatform(o);if(null!==i&&a!==i&&o!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");return{...null===i?{}:{modelOverrideId:i},effectiveModelId:a,...e.resolved.modelProvider?{modelProvider:e.resolved.modelProvider}:{}}}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}export function openClawSessionModePatch(e){const t="verbose"===e||"detailed"===e?{verboseLevel:"full"}:"reasoning"===e||"thinking"===e?{reasoningLevel:"stream"}:"concise"===e||"default"===e?{verboseLevel:"default"}:null;if(!t)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${e}`);return t}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const a=o.model;return"string"==typeof a&&0!==a.length&&a.trim()===a&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}
1
+ import{randomUUID}from"node:crypto";import{performance}from"node:perf_hooks";import{RuntimeCommandError}from"./runtime-command-dispatcher.js";import{ClearConversationUseCase}from"./conversation-clear-use-case.js";import{createSessionModelRefs}from"./openclaw-session-model.js";import{openClawSessionKeysFromList}from"./openclaw-session-key-resolution.js";async function modelSwitchTimingSpan(e,t,i){const s=performance.now();let n="succeeded";try{return await i()}catch(e){throw n="failed",e}finally{e&&process.stderr.write(`[MODEL_SWITCH_TIMING] component=runtime operationId=${e} stage=${t} outcome=${n} durationMs=${(performance.now()-s).toFixed(1)}\n`)}}export class RuntimeSessionControlService{clearRuntime;fallback;activeRuns;constructor(e,t,i){this.clearRuntime=e,this.fallback=t,this.activeRuns=i}async execute(e,t,i){const s=e;if("session.clear"===s.name){if(!i.historyBoundary)throw new RuntimeCommandError("INVALID_PARAMS",`${i.origin} clear attempt lacks its history boundary`);const e=s.context.conversation;return await new ClearConversationUseCase(i.historyBoundary,this.clearRuntime).execute({operationId:s.context.operationId,conversationId:e.runtimeConversationUid,runtimeSessionKey:e.runtimeSessionKey,runtimeEpoch:s.context.dispatchEpoch})}if("session.cancel"===s.name){if(!this.activeRuns)throw new RuntimeCommandError("INVALID_PARAMS","session.cancel active-run authority is not configured");const e=s;return await this.activeRuns.cancelExact({runtimeConversationUid:e.context.conversation.runtimeConversationUid,runtimeSessionKey:e.context.conversation.runtimeSessionKey,historyGeneration:e.context.historyGeneration,targetMessageId:e.params.targetMessageId}),{}}if(!this.fallback)throw new Error(`session command delegate is not configured: ${s.name}`);return await this.fallback.execute(s,t,i)}async projectCommitted(e,t,i){return await(this.fallback?.projectCommitted?.(e,t,i))}}export class OpenClawSessionMetadataService{metadata;constructor(e){this.metadata="function"==typeof e?new OpenClawGatewaySessionMetadataPort(e):e}async execute(e,t,i){const s=e,n=s.context.conversation.runtimeSessionKey,o={lease:t,attempt:i};switch(s.name){case"session.load":return await this.metadata.load(n,o);case"session.rename":{const e=s.params.title.slice(0,64);return await this.metadata.rename(n,e,o)}case"session.delete":return await this.metadata.delete(n,o);case"session.setMode":{const e=s.params.mode;return await this.metadata.setMode(n,e,o)}case"session.setModel":{const e=s.params.modelOverrideId;if(null!==e&&("string"!=typeof e||0===e.length||e.trim()!==e))throw new RuntimeCommandError("INVALID_PARAMS","modelOverrideId must be a non-empty string or null");const t=null===e?await modelSwitchTimingSpan(i.operationId,"resolve_agent_default_model",async()=>await this.metadata.resolveAgentDefaultModel(n,o)):void 0;if(null===e&&!t)throw new RuntimeCommandError("AGENT_DEFAULT_MODEL_NOT_CONFIGURED","agent default model is not configured");return await modelSwitchTimingSpan(i.operationId,"metadata_set_model",async()=>await this.metadata.setModel(n,e,t??void 0,o))}}throw new RuntimeCommandError("INVALID_PARAMS","unsupported session metadata command")}async projectCommitted(e,t,i){if("session.setModel"!==e.name)return;const s=e,n=t;if(this.metadata.projectCommittedSetModel&&await this.metadata.projectCommittedSetModel(s.context.conversation.runtimeSessionKey,n),"service"===i?.origin){if(!i.sessionModelProjection)throw new Error("Message Service session model projection is unavailable");return Object.freeze({state:"pending",revision:"0"})}if(!this.metadata.projectCommittedSetModel)throw new Error("session metadata projection is unavailable")}}export class OpenClawGatewaySessionMetadataPort{getGateway;projectModel;liveSessionKeys;modelMutations;constructor(e,t,i,s){this.getGateway=e,this.projectModel=t,this.liveSessionKeys=i,this.modelMutations=s}modelRefs(){return createSessionModelRefs()}async load(e,t){const i=await this.requireExistingOpenClawKey(e,t);return await this.gateway().request("sessions.resolve",{sessionKey:i}),{loaded:!0}}async rename(e,t,i){const s=await this.requireExistingOpenClawStoreKey(e,i);return await this.gateway().request("sessions.patch",{key:s,label:t}),{title:t}}async delete(e,t){const i=await this.requireExistingOpenClawStoreKey(e,t);return await this.gateway().request("sessions.delete",{key:i}),{deleted:!0}}async setMode(e,t,i){const s=openClawSessionModePatch(t),n=await this.requireExistingOpenClawStoreKey(e,i);if(!isAuthoritativeSetModeAck(await this.gateway().request("sessions.patch",{key:n,...s}),n,s))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");return{mode:t}}async resolveAgentDefaultModel(e,t){const i=runtimeAgentId(this.liveSessionKeys?(await this.resolveSetModelKey(e,t)).key:await this.requireExistingOpenClawKey(e,t)),s=await this.gateway().request("agents.list",{}),n=s?.agents?.find(e=>e.id===i),o=n?.defaultModel??n?.model;if("string"!=typeof o||!o.trim()||o.trim()!==o)return null;const r=this.modelRefs().toPlatform(o),a=r.indexOf("/");return{modelId:r,...a>0?{modelProvider:r.slice(0,a)}:{}}}async setModel(e,t,i,s){const n=null===t?null:this.modelRefs().toOpenClaw(t),o=s?.attempt.operationId;let r=await modelSwitchTimingSpan(o,"resolve_set_model_key",async()=>await this.resolveSetModelKey(e,s));const a=await(this.modelMutations?.acquireInternal({sessionKey:r.key,label:"session model mutation"}));try{if(a){const t=await this.resolveSetModelKey(e,s);if(t.key!==a.sessionKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model target changed while waiting for its physical session gate");r=t}const l=async()=>await modelSwitchTimingSpan(o,"openclaw_sessions_patch",async()=>await this.gateway().request("sessions.patch",{key:r.key,model:n,mustExist:!r.materialized}));let d;try{d=await l()}catch(t){if(r.materialized||!isMissingOpenClawStoreSession(t,r.key))throw t;r=await modelSwitchTimingSpan(o,"materialize_set_model_key",async()=>await this.materializeSetModelKey(e,r.key,s)),d=await l()}const{key:c,materialized:m}=r,u=resolveModelPatch(d,c,t,this.modelRefs()),h=u.effectiveModelId;if(null===t&&(!i||h!==i.modelId))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the prevalidated agent default");if(m){const e=await modelSwitchTimingSpan(o,"openclaw_sessions_list_post_materialize",async()=>await this.gateway().request("sessions.list",{includeDerivedTitles:!0}));if(!openClawSessionKeysFromList(e).has(c))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch acknowledged a model-intent session that is absent from the authoritative store");if(!this.liveSessionKeys?.registerMaterializedModelKey)throw new RuntimeCommandError("OUTCOME_UNKNOWN","model-intent session registration authority is unavailable");this.liveSessionKeys.registerMaterializedModelKey(c)}return u}finally{this.modelMutations?.releaseBeforeRuntime(a)}}async prepareNativeModel(e,t,i,s,n){await i();const o=void 0===t?null:this.modelRefs().toOpenClaw(t),r=void 0===s?void 0:openClawSessionModePatch(s),a=void 0===n?void 0:openClawPermissionModePatch(n),l=await this.gateway().request("sessions.patch",{key:e,model:o,...r,...a});await i();const d=l;if(!0!==d?.ok||d.key!==e)throw new RuntimeCommandError("OUTCOME_UNKNOWN","native model patch did not acknowledge the exact qualified session");if(r&&!isAuthoritativeSetModeAck(l,e,r))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact target and effective mode");if(a&&!isAuthoritativePermissionModeAck(l,a))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the effective permission mode");return resolveModelPatch(l,e,t??null,this.modelRefs())}async projectCommittedSetModel(e,t){if(!this.projectModel)throw new Error("session metadata projection is unavailable");const i=await this.requireExistingOpenClawKey(e);await this.projectModel(i,t)}async requireExistingOpenClawKey(e,t){if(await(this.liveSessionKeys?.waitForHydration?.()),!this.liveSessionKeys)return e;try{return this.liveSessionKeys.resolveLiveKey(e)}catch(i){if(!t||!isMissingLiveSession(i)||!this.liveSessionKeys.reconcileMissingLiveKey)throw i;return await this.liveSessionKeys.reconcileMissingLiveKey(e,t),this.liveSessionKeys.resolveLiveKey(e)}}async requireExistingOpenClawStoreKey(e,t){const i=await this.requireExistingOpenClawKey(e,t),s=await this.gateway().request("sessions.list",{includeDerivedTitles:!0});if(!openClawSessionKeysFromList(s).has(i))throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${i}`);return i}async resolveSetModelKey(e,t){let i;const s="service"===t?.attempt.origin&&void 0!==t.attempt.verifiedMessageGrant;try{i=await this.requireExistingOpenClawKey(e,t)}catch(e){if(!(s&&t&&isMissingLiveSession(e)&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey))throw e}return i?{key:i,materialized:!1}:await this.materializeSetModelKey(e,void 0,t)}async materializeSetModelKey(e,t,i){if(!("service"===i?.attempt.origin&&void 0!==i.attempt.verifiedMessageGrant&&i&&this.liveSessionKeys?.materializeMissingModelKey&&this.liveSessionKeys.registerMaterializedModelKey)){throw new RuntimeCommandError("INVALID_PARAMS",`OpenClaw session key does not exist (refusing sessions.patch upsert): ${t??e}`)}const s=await this.liveSessionKeys.materializeMissingModelKey(e,i);if(t&&s!==t)throw new RuntimeCommandError("INVALID_PARAMS","verified model-intent key conflicts with the live local session");return{key:s,materialized:!0}}gateway(){const e=this.getGateway();if(!e?.isConnected)throw new RuntimeCommandError("GATEWAY_NOT_CONNECTED","Gateway not connected");return e}}function isMissingLiveSession(e){return e instanceof RuntimeCommandError&&"INVALID_PARAMS"===e.code&&e.message.startsWith("no live local session")}function isMissingOpenClawStoreSession(e,t){return e instanceof Error&&e.message===`No session found: ${t}`}function resolveModelPatch(e,t,i,s){const n=null===i?null:s.toOpenClaw(i);if(!isAuthoritativeSetModelAck(e,t,n,i))throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch did not acknowledge the exact existing session and effective model");const o=e.resolved.model,r=s.toPlatform(o);if(null!==i&&r!==i&&o!==n)throw new RuntimeCommandError("OUTCOME_UNKNOWN","sessions.patch effective model does not match the requested override");return{...null===i?{}:{modelOverrideId:i},effectiveModelId:r,...e.resolved.modelProvider?{modelProvider:e.resolved.modelProvider}:{}}}function runtimeAgentId(e){const t=e.split(":");return"agent"===t[0]&&t[1]?.trim()?t[1]:"main"}export function openClawSessionModePatch(e){const t="verbose"===e||"detailed"===e?{verboseLevel:"full"}:"reasoning"===e||"thinking"===e?{reasoningLevel:"stream"}:"concise"===e||"default"===e?{verboseLevel:"default"}:null;if(!t)throw new RuntimeCommandError("INVALID_PARAMS",`unsupported session mode: ${e}`);return t}export function openClawPermissionModePatch(e){return{permissionMode:"full"===e?"full":null}}function isAuthoritativePermissionModeAck(e,t){const i=e?.entry;return!(!i||"object"!=typeof i||Array.isArray(i))&&i.permissionMode===(t.permissionMode??void 0)}function isAuthoritativeSetModeAck(e,t,i){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const s=e;if(!0!==s.ok||s.key!==t||!s.entry||"object"!=typeof s.entry||Array.isArray(s.entry))return!1;const n=s.entry;return"verboseLevel"in i?n.verboseLevel===i.verboseLevel:n.reasoningLevel===i.reasoningLevel}function isAuthoritativeSetModelAck(e,t,i,s){if(!e||"object"!=typeof e||Array.isArray(e))return!1;const n=e;if("ok"in n&&!0!==n.ok)return!1;if("key"in n&&n.key!==t)return!1;const o=n.resolved;if(!o||"object"!=typeof o||Array.isArray(o))return!1;const r=o.model;return"string"==typeof r&&0!==r.length&&r.trim()===r&&(null!==s||null===i)}export class LocalConversationHistoryBoundary{onCommit;kind="local";intents=new Map;convergence=new Map;generations=new Map;constructor(e){this.onCommit=e}currentGeneration(e){return String(this.generations.get(e)??0n)}async prepare(e){const t=this.intents.get(e.operationId);if(t)return t;const i=(new Date).toISOString(),s=this.generations.get(e.conversationId)??0n,n={intentId:randomUUID(),operationId:e.operationId,conversationId:e.conversationId,state:"prepared",runtimeEpoch:e.runtimeEpoch,fromGeneration:String(s),toGeneration:String(s+1n),boundaryOffset:"0",version:"1",createdAt:i,updatedAt:i};return this.intents.set(e.operationId,n),n}async markResetting(e,t){return this.assertState(e,"prepared"),this.convergence.set(e.operationId,t),this.transition(e,"resetting")}async loadRunConvergence(e){const t=this.convergence.get(e.operationId);if(!t)throw new Error("local clear convergence receipt missing");return t}async markResetDone(e){return this.assertState(e,"resetting"),this.transition(e,"reset_done")}async commit(e){return this.assertState(e,"reset_done"),await(this.onCommit?.(e.conversationId)),this.generations.set(e.conversationId,BigInt(e.toGeneration)),this.transition(e,"committed")}async abortPrepared(e){this.assertState(e,"prepared"),this.transition(e,"aborted")}transition(e,t){const i={...e,state:t,version:String(BigInt(e.version)+1n),updatedAt:(new Date).toISOString()};return this.intents.set(e.operationId,i),i}assertState(e,t){const i=this.intents.get(e.operationId);if(!i||i.version!==e.version||i.state!==t)throw new Error(`local clear transition fenced: expected ${t}`)}}
@@ -1 +1 @@
1
- import{TERMINAL_MAX_FRAME_BYTES,TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5,DEFAULT_LEASE_RECOVERY_GRACE_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;handlerIdentity;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",(...i)=>{if(!this.acceptsEvent(t))return;if(this.connected)return;this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this);const n=i[0]?.handlerIdentity;n&&(this.handlerIdentity=n,this.service.closeParkedServiceSessions("runtime_lease_lost",n)),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("transport")}),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 r=e[0],s="string"==typeof r?r:Buffer.from(r).toString("utf8");this.handleWire(s,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",(...i)=>{if(this.acceptsEvent(t)){if(!0===i[0]?.recoverable)return this.invalidateConnection("park"),void e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals parked for recovery");this.invalidateConnection("fence"),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced")}}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection("park"),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch parked for resume"))})}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.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:e,wire:t,bytes: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 r=BigInt(i),s=r<(this.droppedThrough.get(t)??0n),a=[];let o=-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<=o)throw new Error("terminal replay sequence is not monotonic");o=n,n<=r||a.push({clientId:e,terminalId:t,frame:i.frame,wire:i.wire,bytes:i.bytes})}if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");for(const e of a)this.enqueueWithOutputBackpressure(e);return this.acknowledgeReplay(t,r),this.kickFlush(),{gap:s}}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 r=n.map(i=>{const n=Object.freeze({...i.frame,clientId:t}),r=JSON.stringify(n),s=Buffer.byteLength(r);return e+=s,{frame:n,wire:r,bytes:s}});this.rings.set(i,r),this.ringBytes.set(i,e)}for(let n=0;n<this.pending.length;n++){const r=this.pending[n];if(r.terminalId!==i||r.clientId!==e)continue;const s={...r.frame,clientId:t},a=JSON.stringify(s),o=Buffer.byteLength(a);this.pendingBytes+=o-r.bytes,this.pending[n]={clientId:t,terminalId:i,frame:s,wire:a,bytes:o}}if(this.activeSend?.item.terminalId===i&&this.activeSend.item.clientId===e){const e=this.activeSend.item,n={...e.frame,clientId:t},r=JSON.stringify(n),s=Buffer.byteLength(r);this.activeSend.counted&&(this.pendingBytes+=s-e.bytes),this.activeSend={item:{clientId:t,terminalId:i,frame:n,wire:r,bytes:s},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)),r=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)||r&&r.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const s=this.replayExpiryTimers.get(t);let a=!1;return()=>{if(a)return;a=!0,s&&this.replayExpiryTimers.get(t)===s&&(clearTimeout(s),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,r&&this.activeSend===r&&r.counted&&(r.counted=!1,this.pendingBytes-=r.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("fence");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,r;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{r=await this.authorityAdmission.admit(n),await this.service.handleClientFrame(r.frame,t,r.admission)}catch(e){if(this.acceptsTransport(i,t)){r&&this.sendRejectedFrame(r.frame,e);const t=e instanceof Error?e.message:String(e);this.options.logger?.warn?.(`[terminal-service] client frame rejected: ${t}`,e)}}}sendRejectedFrame(e,t){const i=(t instanceof Error?t.message:String(t)).toLowerCase(),n=t instanceof TerminalNotFoundError||i.includes("not found")?"TERMINAL_NOT_FOUND":i.includes("quota")?"TERMINAL_QUOTA_EXCEEDED":i.includes("unauthorized")||i.includes("authority")?"UNAUTHORIZED_TARGET":"TERMINAL_IO_ERROR",r=this.rings.get(e.terminalId),s="TERMINAL_NOT_FOUND"!==n&&this.options.manager.getSession(e.terminalId)?.sessionId===e.clientId&&(!r||r.every(t=>t.frame.clientId===e.clientId)),a=s?this.options.manager.getSessionIncarnation(e.terminalId):void 0,o=s||"terminal.resume"!==e.type?a?BigInt(this.options.manager.nextServerSequence(e.terminalId,a)):1n:BigInt(e.lastServerSequence)+1n,c={protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.error",clientId:e.clientId,terminalId:e.terminalId,sequence:String(o),code:n,message:n,recoverable:"TERMINAL_IO_ERROR"===n||"TERMINAL_QUOTA_EXCEEDED"===n};if(!s){const t=JSON.stringify(c);return this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:c,wire:t,bytes:Buffer.byteLength(t)}),void this.kickFlush()}this.send(c)}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let r=(this.ringBytes.get(e.terminalId)??0)+i;const s=this.options.maxBufferedFramesPerTerminal??512,a=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>s||r>a;){const t=n.shift();if(!t)break;r-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,r),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}enqueueWithOutputBackpressure(e){const t=this.options.maxPendingSendBytes??1048576;if(this.pendingBytes+e.bytes<=t)return this.pending.push(e),void(this.pendingBytes+=e.bytes);if("terminal.output"===e.frame.type){if(this.hasPendingTruncation(e.terminalId))return;const i=this.toTruncationMarker(e);return this.compactPendingOutput(i.bytes,t),void(this.hasPendingTruncation(e.terminalId)||(this.pending.push(i),this.pendingBytes+=i.bytes))}this.compactPendingOutput(e.bytes,t),this.pending.push(e),this.pendingBytes+=e.bytes}compactPendingOutput(e,t){for(;this.pendingBytes+e>t;){const e=this.pending.findIndex(e=>"terminal.output"===e.frame.type&&!0!==e.frame.truncated);if(e<0)return;const t=this.pending[e];if(this.hasPendingTruncation(t.terminalId,t)){this.pending.splice(e,1),this.pendingBytes-=t.bytes;continue}const i=this.toTruncationMarker(t);this.pending[e]=i,this.pendingBytes+=i.bytes-t.bytes}}hasPendingTruncation(e,t){return this.activeSend?.item!==t&&this.activeSend?.item.terminalId===e&&"terminal.output"===this.activeSend.item.frame.type&&!0===this.activeSend.item.frame.truncated||this.pending.some(i=>i!==t&&i.terminalId===e&&"terminal.output"===i.frame.type&&!0===i.frame.truncated)}toTruncationMarker(e){if("terminal.output"!==e.frame.type)throw new Error("only terminal output can be truncated");const t={...e.frame,data:"",encoding:"base64",truncated:!0},i=JSON.stringify(t);return{...e,frame:t,wire:i,bytes:Buffer.byteLength(i)}}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("transport"))}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;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;const i=this.serviceGeneration;switch(e){case"transport":return;case"park":i&&this.service.parkServiceSessions(i,this.handlerIdentity,this.options.leaseRecoveryGraceMs??3e5);break;case"fence":this.service.closeParkedServiceSessions("runtime_lease_lost"),i&&this.service.fenceServiceSessions("runtime_lease_lost",i);break;default:throw new Error(`unhandled connection invalidation ${String(e)}`)}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,r=this.droppedThrough.get(e)??0n,s=0;for(;s<i.length;){const e=i[s],a=BigInt(e.frame.sequence);if(a>t)break;n+=e.bytes,a>r&&(r=a),s++}0!==s&&(i.splice(0,s),this.ringBytes.set(e,Math.max(0,(this.ringBytes.get(e)??0)-n)),this.droppedThrough.set(e,r))}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,r)=>{let s=!1;const a=e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),n(e))},o=()=>a(interrupted),c=setTimeout(()=>a(interrupted),Math.max(0,t));c.unref?.(),i?.addEventListener("abort",o,{once:!0}),e.then(e=>a(e),e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),r(e))})})}
1
+ import{TERMINAL_MAX_FRAME_BYTES,TERMINAL_SERVICE_PROTOCOL_VERSION}from"@beeos-ai/beeos-types/runtime";import{TerminalNotFoundError}from"./session-manager.js";import{getOrCreateRuntimeTerminalService}from"./runtime-terminal-service.js";import{TerminalAuthorityAdmission}from"./terminal-authority-admission.js";const DEFAULT_REPLAY_RETENTION_MS=3e5,DEFAULT_LEASE_RECOVERY_GRACE_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;handlerIdentity;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",(...i)=>{if(!this.acceptsEvent(t))return;if(this.connected)return;this.transportEpoch++,this.activeFlush=void 0,this.serviceGeneration??=this.service.setOutbound(this);const n=i[0]?.handlerIdentity;n&&(this.handlerIdentity=n,this.service.closeParkedServiceSessions("runtime_lease_lost",n)),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("transport")}),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 r=e[0],s="string"==typeof r?r:Buffer.from(r).toString("utf8");this.handleWire(s,i,n)}),e.client.on("error",(...i)=>{this.acceptsEvent(t)&&e.logger?.warn?.("[terminal-service] outbound registration error",i[0])}),e.client.on("authorityLost",(...i)=>{if(this.acceptsEvent(t)){if(!0===i[0]?.recoverable)return this.invalidateConnection("park"),void e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals parked for recovery");this.invalidateConnection("fence"),e.logger?.warn?.("[terminal-service] runtime lease lost; Service terminals fenced")}}),e.client.on("transportEpochChanged",()=>{this.acceptsEvent(t)&&(this.invalidateConnection("park"),e.logger?.info?.("[terminal-service] runtime lease rotated; prior Terminal epoch parked for resume"))})}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.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:e,wire:t,bytes: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 r=BigInt(i),s=r<(this.droppedThrough.get(t)??0n),a=[];let o=-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<=o)throw new Error("terminal replay sequence is not monotonic");o=n,n<=r||a.push({clientId:e,terminalId:t,frame:i.frame,wire:i.wire,bytes:i.bytes})}if(!this.acceptsConnection(n))throw new Error("terminal outbound registration epoch changed");for(const e of a)this.enqueueWithOutputBackpressure(e);return this.kickFlush(),{gap:s}}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 r=n.map(i=>{const n=Object.freeze({...i.frame,clientId:t}),r=JSON.stringify(n),s=Buffer.byteLength(r);return e+=s,{frame:n,wire:r,bytes:s}});this.rings.set(i,r),this.ringBytes.set(i,e)}for(let n=0;n<this.pending.length;n++){const r=this.pending[n];if(r.terminalId!==i||r.clientId!==e)continue;const s={...r.frame,clientId:t},a=JSON.stringify(s),o=Buffer.byteLength(a);this.pendingBytes+=o-r.bytes,this.pending[n]={clientId:t,terminalId:i,frame:s,wire:a,bytes:o}}if(this.activeSend?.item.terminalId===i&&this.activeSend.item.clientId===e){const e=this.activeSend.item,n={...e.frame,clientId:t},r=JSON.stringify(n),s=Buffer.byteLength(r);this.activeSend.counted&&(this.pendingBytes+=s-e.bytes),this.activeSend={item:{clientId:t,terminalId:i,frame:n,wire:r,bytes:s},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)),r=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)||r&&r.item.clientId!==e)throw new Error("terminal forget ownership mismatch");const s=this.replayExpiryTimers.get(t);let a=!1;return()=>{if(a)return;a=!0,s&&this.replayExpiryTimers.get(t)===s&&(clearTimeout(s),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,r&&this.activeSend===r&&r.counted&&(r.counted=!1,this.pendingBytes-=r.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("fence");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,r;try{n=JSON.parse(e)}catch{return void this.options.logger?.warn?.("[terminal-service] invalid client JSON")}try{r=await this.authorityAdmission.admit(n),await this.service.handleClientFrame(r.frame,t,r.admission)}catch(e){if(this.acceptsTransport(i,t)){r&&this.sendRejectedFrame(r.frame,e);const t=e instanceof Error?e.message:String(e);this.options.logger?.warn?.(`[terminal-service] client frame rejected: ${t}`,e)}}}sendRejectedFrame(e,t){const i=(t instanceof Error?t.message:String(t)).toLowerCase(),n=t instanceof TerminalNotFoundError||i.includes("not found")?"TERMINAL_NOT_FOUND":i.includes("quota")?"TERMINAL_QUOTA_EXCEEDED":i.includes("unauthorized")||i.includes("authority")?"UNAUTHORIZED_TARGET":"TERMINAL_IO_ERROR",r=this.rings.get(e.terminalId),s="TERMINAL_NOT_FOUND"!==n&&this.options.manager.getSession(e.terminalId)?.sessionId===e.clientId&&(!r||r.every(t=>t.frame.clientId===e.clientId)),a=s?this.options.manager.getSessionIncarnation(e.terminalId):void 0,o=s||"terminal.resume"!==e.type?a?BigInt(this.options.manager.nextServerSequence(e.terminalId,a)):1n:BigInt(e.lastServerSequence)+1n,c={protocolVersion:TERMINAL_SERVICE_PROTOCOL_VERSION,type:"terminal.error",clientId:e.clientId,terminalId:e.terminalId,sequence:String(o),code:n,message:n,recoverable:"TERMINAL_IO_ERROR"===n||"TERMINAL_QUOTA_EXCEEDED"===n};if(!s){const t=JSON.stringify(c);return this.enqueueWithOutputBackpressure({clientId:e.clientId,terminalId:e.terminalId,frame:c,wire:t,bytes:Buffer.byteLength(t)}),void this.kickFlush()}this.send(c)}remember(e,t,i){const n=this.rings.get(e.terminalId)??[];n.push({frame:Object.freeze({...e}),wire:t,bytes:i});let r=(this.ringBytes.get(e.terminalId)??0)+i;const s=this.options.maxBufferedFramesPerTerminal??512,a=this.options.maxBufferedBytesPerTerminal??1048576;for(;n.length>s||r>a;){const t=n.shift();if(!t)break;r-=t.bytes,this.droppedThrough.set(e.terminalId,BigInt(t.frame.sequence))}this.rings.set(e.terminalId,n),this.ringBytes.set(e.terminalId,r),"terminal.exit"===e.type&&this.scheduleReplayExpiry(e.clientId,e.terminalId)}enqueueWithOutputBackpressure(e){const t=this.options.maxPendingSendBytes??1048576;if(this.pendingBytes+e.bytes<=t)return this.pending.push(e),void(this.pendingBytes+=e.bytes);if("terminal.output"===e.frame.type){if(this.hasPendingTruncation(e.terminalId))return;const i=this.toTruncationMarker(e);return this.compactPendingOutput(i.bytes,t),void(this.hasPendingTruncation(e.terminalId)||(this.pending.push(i),this.pendingBytes+=i.bytes))}this.compactPendingOutput(e.bytes,t),this.pending.push(e),this.pendingBytes+=e.bytes}compactPendingOutput(e,t){for(;this.pendingBytes+e>t;){const e=this.pending.findIndex(e=>"terminal.output"===e.frame.type&&!0!==e.frame.truncated);if(e<0)return;const t=this.pending[e];if(this.hasPendingTruncation(t.terminalId,t)){this.pending.splice(e,1),this.pendingBytes-=t.bytes;continue}const i=this.toTruncationMarker(t);this.pending[e]=i,this.pendingBytes+=i.bytes-t.bytes}}hasPendingTruncation(e,t){return this.activeSend?.item!==t&&this.activeSend?.item.terminalId===e&&"terminal.output"===this.activeSend.item.frame.type&&!0===this.activeSend.item.frame.truncated||this.pending.some(i=>i!==t&&i.terminalId===e&&"terminal.output"===i.frame.type&&!0===i.frame.truncated)}toTruncationMarker(e){if("terminal.output"!==e.frame.type)throw new Error("only terminal output can be truncated");const t={...e.frame,data:"",encoding:"base64",truncated:!0},i=JSON.stringify(t);return{...e,frame:t,wire:i,bytes:Buffer.byteLength(i)}}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("transport"))}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;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;const i=this.serviceGeneration;switch(e){case"transport":return;case"park":i&&this.service.parkServiceSessions(i,this.handlerIdentity,this.options.leaseRecoveryGraceMs??3e5);break;case"fence":this.service.closeParkedServiceSessions("runtime_lease_lost"),i&&this.service.fenceServiceSessions("runtime_lease_lost",i);break;default:throw new Error(`unhandled connection invalidation ${String(e)}`)}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()}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,r)=>{let s=!1;const a=e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),n(e))},o=()=>a(interrupted),c=setTimeout(()=>a(interrupted),Math.max(0,t));c.unref?.(),i?.addEventListener("abort",o,{once:!0}),e.then(e=>a(e),e=>{s||(s=!0,clearTimeout(c),i?.removeEventListener("abort",o),r(e))})})}
@@ -1 +1 @@
1
- import{randomUUID}from"node:crypto";import{assertTerminalClientFrame}from"./runtime-terminal-service.js";import{systemTerminalAuthorityClock,verifyTerminalAuthority}from"./terminal-authority-contract.js";import{InMemoryTerminalAuthorityJournal}from"./terminal-authority-journal.js";export class TerminalAuthorityAdmission{journal;clock;constructor(r=new InMemoryTerminalAuthorityJournal,e=systemTerminalAuthorityClock){this.journal=r,this.clock=e}async admit(r){if(!isRecord(r))throw new Error("invalid terminal client frame");const e=verifyTerminalAuthority(r.terminalAuthority,this.clock);if(r.clientId!==e.clientId)throw new Error("terminal authority client mismatch");if("terminal.open"===r.type||"terminal.create"===r.type){if(e.resumeTerminalId)throw new Error("terminal resume requires terminal.resume");if(""!==r.terminalId)throw new Error("new terminal must be runtime allocated");assertTerminalClientFrame({...r,terminalId:VALIDATION_TERMINAL_ID});const t=await this.journal.bindNew(e,randomUUID());return{frame:{...r,terminalId:t.binding.terminalId},admission:{recovered:t.recovered}}}assertTerminalClientFrame(r);const t=r.terminalId;if("terminal.resume"===r.type){if(!e.resumeTerminalId||e.resumeTerminalId!==t)throw new Error("terminal resume id mismatch");return await this.journal.resume(e),{frame:r,admission:{recovered:!0}}}return await this.journal.assertBound(t,e),{frame:r,admission:{recovered:!1}}}}const VALIDATION_TERMINAL_ID="018f0000-0000-7000-8000-0000000000f0";function isRecord(r){return null!==r&&"object"==typeof r&&!Array.isArray(r)}
1
+ import{randomUUID}from"node:crypto";import{assertTerminalClientFrame}from"./runtime-terminal-service.js";import{systemTerminalAuthorityClock,verifyTerminalAuthority}from"./terminal-authority-contract.js";import{InMemoryTerminalAuthorityJournal}from"./terminal-authority-journal.js";export class TerminalAuthorityAdmission{journal;clock;constructor(r=new InMemoryTerminalAuthorityJournal,e=systemTerminalAuthorityClock){this.journal=r,this.clock=e}async admit(r){if(!isRecord(r))throw new Error("invalid terminal client frame");const e="terminal.open"===r.type||"terminal.create"===r.type,t=e||"terminal.resume"===r.type?"establish":"bound",i=verifyTerminalAuthority(r.terminalAuthority,this.clock,t);if(r.clientId!==i.clientId)throw new Error("terminal authority client mismatch");if(e){if(i.resumeTerminalId)throw new Error("terminal resume requires terminal.resume");if(""!==r.terminalId)throw new Error("new terminal must be runtime allocated");assertTerminalClientFrame({...r,terminalId:VALIDATION_TERMINAL_ID});const e=await this.journal.bindNew(i,randomUUID());return{frame:{...r,terminalId:e.binding.terminalId},admission:{recovered:e.recovered}}}assertTerminalClientFrame(r);const n=r.terminalId;if("terminal.resume"===r.type){if(!i.resumeTerminalId||i.resumeTerminalId!==n)throw new Error("terminal resume id mismatch");return await this.journal.resume(i),{frame:r,admission:{recovered:!0}}}return await this.journal.assertBound(n,i),{frame:r,admission:{recovered:!1}}}}const VALIDATION_TERMINAL_ID="018f0000-0000-7000-8000-0000000000f0";function isRecord(r){return null!==r&&"object"==typeof r&&!Array.isArray(r)}
@@ -1 +1 @@
1
- import{createHash}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";export const TERMINAL_AUTHORITY_SCHEMA_VERSION=1;export const TERMINAL_AUTHORITY_CONTRACT_REVISION="beeos.terminal-authority.v1";export const TERMINAL_AUTHORITY_SCHEMA_HASH="e1e1acbf9d3c7a8ee2377e7224d327b1a136069de7024c71e229702e997cdcea";export const TERMINAL_AUTHORITY_CONTRACT_DIGEST="88eaec1ec16ab48462c81b58a6c65920ba721e9f7adf074f6467373075950c8f";export const TERMINAL_AUTHORITY_CANONICALIZATION_REVISION="rfc8785-jcs";export const TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE="JCS(authority with ticketProof and authorityHash omitted)";export const TERMINAL_AUTHORITY_CLOCK_SKEW_MS=5e3;export const TERMINAL_AUTHORITY_MAX_LIFETIME_MS=6e4;const HEX_256=/^[0-9a-f]{64}$/,RFC3339_NANO=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/,NANOS_PER_MILLISECOND=1000000n,REQUIRED_KEYS=["schemaVersion","contractRevision","schemaHash","userId","instanceId","platformAgentId","clientId","ticketId","ticketProof","ticketProofHash","issuedAt","expiresAt","scopeHash","authorityHash"],AUTHORITY_KEYS=new Set([...REQUIRED_KEYS,"conversationId","resumeTerminalId"]);export const systemTerminalAuthorityClock=Object.freeze({now:()=>new Date});export const terminalAuthorityJcsPort=Object.freeze({revision:"rfc8785-jcs",canonicalize:t=>canonicalizeJcs(t),hash:t=>sha256(canonicalizeJcs(t))});export function verifyTerminalAuthorityGolden(t){if("beeos.terminal-authority.v1.golden"!==t.format||"rfc8785-jcs"!==t.canonicalization||"sha256"!==t.hashAlgorithm||t.contractDigest!==TERMINAL_AUTHORITY_CONTRACT_DIGEST||1!==t.contract.schemaVersion||"beeos.terminal-authority.v1"!==t.contract.contractRevision||t.contract.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH||t.contract.authorityHashPreimageRule!==TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE||sha256(t.contract.schemaCanonicalJson)!==TERMINAL_AUTHORITY_SCHEMA_HASH||terminalAuthorityJcsPort.hash(t.contract)!==t.contractDigest||0===t.fixtures.length)throw new Error("terminal authority JCS golden contract mismatch");for(const e of t.fixtures){const t=authorityHashPreimage(e.authority);if(terminalAuthorityJcsPort.canonicalize(JSON.parse(e.authorityHashPreimageJson))!==t||t!==e.authorityCanonicalJson||sha256(t)!==e.authorityHash||e.authorityHash!==e.authority.authorityHash)throw new Error(`terminal authority JCS golden fixture mismatch: ${e.name}`)}}export function verifyTerminalAuthority(t,e=systemTerminalAuthorityClock){if(!isPlainObject(t)||Object.keys(t).some(t=>!AUTHORITY_KEYS.has(t))||REQUIRED_KEYS.some(e=>!(e in t)))throw new Error("terminal authority schema mismatch");const r=t;if(1!==r.schemaVersion||"beeos.terminal-authority.v1"!==r.contractRevision||r.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH)throw new Error("terminal authority contract mismatch");const i=requireString(r.userId,256,"userId"),o=requireString(r.instanceId,128,"instanceId"),n=requireString(r.platformAgentId,128,"platformAgentId"),a=optionalString(r.conversationId,128,"conversationId"),s=requireString(r.clientId,128,"clientId"),c=requireString(r.ticketId,128,"ticketId"),h=requireString(r.ticketProof,8192,"ticketProof"),u=requireDigest(r.ticketProofHash,"ticketProofHash"),m=requireTimestamp(r.issuedAt,"issuedAt"),I=requireTimestamp(r.expiresAt,"expiresAt"),l=optionalString(r.resumeTerminalId,128,"resumeTerminalId"),d=requireDigest(r.scopeHash,"scopeHash"),A=requireDigest(r.authorityHash,"authorityHash"),f=timestampNanos(m),T=timestampNanos(I),H=e.now().getTime();if(!Number.isFinite(H))throw new Error("terminal authority clock is invalid");const E=1000000n*BigInt(Math.trunc(H));if(T<=f)throw new Error("terminal authority time range is invalid");if(T-f>1000000n*BigInt(6e4))throw new Error("terminal authority lifetime exceeds maximum");if(f>E+1000000n*BigInt(5e3))throw new Error("terminal authority issue time is in the future");if(T<=E)throw new Error("terminal authority has expired");if(sha256(h)!==u)throw new Error("terminal authority ticket proof mismatch");const y=authorityHashPreimage(r);if(sha256(y)!==A)throw new Error("terminal authority integrity mismatch");return Object.freeze({schemaVersion:1,contractRevision:"beeos.terminal-authority.v1",schemaHash:TERMINAL_AUTHORITY_SCHEMA_HASH,userId:i,instanceId:o,platformAgentId:n,conversationId:a,clientId:s,ticketId:c,issuedAt:m,expiresAt:I,resumeTerminalId:l,scopeHash:d,authorityHash:A,terminalBindingHash:sha256(["terminal-binding-v1",i,o,n,a,s,c].join("\0"))})}export function authorityHashPreimage(t){const e=t,r={schemaVersion:e.schemaVersion,contractRevision:e.contractRevision,schemaHash:e.schemaHash,userId:e.userId,instanceId:e.instanceId,platformAgentId:e.platformAgentId,...e.conversationId?{conversationId:e.conversationId}:{},clientId:e.clientId,ticketId:e.ticketId,ticketProofHash:e.ticketProofHash,issuedAt:e.issuedAt,expiresAt:e.expiresAt,...e.resumeTerminalId?{resumeTerminalId:e.resumeTerminalId}:{},scopeHash:e.scopeHash};return terminalAuthorityJcsPort.canonicalize(r)}export const sha256=t=>createHash("sha256").update(t,"utf8").digest("hex");function requireString(t,e,r){if("string"!=typeof t||t.length<1||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function optionalString(t,e,r){if(void 0===t)return"";if("string"!=typeof t||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function requireDigest(t,e){if("string"!=typeof t||!HEX_256.test(t))throw new Error(`terminal authority ${e} is invalid`);return t}function requireTimestamp(t,e){if("string"!=typeof t||!RFC3339_NANO.test(t)||!Number.isFinite(Date.parse(t)))throw new Error(`terminal authority ${e} is invalid`);return t}function timestampNanos(t){const e=RFC3339_NANO.exec(t);if(!e)throw new Error("terminal authority timestamp is invalid");const r=Date.parse(`${e[1]}${e[3]}`),i=BigInt((e[2]??"").padEnd(9,"0"));return 1000000n*BigInt(r)+i}function isPlainObject(t){return null!==t&&"object"==typeof t&&!Array.isArray(t)&&(Object.getPrototypeOf(t)===Object.prototype||null===Object.getPrototypeOf(t))}function hasLoneSurrogate(t){for(let e=0;e<t.length;e++){const r=t.charCodeAt(e);if(r>=55296&&r<=56319){if(++e>=t.length||t.charCodeAt(e)<56320||t.charCodeAt(e)>57343)return!0}else if(r>=56320&&r<=57343)return!0}return!1}
1
+ import{createHash}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";export const TERMINAL_AUTHORITY_SCHEMA_VERSION=1;export const TERMINAL_AUTHORITY_CONTRACT_REVISION="beeos.terminal-authority.v1";export const TERMINAL_AUTHORITY_SCHEMA_HASH="e1e1acbf9d3c7a8ee2377e7224d327b1a136069de7024c71e229702e997cdcea";export const TERMINAL_AUTHORITY_CONTRACT_DIGEST="88eaec1ec16ab48462c81b58a6c65920ba721e9f7adf074f6467373075950c8f";export const TERMINAL_AUTHORITY_CANONICALIZATION_REVISION="rfc8785-jcs";export const TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE="JCS(authority with ticketProof and authorityHash omitted)";export const TERMINAL_AUTHORITY_CLOCK_SKEW_MS=5e3;export const TERMINAL_AUTHORITY_MAX_LIFETIME_MS=6e4;const HEX_256=/^[0-9a-f]{64}$/,RFC3339_NANO=/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d{1,9}))?(Z|[+-]\d{2}:\d{2})$/,NANOS_PER_MILLISECOND=1000000n,REQUIRED_KEYS=["schemaVersion","contractRevision","schemaHash","userId","instanceId","platformAgentId","clientId","ticketId","ticketProof","ticketProofHash","issuedAt","expiresAt","scopeHash","authorityHash"],AUTHORITY_KEYS=new Set([...REQUIRED_KEYS,"conversationId","resumeTerminalId"]);export const systemTerminalAuthorityClock=Object.freeze({now:()=>new Date});export const terminalAuthorityJcsPort=Object.freeze({revision:"rfc8785-jcs",canonicalize:t=>canonicalizeJcs(t),hash:t=>sha256(canonicalizeJcs(t))});export function verifyTerminalAuthorityGolden(t){if("beeos.terminal-authority.v1.golden"!==t.format||"rfc8785-jcs"!==t.canonicalization||"sha256"!==t.hashAlgorithm||t.contractDigest!==TERMINAL_AUTHORITY_CONTRACT_DIGEST||1!==t.contract.schemaVersion||"beeos.terminal-authority.v1"!==t.contract.contractRevision||t.contract.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH||t.contract.authorityHashPreimageRule!==TERMINAL_AUTHORITY_HASH_PREIMAGE_RULE||sha256(t.contract.schemaCanonicalJson)!==TERMINAL_AUTHORITY_SCHEMA_HASH||terminalAuthorityJcsPort.hash(t.contract)!==t.contractDigest||0===t.fixtures.length)throw new Error("terminal authority JCS golden contract mismatch");for(const e of t.fixtures){const t=authorityHashPreimage(e.authority);if(terminalAuthorityJcsPort.canonicalize(JSON.parse(e.authorityHashPreimageJson))!==t||t!==e.authorityCanonicalJson||sha256(t)!==e.authorityHash||e.authorityHash!==e.authority.authorityHash)throw new Error(`terminal authority JCS golden fixture mismatch: ${e.name}`)}}export function verifyTerminalAuthority(t,e=systemTerminalAuthorityClock,r="establish"){if(!isPlainObject(t)||Object.keys(t).some(t=>!AUTHORITY_KEYS.has(t))||REQUIRED_KEYS.some(e=>!(e in t)))throw new Error("terminal authority schema mismatch");const i=t;if(1!==i.schemaVersion||"beeos.terminal-authority.v1"!==i.contractRevision||i.schemaHash!==TERMINAL_AUTHORITY_SCHEMA_HASH)throw new Error("terminal authority contract mismatch");const o=requireString(i.userId,256,"userId"),n=requireString(i.instanceId,128,"instanceId"),a=requireString(i.platformAgentId,128,"platformAgentId"),s=optionalString(i.conversationId,128,"conversationId"),c=requireString(i.clientId,128,"clientId"),h=requireString(i.ticketId,128,"ticketId"),u=requireString(i.ticketProof,8192,"ticketProof"),m=requireDigest(i.ticketProofHash,"ticketProofHash"),I=requireTimestamp(i.issuedAt,"issuedAt"),l=requireTimestamp(i.expiresAt,"expiresAt"),d=optionalString(i.resumeTerminalId,128,"resumeTerminalId"),A=requireDigest(i.scopeHash,"scopeHash"),f=requireDigest(i.authorityHash,"authorityHash"),T=timestampNanos(I),H=timestampNanos(l),E=e.now().getTime();if(!Number.isFinite(E))throw new Error("terminal authority clock is invalid");const y=1000000n*BigInt(Math.trunc(E));if(H<=T)throw new Error("terminal authority time range is invalid");if(H-T>1000000n*BigInt(6e4))throw new Error("terminal authority lifetime exceeds maximum");if(T>y+1000000n*BigInt(5e3))throw new Error("terminal authority issue time is in the future");if("establish"===r&&H<=y)throw new Error("terminal authority has expired");if(sha256(u)!==m)throw new Error("terminal authority ticket proof mismatch");const p=authorityHashPreimage(i);if(sha256(p)!==f)throw new Error("terminal authority integrity mismatch");return Object.freeze({schemaVersion:1,contractRevision:"beeos.terminal-authority.v1",schemaHash:TERMINAL_AUTHORITY_SCHEMA_HASH,userId:o,instanceId:n,platformAgentId:a,conversationId:s,clientId:c,ticketId:h,issuedAt:I,expiresAt:l,resumeTerminalId:d,scopeHash:A,authorityHash:f,terminalBindingHash:sha256(["terminal-binding-v1",o,n,a,s,c,h].join("\0"))})}export function authorityHashPreimage(t){const e=t,r={schemaVersion:e.schemaVersion,contractRevision:e.contractRevision,schemaHash:e.schemaHash,userId:e.userId,instanceId:e.instanceId,platformAgentId:e.platformAgentId,...e.conversationId?{conversationId:e.conversationId}:{},clientId:e.clientId,ticketId:e.ticketId,ticketProofHash:e.ticketProofHash,issuedAt:e.issuedAt,expiresAt:e.expiresAt,...e.resumeTerminalId?{resumeTerminalId:e.resumeTerminalId}:{},scopeHash:e.scopeHash};return terminalAuthorityJcsPort.canonicalize(r)}export const sha256=t=>createHash("sha256").update(t,"utf8").digest("hex");function requireString(t,e,r){if("string"!=typeof t||t.length<1||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function optionalString(t,e,r){if(void 0===t)return"";if("string"!=typeof t||t.length>e||hasLoneSurrogate(t))throw new Error(`terminal authority ${r} is invalid`);return t}function requireDigest(t,e){if("string"!=typeof t||!HEX_256.test(t))throw new Error(`terminal authority ${e} is invalid`);return t}function requireTimestamp(t,e){if("string"!=typeof t||!RFC3339_NANO.test(t)||!Number.isFinite(Date.parse(t)))throw new Error(`terminal authority ${e} is invalid`);return t}function timestampNanos(t){const e=RFC3339_NANO.exec(t);if(!e)throw new Error("terminal authority timestamp is invalid");const r=Date.parse(`${e[1]}${e[3]}`),i=BigInt((e[2]??"").padEnd(9,"0"));return 1000000n*BigInt(r)+i}function isPlainObject(t){return null!==t&&"object"==typeof t&&!Array.isArray(t)&&(Object.getPrototypeOf(t)===Object.prototype||null===Object.getPrototypeOf(t))}function hasLoneSurrogate(t){for(let e=0;e<t.length;e++){const r=t.charCodeAt(e);if(r>=55296&&r<=56319){if(++e>=t.length||t.charCodeAt(e)<56320||t.charCodeAt(e)>57343)return!0}else if(r>=56320&&r<=57343)return!0}return!1}
@@ -7,7 +7,6 @@
7
7
  "onStartup": true
8
8
  },
9
9
  "contracts": {
10
- "trustedToolPolicies": ["beeos-approval-v1"],
11
10
  "tools": [
12
11
  "beeos_upload_file",
13
12
  "beeos_desktop_screenshot",
@@ -30,11 +29,7 @@
30
29
  "beeos_automation_get_run",
31
30
  "cron_toggle",
32
31
  "cron_run",
33
- "cron_delete",
34
- "list_connectors",
35
- "search_actions",
36
- "get_action_guide",
37
- "execute_action"
32
+ "cron_delete"
38
33
  ],
39
34
  "imageGenerationProviders": [
40
35
  "arouter"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.6.66-cloud.5",
3
+ "version": "0.6.66-cloud.7",
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>",
@@ -88,5 +88,5 @@
88
88
  "fast-uri": ">=3.1.4",
89
89
  "brace-expansion": ">=5.0.9"
90
90
  },
91
- "gitHead": "2e2d94b544e224a005f81c54bf4ef728a730060c"
91
+ "gitHead": "d7fccf61aa9337eb2d13c36c9666de4bc80eceda"
92
92
  }
@@ -1 +0,0 @@
1
- import{createHash,randomUUID}from"node:crypto";import{canonicalizeJcs}from"@beeos-ai/beeos-types/runtime";import{APPROVAL_PROTOCOL,parseApprovalUserAction}from"./protocol.generated.js";export function approvalOperationHash(e,t){const s=canonicalizeJcs({toolName:e,params:t});if(s.length>1048576)throw new Error("approval_operation_too_large");return createHash("sha256").update(s).digest("hex")}export class ApprovalGate{journal;publish;timeoutMs;now;pending=new Map;tasks=new Set;issued=new Map;constructor(e,t,s=3e5,r=Date.now){if(this.journal=e,this.publish=t,this.timeoutMs=s,this.now=r,!Number.isFinite(s)||s<=0||s>3e5)throw new Error("invalid_approval_timeout")}async request(e,t,s,r,o){if(!e.active()||!e.ownerIdentity.startsWith("user:")||e.ownerIdentity.length<=5||!e.sessionId||!e.taskId||!/^[1-9]\d*$/u.test(e.runtimeEpoch)||o?.aborted)return;const i=`${e.sessionId}\0${e.taskId}`;if(this.tasks.has(i))return;const n=`apr_${randomUUID()}`,a={protocol:APPROVAL_PROTOCOL,kind:"approval_request",requestId:n,requestRevision:1,sessionId:e.sessionId,taskId:e.taskId,trigger:{type:"policy_required",reasonCode:"SESSION_HUMAN_APPROVAL_REQUIRED"},operation:{type:"tool.invoke",summary:`Run ${t}`,parameters:r},availableActions:["approve_once","deny","cancel"],createdAt:new Date(this.now()).toISOString(),expiresAt:new Date(this.now()+this.timeoutMs).toISOString()},d={request:a,ownerIdentity:e.ownerIdentity,runtimeEpoch:e.runtimeEpoch,platformAgentId:e.platformAgentId,operationHash:approvalOperationHash(t,s)};this.journal.write(d);const c=this.issued.get(i)??new Set;let l;c.add(n),this.issued.set(i,c);const u=new Promise(e=>{l=e}),p={record:d,context:e,release:l};this.pending.set(n,p),this.tasks.add(i);const h=()=>{this.settleSystem(p,"task_cancelled","TASK_CANCELLED").catch(()=>l(!1))};o?.addEventListener("abort",h,{once:!0});const I=setTimeout(()=>{this.settleSystem(p,"expired","APPROVAL_EXPIRED").catch(()=>l(!1)),l(!1)},this.timeoutMs);try{await Promise.race([this.publish("agent.input_required",`approval_request:${n}`,e.sessionId,a),u.then(e=>{if(!e)throw new Error("approval_wait_closed")})]),e.active()&&!o?.aborted||h();return await u&&e.active()&&!o?.aborted&&this.now()<Date.parse(a.expiresAt)?n:void 0}catch{return void this.settleSystem(p,"withdrawn","APPROVAL_DELIVERY_FAILED").catch(()=>{})}finally{clearTimeout(I),o?.removeEventListener("abort",h),this.pending.delete(n),this.tasks.delete(i)}}async resolve(e,t,s){const r=parseApprovalUserAction(e);if(!r||"approval_decision"!==r.kind)throw new Error("INVALID_APPROVAL_ACTION");const o=this.journal.read(r.requestId);if(!o)throw new Error("APPROVAL_NOT_FOUND");const i=o.request;if(o.ownerIdentity!==t||i.sessionId!==s||r.sessionId!==s||r.taskId!==i.taskId||r.requestRevision!==i.requestRevision)throw new Error("APPROVAL_REQUEST_MISMATCH");if(o.result){if(o.result.acceptedActionId===r.actionId&&canonicalizeJcs(o.acceptedAction)!==canonicalizeJcs(r))throw new Error("APPROVAL_ACTION_CONFLICT");return await this.publishResult(o),structuredClone(o.result)}const n=this.pending.get(r.requestId);if(!n||!n.context.active())throw new Error("APPROVAL_UNAVAILABLE");if(this.now()>=Date.parse(i.expiresAt))throw await this.settleSystem(n,"expired","APPROVAL_EXPIRED"),new Error("APPROVAL_EXPIRED");const a="approve_once"===r.decision?"approved":"deny"===r.decision?"denied":"cancelled";return o.acceptedAction=r,o.result={protocol:APPROVAL_PROTOCOL,kind:"approval_resolved",requestId:i.requestId,sessionId:s,taskId:i.taskId,resolution:{status:a,decision:r.decision,responseAction:r.decision,source:"user",reasonCode:`USER_${r.decision.toUpperCase()}`},acceptedActionId:r.actionId,resolvedAt:new Date(this.now()).toISOString()},this.journal.write(o),n.record=o,await this.publishResult(o),structuredClone(o.result)}consume(e,t,s,r){const o=this.journal.read(e);return!(!o||o.consumed||o.invalidated||"approved"!==o.result?.resolution.status||!t.active()||o.request.sessionId!==t.sessionId||o.request.taskId!==t.taskId||o.ownerIdentity!==t.ownerIdentity||o.runtimeEpoch!==t.runtimeEpoch||o.platformAgentId!==t.platformAgentId||this.now()>=Date.parse(o.request.expiresAt)||o.operationHash!==approvalOperationHash(s,r))&&(o.consumed=!0,this.journal.write(o),!0)}cancelTask(e,t){const s=`${e}\0${t}`;for(const e of this.issued.get(s)??[]){const t=this.journal.read(e);t&&!t.consumed&&(t.invalidated=!0,this.journal.write(t))}this.issued.delete(s);for(const s of this.pending.values())s.record.request.sessionId===e&&s.record.request.taskId===t&&this.settleSystem(s,"task_cancelled","TASK_CANCELLED").catch(()=>s.release(!1))}async settleSystem(e,t,s){if(e.record.result)return void e.release(!1);const r=e.record.request;e.record.result={protocol:APPROVAL_PROTOCOL,kind:"approval_resolved",requestId:r.requestId,sessionId:r.sessionId,taskId:r.taskId,resolution:{status:t,source:"system",reasonCode:s},resolvedAt:new Date(this.now()).toISOString()},this.journal.write(e.record),e.release(!1);try{await this.publishResult(e.record)}finally{e.release(!1)}}async publishResult(e){let t;try{await Promise.race([this.publish("approval.resolved",`approval_resolved:${e.request.requestId}`,e.request.sessionId,e.result),new Promise((e,s)=>{t=setTimeout(()=>s(new Error("approval_publication_timeout")),Math.min(this.timeoutMs,3e4))})])}finally{t&&clearTimeout(t)}this.pending.get(e.request.requestId)?.release("approved"===e.result.resolution.status)}}
@@ -1 +0,0 @@
1
- import{closeSync,existsSync,fsyncSync,mkdirSync,openSync,readFileSync,renameSync,writeFileSync}from"node:fs";import{createHash,randomUUID}from"node:crypto";import{join}from"node:path";export class FileApprovalJournal{directory;constructor(r){this.directory=r,mkdirSync(r,{recursive:!0,mode:448})}path(r){return join(this.directory,`${createHash("sha256").update(r).digest("hex")}.json`)}read(r){const e=this.path(r);if(!existsSync(e))return;const n=JSON.parse(readFileSync(e,"utf8"));if(n.request?.requestId!==r)throw new Error("approval_journal_mismatch");return n}write(r){const e=this.path(r.request.requestId),n=`${e}.${randomUUID()}.tmp`,t=openSync(n,"wx",384);try{writeFileSync(t,JSON.stringify(r)),fsyncSync(t)}finally{closeSync(t)}renameSync(n,e);const c=openSync(this.directory,"r");try{fsyncSync(c)}finally{closeSync(c)}}}
@@ -1 +0,0 @@
1
- import{AsyncLocalStorage}from"node:async_hooks";import{approvalPresentation}from"./presentation.js";export class ApprovalTurnPolicy{gate;secrets;turns=new Map;current=new AsyncLocalStorage;constructor(e,t=[]){this.gate=e,this.secrets=t}isManaged(e){return Boolean(this.current.getStore()||e.runId&&this.turns.has(e.runId))}enter(e,t,s){if(!e||!t||this.turns.has(t))throw new Error("approval_turn_identity_conflict");const o={sessionKey:e,open:!0,permits:new Map,seenCalls:new Set,context:{...s,active:()=>o.open&&s.active()}};return this.turns.set(t,o),{run:e=>this.current.run(o,e),close:()=>{o.open=!1,this.gate.cancelTask(s.sessionId,s.taskId),o.permits.clear(),this.turns.delete(t)}}}async evaluate(e,t){const s=this.resolve(t);if(!s||!s.context.active()||!t.toolCallId||t.abortSignal?.aborted)return{block:!0,blockReason:"APPROVAL_EXECUTION_CONTEXT_UNAVAILABLE"};if(["sessions_spawn","subagents","delegate_task"].includes(e.toolName))return{block:!0,blockReason:"APPROVAL_DELEGATED_EXECUTION_NOT_SUPPORTED"};if(s.seenCalls.has(t.toolCallId))return{block:!0,blockReason:"APPROVAL_DUPLICATE_TOOL_CALL"};s.seenCalls.add(t.toolCallId);const o=approvalPresentation(e.params,this.secrets),r=await this.gate.request(s.context,e.toolName,e.params,{toolName:e.toolName,arguments:o},t.abortSignal);if(!r)return{block:!0,blockReason:"APPROVAL_OPERATION_NOT_APPROVED"};s.permits.set(t.toolCallId,r)}finalize(e,t){try{const s=this.resolve(t);if(!s||!t.toolCallId||t.abortSignal?.aborted)return{block:!0,blockReason:"APPROVAL_EXECUTION_CONTEXT_UNAVAILABLE"};const o=s.permits.get(t.toolCallId);return s.permits.delete(t.toolCallId),o&&this.gate.consume(o,s.context,e.toolName,e.params)?{params:structuredClone(e.params)}:{block:!0,blockReason:"APPROVAL_EXECUTION_BINDING_CHANGED"}}catch{return{block:!0,blockReason:"APPROVAL_EXECUTION_AUTHORITY_UNAVAILABLE"}}}resolve(e){const t=e.runId?this.turns.get(e.runId):this.current.getStore();return!t||e.sessionKey&&e.sessionKey!==t.sessionKey?void 0:t}}
@@ -1 +0,0 @@
1
- const CONNECTOR_TOOL_NAMES=["list_connectors","search_actions","get_action_guide","execute_action"],objectSchema=(t,e)=>({type:"object",additionalProperties:!1,properties:t,...e&&e.length>0?{required:[...e]}:{}}),TOOLS={list_connectors:{description:"List the connectors enabled for this OpenClaw instance.",parameters:objectSchema({})},search_actions:{description:"Search actions exposed by enabled connectors.",parameters:objectSchema({query:{type:"string"},connectorId:{type:"string"},limit:{type:"integer"}})},get_action_guide:{description:"Get the input and output schema for one connector action.",parameters:objectSchema({actionId:{type:"string"}},["actionId"])},execute_action:{description:"Execute one connector action using the credential bound to this instance. For the Weather connector, actionId is wttr.get_weather and input is an object with location.",parameters:objectSchema({actionId:{type:"string"},input:{type:"object"}},["actionId","input"])}};function argumentsOf(t){if(null==t)return{};if("object"!=typeof t||Array.isArray(t))throw new Error("connector tool arguments must be an object");return t}function textFromContent(t){if(!Array.isArray(t))return;const e=t.flatMap(t=>{if(!t||"object"!=typeof t||!("text"in t))return[];const e=t.text;return"string"==typeof e?[e]:[]});return e.length>0?e.join("\n"):void 0}export function readableConnectorResult(t){if(!t||"object"!=typeof t)return{content:JSON.stringify(t)};const e=t;if(void 0!==e.error&&null!==e.error){const t=e.error,n=t&&"object"==typeof t&&"message"in t?String(t.message):JSON.stringify(t);throw new Error(n)}const n="result"in e?e.result:t;if(n&&"object"==typeof n){const t=n;if(!0===t.isError)throw new Error(textFromContent(t.content)||"connector tool failed");if(void 0!==t.structuredContent)return{content:JSON.stringify(t.structuredContent)};const e=textFromContent(t.content);if(void 0!==e)return{content:e}}return{content:"string"==typeof n?n:JSON.stringify(n)}}async function callConnectorTool(t,e,n,o){return readableConnectorResult(await t.connectors.callJson({jsonrpc:"2.0",id:e,method:"tools/call",params:{name:e,arguments:"list_connectors"===e?{}:argumentsOf(n)}},o?{signal:o}:void 0))}export function registerManagedConnectorTools(t,e){if(e)for(const n of CONNECTOR_TOOL_NAMES){const o=TOOLS[n];t.registerTool({name:n,description:o.description,parameters:o.parameters,execute:(t,o,r)=>callConnectorTool(e,n,o,r)})}}
@@ -1 +0,0 @@
1
- export const CONNECTOR_MCP_PROXY_PATH="/beeos/connectors/mcp";export const CONNECTOR_MCP_MAX_BODY_BYTES=262144;class RequestBodyTooLargeError extends Error{}export function isLoopbackAddress(e){return"127.0.0.1"===e||"::1"===e||"::ffff:127.0.0.1"===e}export function isLoopbackRequest(e){return isLoopbackAddress(e.socket.remoteAddress)}export function readBoundedRawBody(e,o=262144){return new Promise((r,t)=>{const n=[];let s=0,a=!1;const d=()=>{e.off("data",u),e.off("end",i),e.off("error",f),e.off("aborted",p)},c=e=>{a||(a=!0,d(),t(e))},u=r=>{const t=Buffer.isBuffer(r)?r:Buffer.from(r);if(s+=t.byteLength,s>o)return d(),e.resume(),void c(new RequestBodyTooLargeError("connector MCP request body is too large"));n.push(t)},i=()=>{a||(a=!0,d(),r(Buffer.concat(n,s)))},f=e=>c(e),p=()=>c(new Error("connector MCP request was aborted"));e.on("data",u),e.on("end",i),e.on("error",f),e.on("aborted",p)})}function writeTextResponse(e,o,r){e.statusCode=o,e.setHeader("Content-Type","text/plain; charset=utf-8"),e.end(r)}function copyRequestHeader(e,o,r){const t=e.headers[r.toLowerCase()];"string"==typeof t?o.set(r,t):Array.isArray(t)&&o.set(r,t.join(", "))}export async function handleConnectorMcpRequest(e,o,r){if(!isLoopbackRequest(e))return e.resume(),void writeTextResponse(o,403,"loopback access required");if("POST"!==e.method)return e.resume(),o.setHeader("Allow","POST"),void writeTextResponse(o,405,"method not allowed");if(e.url?.includes("?"))return e.resume(),void writeTextResponse(o,400,"query parameters are not supported");if(!r.gateway)return e.resume(),void writeTextResponse(o,503,"connector MCP proxy is not configured");let t;try{t=await readBoundedRawBody(e)}catch(e){return e instanceof RequestBodyTooLargeError?void writeTextResponse(o,413,"request body too large"):(r.logger?.warn?.(`[connector-mcp] failed to read request: ${String(e)}`),void(o.writableEnded||writeTextResponse(o,400,"invalid request body")))}const n=new Headers;copyRequestHeader(e,n,"Content-Type"),copyRequestHeader(e,n,"Accept"),copyRequestHeader(e,n,"MCP-Protocol-Version");try{const e=await r.gateway.connectors.mcp({method:"POST",headers:n,body:new Uint8Array(t)}),s=Buffer.from(await e.arrayBuffer());o.statusCode=e.status;const a=e.headers.get("content-type");a&&o.setHeader("Content-Type",a),o.end(s)}catch(e){r.logger?.warn?.(`[connector-mcp] upstream request failed: ${String(e)}`),o.writableEnded||writeTextResponse(o,502,"connector MCP upstream unavailable")}}