beeos-claw 0.5.48 → 0.5.49

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{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const normalizePromptTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},deterministicMessageId=(e,n,t,a)=>`${e}_${createHash("sha256").update(`${n}:${t}:${a}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:t,client:a,logger:r,promptRunStore:i}=e;if(!hasRuntimeReplyApi(n))return r.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),!1;const s=`im_req_${randomUUID().replace(/-/g,"")}`,o=Date.now(),d=e.origin??"im",l={protocol:"im",requestId:s,sessionId:t.sessionKey??"agent:main",sourceMessageId:t.inboundMessageId,chatId:t.channelId,origin:d},c=normalizePromptTimeoutMs(e.promptTimeoutMs),u=promptRunKey(t.channelId,t.inboundMessageId),m=new AbortController;if(!i.register(u,{requestId:s,channelId:t.channelId,abortController:m,startedAt:o}))return r.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${t.inboundMessageId} requestId=${s} channel=${t.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const p=a.messages.startStream({conversationId:t.channelId,id:deterministicMessageId("asst",t.channelId,t.inboundMessageId,t.selfPrincipalId),replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reply"},{onError:(e,n)=>{const a="terminal"===n?r.warn:r.debug;a?.(`[im] runtime stream ${n} PATCH error requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${e.message}`)}},{sender:t.selfPrincipalId});let I,h=!1;try{h=!0===(await p.opened()).idempotent}catch(e){r.debug?.(`[im] runtime reply stream open probe failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}; continuing so terminal fallback can land an error row`)}if(h)return r.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(u),!1;const g=new SerializedCallbackQueue,y=createImReplySink({stream:p,openReasoningStream:()=>(I=a.messages.startStream({conversationId:t.channelId,id:deterministicMessageId("rsn",t.channelId,t.inboundMessageId,t.selfPrincipalId),replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reasoning"},{onError:(e,n)=>{const a="terminal"===n?r.warn:r.debug;a?.(`[im] runtime reasoning stream ${n} PATCH error requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${e.message}`)}},{sender:t.selfPrincipalId}),I),abortSignal:m.signal,callbackQueue:g,logger:r,requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,origin:d}),f=async(e,n)=>{if(!I)return!1;try{return"cancel"===e?await I.cancel({stopReason:"user_stop"}):"fail"===e?await I.fail(n?{stopReason:n}:void 0):await I.finalize({stopReason:"end_turn"}),!1}catch(e){return r.warn?.(`[im] runtime reasoning stream terminal write failed requestId=${s} channel=${t.channelId}: ${formatErrMessage(e)}`),!0}};r.info?.(`[im] runtime dispatching requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} origin=${d}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId});let $,b,M=!1;const _=async()=>{y.dispatcher.markComplete();try{await y.dispatcher.waitForIdle()}catch{}};try{const a=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:t.dispatchInput,replyRoute:l,replySinks:{im:y},onDispatchInvoked:()=>{M=!0}}),i=void 0!==c?Promise.race([a,new Promise((e,n)=>{$=setTimeout(()=>{b=new Error(`IM runtime dispatch timed out after ${c}ms`),r.warn?.(`[im] runtime dispatch timeout requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} timeout_ms=${c}`),m.abort(b),n(b)},c)})]):a,d=await i.finally(()=>{$&&(clearTimeout($),$=void 0)});await _();const u=await f("complete");let I=!1;try{await p.finalize({stopReason:"end_turn"})}catch(e){I=!0,r.warn?.(`[im] runtime reply stream finalize failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}return r.info?.(`[im] runtime dispatch completed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} counts=${JSON.stringify(d.counts)} elapsed_ms=${Date.now()-o}`),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,counts:d.counts,...I?{terminalWriteFailed:!0}:{},...u?{reasoningTerminalWriteFailed:!0}:{}}),!0}catch(n){await _();const a=m.signal.aborted,i=a?m.signal.reason:void 0,o=a&&"string"==typeof i&&i.startsWith("chat_cancel"),d=void 0!==b;let l="runtime_error";d?l="timeout":a&&(l="abort");const c=formatErrMessage(n);o?await f("cancel"):d?await f("fail","timeout"):await f("fail");try{o?await p.cancel({stopReason:"user_stop"}):d?await p.fail({body:c,stopReason:"timeout"}):await p.fail({body:c||"execution failed"})}catch(e){r.warn?.(`[im] runtime stream terminal write failed requestId=${s} channel=${t.channelId}: ${formatErrMessage(e)}`)}throw r.warn?.(`[im] runtime dispatch failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} cause=${l} dispatchInvoked=${M} error=${c}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:c,cause:l}),n}finally{i.unregister(u)}};
1
+ import{createHash,randomUUID}from"node:crypto";import{dispatchRuntimeReplyWithRoute,hasRuntimeReplyApi}from"./runtime/reply-router.js";import{createImReplySink}from"./runtime/im-reply-sink.js";import{SerializedCallbackQueue}from"./runtime/callback-queue.js";import{promptRunKey}from"./runtime/prompt-run-store.js";const normalizePromptTimeoutMs=e=>{if("number"!=typeof e||!Number.isFinite(e))return;const n=Math.max(0,Math.trunc(e));return n>0?n:void 0},formatErrMessage=e=>{if(e instanceof Error)return e.message;if("string"==typeof e)return e;try{return JSON.stringify(e)}catch{return String(e)}},deterministicMessageId=(e,n,t,a)=>`${e}_${createHash("sha256").update(`${n}:${t}:${a}`).digest("hex").slice(0,32)}`;export const dispatchImInboundPrompt=async e=>{const{runtime:n,prompt:t,client:a,logger:r,promptRunStore:i}=e;if(!hasRuntimeReplyApi(n))return r.warn?.(`[im] runtime dispatch skipped reason=reply_api_unavailable channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"reply_api_unavailable",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),!1;const s=`im_req_${randomUUID().replace(/-/g,"")}`,o=Date.now(),d=e.origin??"im",l={protocol:"im",requestId:s,sessionId:t.sessionKey??"agent:main",sourceMessageId:t.inboundMessageId,chatId:t.channelId,origin:d},c=normalizePromptTimeoutMs(e.promptTimeoutMs),u=promptRunKey(t.channelId,t.inboundMessageId),m=new AbortController;if(!i.register(u,{requestId:s,channelId:t.channelId,abortController:m,startedAt:o}))return r.warn?.(`[im] runtime dispatch rejected duplicate inboundMessageId=${t.inboundMessageId} requestId=${s} channel=${t.channelId}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:"duplicate_inbound_message_id",cause:"abort"}),!1;const p=a.messages.startStream({conversationId:t.channelId,id:deterministicMessageId("asst",t.channelId,t.inboundMessageId,t.selfPrincipalId),replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reply"},{onError:(e,n)=>{const a="terminal"===n?r.warn:r.debug;a?.(`[im] runtime stream ${n} PATCH error requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${e.message}`)}},{sender:t.selfPrincipalId});let I,g=!1;try{g=!0===(await p.opened()).idempotent}catch(e){r.debug?.(`[im] runtime reply stream open probe failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}; continuing so terminal fallback can land an error row`)}if(g)return r.info?.(`[im] runtime dispatch skipped existing deterministic reply requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}`),e.onLifecycleEvent?.({type:"skipped",reason:"idempotent_reply_exists",channelId:t.channelId,inboundMessageId:t.inboundMessageId}),i.unregister(u),!1;const h=new SerializedCallbackQueue,y=createImReplySink({stream:p,openReasoningStream:()=>(I=a.messages.startStream({conversationId:t.channelId,id:deterministicMessageId("rsn",t.channelId,t.inboundMessageId,t.selfPrincipalId),replyTo:e.inReplyTo??t.inboundMessageId,type:"agent_reasoning"},{onError:(e,n)=>{const a="terminal"===n?r.warn:r.debug;a?.(`[im] runtime reasoning stream ${n} PATCH error requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${e.message}`)}},{sender:t.selfPrincipalId}),I),abortSignal:m.signal,callbackQueue:h,logger:r,requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,origin:d}),f=async(e,n)=>{if(!I)return!1;try{return"cancel"===e?await I.cancel({stopReason:"user_stop"}):"fail"===e?await I.fail(n?{stopReason:n}:void 0):await I.finalize({stopReason:"end_turn"}),!1}catch(e){return r.warn?.(`[im] runtime reasoning stream terminal write failed requestId=${s} channel=${t.channelId}: ${formatErrMessage(e)}`),!0}};r.info?.(`[im] runtime dispatching requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} origin=${d}`),e.onLifecycleEvent?.({type:"dispatch_start",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId});let b,$,M=!1;const _=async()=>{y.dispatcher.markComplete();try{await y.dispatcher.waitForIdle()}catch{}},w=()=>{if(!m.signal.aborted)return;const e=m.signal.reason;throw e instanceof Error?e:new Error(String(e??"runtime dispatch aborted"))};try{const a=dispatchRuntimeReplyWithRoute({runtime:n,pluginConfig:e.pluginConfig,preparedPrompt:t.dispatchInput,replyRoute:l,replySinks:{im:y},onDispatchInvoked:()=>{M=!0}}),i=void 0!==c?Promise.race([a,new Promise((e,n)=>{b=setTimeout(()=>{$=new Error(`IM runtime dispatch timed out after ${c}ms`),r.warn?.(`[im] runtime dispatch timeout requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} timeout_ms=${c}`),m.abort($),n($)},c)})]):a,d=await i.finally(()=>{b&&(clearTimeout(b),b=void 0)});w(),await _(),w();const u=await f("complete");let I=!1;try{await p.finalize({stopReason:"end_turn"})}catch(e){I=!0,r.warn?.(`[im] runtime reply stream finalize failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId}: ${formatErrMessage(e)}`)}return r.info?.(`[im] runtime dispatch completed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} counts=${JSON.stringify(d.counts)} elapsed_ms=${Date.now()-o}`),e.onLifecycleEvent?.({type:"dispatch_complete",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,counts:d.counts,...I?{terminalWriteFailed:!0}:{},...u?{reasoningTerminalWriteFailed:!0}:{}}),!0}catch(n){await _();const a=m.signal.aborted,i=a?m.signal.reason:void 0,o=a&&"string"==typeof i&&i.startsWith("chat_cancel"),d=void 0!==$;let l="runtime_error";d?l="timeout":a&&(l="abort");const c=formatErrMessage(n);o?await f("cancel"):d?await f("fail","timeout"):await f("fail");try{o?await p.cancel({stopReason:"user_stop"}):d?await p.fail({body:c,stopReason:"timeout"}):await p.fail({body:c||"execution failed"})}catch(e){r.warn?.(`[im] runtime stream terminal write failed requestId=${s} channel=${t.channelId}: ${formatErrMessage(e)}`)}throw r.warn?.(`[im] runtime dispatch failed requestId=${s} channel=${t.channelId} in_reply_to=${t.inboundMessageId} cause=${l} dispatchInvoked=${M} error=${c}`),e.onLifecycleEvent?.({type:"dispatch_error",requestId:s,channelId:t.channelId,inboundMessageId:t.inboundMessageId,error:c,cause:l}),n}finally{i.unregister(u)}};
@@ -1 +1 @@
1
- import{describe,it,expect,beforeEach}from"vitest";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{PromptRunStore,promptRunKey}from"./runtime/prompt-run-store.js";class FakeMessageStream{openEnvelope;body="";isTerminated=!1;thinking=[];failNextTerminal=!1;finalized=null;constructor(e){this.openEnvelope=e}async opened(){return this.openEnvelope}appendBody(e){this.body+=e}setBody(e){this.body=e}addPart(){}replacePart(){}appendThinking(e,t){this.thinking.push({text:e,state:t})}appendToolUse(){}appendToolResult(){}maybeThrowTerminal(){if(this.failNextTerminal)throw this.failNextTerminal=!1,new Error("terminal PATCH failed")}async finalize(e){return this.maybeThrowTerminal(),this.finalized={state:"completed",stopReason:e?.stopReason??"end_turn",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"completed"}}async fail(e){return this.finalized={state:"failed",stopReason:e?.stopReason??"error",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"failed"}}async cancel(e){return this.finalized={state:"cancelled",stopReason:e?.stopReason??"user_stop",body:this.body},this.isTerminated=!0,{id:"env_fake",state:"cancelled"}}async refuse(e){return this.finalized={state:"refused",stopReason:e?.stopReason??"refused",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"refused"}}}class FakeClient{startStreamCalls=[];streams=[];nextOpenIdempotent=!1;messages={startStream:e=>{this.startStreamCalls.push({conversationId:e.conversationId,id:e.id??"",replyTo:e.replyTo??"",type:e.type});const t=new FakeMessageStream({id:e.id??"env_fake",state:"streaming",idempotent:this.nextOpenIdempotent});return this.nextOpenIdempotent=!1,this.streams.push(t),t}}}const makeRuntime=e=>({channel:{reply:{dispatchReplyFromConfig:async t=>{const a=t.replyOptions?.abortSignal;if(!a)throw new Error("expected abortSignal on replyOptions");return e({dispatcher:t.dispatcher,replyOptions:t.replyOptions,abortSignal:a})}}}}),stubLogger=()=>{const e=[],t=t=>a=>e.push(`[${t}] ${a}`);return{lines:e,debug:t("debug"),info:t("info"),warn:t("warn"),error:t("error")}},baseArgs=(e,t,a,s,n={})=>({runtime:e,prompt:{channelId:"ch_t1",inboundMessageId:"msg_t1",selfPrincipalId:"agent_self_t1",dispatchInput:{message:"hello agent"},...n.prompt??{}},client:t,logger:stubLogger(),promptRunStore:a,origin:"im",onLifecycleEvent:e=>s.push(e),...n});describe("dispatchImInboundPrompt",()=>{let e,t,a;beforeEach(()=>{e=new FakeClient,t=new PromptRunStore,a=[]}),it("happy path: emits dispatch_start + dispatch_complete and finalizes the stream",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onPartialReply({text:"Hi"}),t.onPartialReply({text:"Hi there"}),e.sendFinalReply({text:"Hi there!"}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(1);const o=e.streams[0];expect(o.body).toBe("Hi there!"),expect(o.finalized).toEqual({state:"completed",stopReason:"end_turn",body:"Hi there!"}),expect(a.map(e=>e.type)).toEqual(["dispatch_start","dispatch_complete"]),expect(t.size()).toBe(0)}),it("skip path: returns false when runtime exposes no dispatchReplyFromConfig",async()=>{const s=await dispatchImInboundPrompt(baseArgs({},e,t,a));expect(s).toBe(!1),expect(e.streams).toHaveLength(0),expect(a).toEqual([{type:"skipped",reason:"reply_api_unavailable",channelId:"ch_t1",inboundMessageId:"msg_t1"}])}),it("idempotent stream open skips runtime dispatch",async()=>{e.nextOpenIdempotent=!0;let s=!1;const n=makeRuntime(async()=>(s=!0,{counts:{tool:0,block:0,final:0}})),o=await dispatchImInboundPrompt(baseArgs(n,e,t,a));expect(o).toBe(!1),expect(s).toBe(!1),expect(e.streams).toHaveLength(1),expect(e.streams[0].finalized).toBeNull(),expect(a).toEqual([{type:"skipped",reason:"idempotent_reply_exists",channelId:"ch_t1",inboundMessageId:"msg_t1"}]),expect(t.size()).toBe(0)}),it("timeout path: AbortController fires, stream is failed with stopReason=timeout",async()=>{const s=makeRuntime(async({abortSignal:e})=>{throw await new Promise(t=>{e.addEventListener("abort",()=>t())}),new Error(e.reason instanceof Error?e.reason.message:String(e.reason))});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a,{promptTimeoutMs:5}))).rejects.toThrow(/timed out after 5ms/);const n=e.streams[0];expect(n.finalized?.state).toBe("failed"),expect(n.finalized?.stopReason).toBe("timeout"),expect(n.finalized?.body).toMatch(/timed out/);const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({cause:"timeout"}),expect(t.size()).toBe(0)}),it("chat_cancel path: PromptRunStore.abort fires user_stop → stream.cancel()",async()=>{let s;const n=new Promise(e=>{s=e}),o=makeRuntime(async({abortSignal:e})=>(s(),await new Promise((t,a)=>{e.addEventListener("abort",()=>{const t=e.reason;a(t instanceof Error?t:new Error(String(t)))})}),{counts:{tool:0,block:0,final:0}})),r=dispatchImInboundPrompt(baseArgs(o,e,t,a));await n;const i=t.abort(promptRunKey("ch_t1","msg_t1"),"chat_cancel:user_request");expect(i.ok).toBe(!0),await expect(r).rejects.toThrow(/chat_cancel:user_request/);const c=e.streams[0];expect(c.finalized?.state).toBe("cancelled"),expect(c.finalized?.stopReason).toBe("user_stop");const p=a.find(e=>"dispatch_error"===e.type);expect(p).toMatchObject({cause:"abort"}),expect(t.size()).toBe(0)}),it("runtime error path: stream is failed with the runtime's error message",async()=>{const s=makeRuntime(async()=>{throw new Error("LLM exploded")});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a))).rejects.toThrow(/LLM exploded/);const n=e.streams[0];expect(n.finalized?.state).toBe("failed"),expect(n.finalized?.body).toBe("LLM exploded");const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({cause:"runtime_error"}),expect(t.size()).toBe(0)}),it("registers with PromptRunStore between dispatch_start and dispatch_complete",async()=>{let s=-1;const n=makeRuntime(async()=>(s=t.size(),{counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(n,e,t,a)),expect(s).toBe(1),expect(t.size()).toBe(0)}),it("duplicate inboundMessageId returns false and emits dispatch_error with cause=abort",async()=>{t.register(promptRunKey("ch_t1","msg_t1"),{requestId:"im_req_existing",channelId:"ch_t1",abortController:new AbortController,startedAt:Date.now()});const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!1),expect(e.streams).toHaveLength(0);const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({error:"duplicate_inbound_message_id",cause:"abort"})}),it("uses inboundMessageId as replyTo by default",async()=>{const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(s,e,t,a)),expect(e.startStreamCalls[0].replyTo).toBe("msg_t1")}),it("honors inReplyTo override",async()=>{const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(s,e,t,a,{inReplyTo:"custom_parent_msg"})),expect(e.startStreamCalls[0].replyTo).toBe("custom_parent_msg")}),it("reasoning happy path: opens an agent_reasoning row, finalizes it completed, backfills thinking",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onReasoningStream({text:"let me think"}),t.onReasoningStream({text:"let me think hard"}),t.onReasoningEnd(),e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(2);const o=e.streams[0],r=e.streams[1];expect(e.startStreamCalls[1].type).toBe("agent_reasoning"),expect(e.startStreamCalls[1].replyTo).toBe("msg_t1"),expect(r.body).toBe("let me think hard"),expect(r.finalized).toMatchObject({state:"completed",stopReason:"end_turn"}),expect(o.finalized?.state).toBe("completed"),expect(o.thinking).toEqual([{text:"let me think hard",state:"done"}])}),it("reasoning timeout path: reasoning row is failed (mirrors the failed reply)",async()=>{const s=makeRuntime(async({replyOptions:e,abortSignal:t})=>{throw e.onReasoningStream({text:"thinking..."}),await new Promise(e=>{t.addEventListener("abort",()=>e())}),new Error(t.reason instanceof Error?t.reason.message:String(t.reason))});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a,{promptTimeoutMs:5}))).rejects.toThrow(/timed out after 5ms/),expect(e.streams).toHaveLength(2);const n=e.streams[0],o=e.streams[1];expect(n.finalized?.state).toBe("failed"),expect(o.finalized?.state).toBe("failed"),expect(o.finalized?.stopReason).toBe("timeout")}),it("reasoning chat_cancel path: reasoning row is cancelled (mirrors the cancelled reply)",async()=>{let s;const n=new Promise(e=>{s=e}),o=makeRuntime(async({replyOptions:e,abortSignal:t})=>(e.onReasoningStream({text:"thinking..."}),s(),await new Promise((e,a)=>{t.addEventListener("abort",()=>{const e=t.reason;a(e instanceof Error?e:new Error(String(e)))})}),{counts:{tool:0,block:0,final:0}})),r=dispatchImInboundPrompt(baseArgs(o,e,t,a));await n,t.abort(promptRunKey("ch_t1","msg_t1"),"chat_cancel:user_request"),await expect(r).rejects.toThrow(/chat_cancel:user_request/),expect(e.streams).toHaveLength(2);const i=e.streams[0],c=e.streams[1];expect(i.finalized?.state).toBe("cancelled"),expect(c.finalized?.state).toBe("cancelled"),expect(c.finalized?.stopReason).toBe("user_stop")}),it("reasoning runtime error path: reasoning row is failed",async()=>{const s=makeRuntime(async({replyOptions:e})=>{throw e.onReasoningStream({text:"thinking..."}),new Error("LLM exploded")});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a))).rejects.toThrow(/LLM exploded/),expect(e.streams).toHaveLength(2),expect(e.streams[0].finalized?.state).toBe("failed"),expect(e.streams[1].finalized?.state).toBe("failed")}),it("no reasoning chunk: only the reply row is opened (terminateReasoning is a no-op)",async()=>{const s=makeRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(1),expect(e.streams[0].finalized?.state).toBe("completed")}),it("reasoning finalize failure: flags reasoningTerminalWriteFailed, reply unaffected",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onReasoningStream({text:"thinking..."}),t.onReasoningEnd(),e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=e.messages.startStream;e.messages.startStream=t=>{const a=n(t);return 2===e.streams.length&&(a.failNextTerminal=!0),a};const o=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(o).toBe(!0),expect(e.streams).toHaveLength(2),expect(e.streams[0].finalized?.state).toBe("completed");const r=a.find(e=>"dispatch_complete"===e.type);expect(r).toMatchObject({reasoningTerminalWriteFailed:!0}),expect(r).not.toHaveProperty("terminalWriteFailed"),expect(a.find(e=>"dispatch_error"===e.type)).toBeUndefined()}),it("success-path finalize failure: returns true and flags terminalWriteFailed",async()=>{const s=makeRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=e.messages.startStream;e.messages.startStream=t=>{const a=n(t);return 1===e.streams.length&&(a.failNextTerminal=!0),a};const o=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(o).toBe(!0);const r=a.find(e=>"dispatch_complete"===e.type);expect(r).toMatchObject({terminalWriteFailed:!0}),expect(a.find(e=>"dispatch_error"===e.type)).toBeUndefined()})});
1
+ import{describe,it,expect,beforeEach}from"vitest";import{dispatchImInboundPrompt}from"./im-runtime-dispatch.js";import{PromptRunStore,promptRunKey}from"./runtime/prompt-run-store.js";class FakeMessageStream{openEnvelope;body="";isTerminated=!1;thinking=[];failNextTerminal=!1;finalized=null;constructor(e){this.openEnvelope=e}async opened(){return this.openEnvelope}appendBody(e){this.body+=e}setBody(e){this.body=e}addPart(){}replacePart(){}appendThinking(e,t){this.thinking.push({text:e,state:t})}appendToolUse(){}appendToolResult(){}maybeThrowTerminal(){if(this.failNextTerminal)throw this.failNextTerminal=!1,new Error("terminal PATCH failed")}async finalize(e){return this.maybeThrowTerminal(),this.finalized={state:"completed",stopReason:e?.stopReason??"end_turn",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"completed"}}async fail(e){return this.finalized={state:"failed",stopReason:e?.stopReason??"error",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"failed"}}async cancel(e){return this.finalized={state:"cancelled",stopReason:e?.stopReason??"user_stop",body:this.body},this.isTerminated=!0,{id:"env_fake",state:"cancelled"}}async refuse(e){return this.finalized={state:"refused",stopReason:e?.stopReason??"refused",body:e?.body??this.body},this.isTerminated=!0,{id:"env_fake",state:"refused"}}}class FakeClient{startStreamCalls=[];streams=[];nextOpenIdempotent=!1;messages={startStream:e=>{this.startStreamCalls.push({conversationId:e.conversationId,id:e.id??"",replyTo:e.replyTo??"",type:e.type});const t=new FakeMessageStream({id:e.id??"env_fake",state:"streaming",idempotent:this.nextOpenIdempotent});return this.nextOpenIdempotent=!1,this.streams.push(t),t}}}const makeRuntime=e=>({channel:{reply:{dispatchReplyFromConfig:async t=>{const a=t.replyOptions?.abortSignal;if(!a)throw new Error("expected abortSignal on replyOptions");return e({dispatcher:t.dispatcher,replyOptions:t.replyOptions,abortSignal:a})}}}}),stubLogger=()=>{const e=[],t=t=>a=>e.push(`[${t}] ${a}`);return{lines:e,debug:t("debug"),info:t("info"),warn:t("warn"),error:t("error")}},baseArgs=(e,t,a,s,n={})=>({runtime:e,prompt:{channelId:"ch_t1",inboundMessageId:"msg_t1",selfPrincipalId:"agent_self_t1",dispatchInput:{message:"hello agent"},...n.prompt??{}},client:t,logger:stubLogger(),promptRunStore:a,origin:"im",onLifecycleEvent:e=>s.push(e),...n});describe("dispatchImInboundPrompt",()=>{let e,t,a;beforeEach(()=>{e=new FakeClient,t=new PromptRunStore,a=[]}),it("happy path: emits dispatch_start + dispatch_complete and finalizes the stream",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onPartialReply({text:"Hi"}),t.onPartialReply({text:"Hi there"}),e.sendFinalReply({text:"Hi there!"}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(1);const o=e.streams[0];expect(o.body).toBe("Hi there!"),expect(o.finalized).toEqual({state:"completed",stopReason:"end_turn",body:"Hi there!"}),expect(a.map(e=>e.type)).toEqual(["dispatch_start","dispatch_complete"]),expect(t.size()).toBe(0)}),it("skip path: returns false when runtime exposes no dispatchReplyFromConfig",async()=>{const s=await dispatchImInboundPrompt(baseArgs({},e,t,a));expect(s).toBe(!1),expect(e.streams).toHaveLength(0),expect(a).toEqual([{type:"skipped",reason:"reply_api_unavailable",channelId:"ch_t1",inboundMessageId:"msg_t1"}])}),it("idempotent stream open skips runtime dispatch",async()=>{e.nextOpenIdempotent=!0;let s=!1;const n=makeRuntime(async()=>(s=!0,{counts:{tool:0,block:0,final:0}})),o=await dispatchImInboundPrompt(baseArgs(n,e,t,a));expect(o).toBe(!1),expect(s).toBe(!1),expect(e.streams).toHaveLength(1),expect(e.streams[0].finalized).toBeNull(),expect(a).toEqual([{type:"skipped",reason:"idempotent_reply_exists",channelId:"ch_t1",inboundMessageId:"msg_t1"}]),expect(t.size()).toBe(0)}),it("timeout path: AbortController fires, stream is failed with stopReason=timeout",async()=>{const s=makeRuntime(async({abortSignal:e})=>{throw await new Promise(t=>{e.addEventListener("abort",()=>t())}),new Error(e.reason instanceof Error?e.reason.message:String(e.reason))});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a,{promptTimeoutMs:5}))).rejects.toThrow(/timed out after 5ms/);const n=e.streams[0];expect(n.finalized?.state).toBe("failed"),expect(n.finalized?.stopReason).toBe("timeout"),expect(n.finalized?.body).toMatch(/timed out/);const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({cause:"timeout"}),expect(t.size()).toBe(0)}),it("chat_cancel path: PromptRunStore.abort fires user_stop → stream.cancel()",async()=>{let s;const n=new Promise(e=>{s=e}),o=makeRuntime(async({abortSignal:e})=>(s(),await new Promise((t,a)=>{e.addEventListener("abort",()=>{const t=e.reason;a(t instanceof Error?t:new Error(String(t)))})}),{counts:{tool:0,block:0,final:0}})),r=dispatchImInboundPrompt(baseArgs(o,e,t,a));await n;const i=t.abort(promptRunKey("ch_t1","msg_t1"),"chat_cancel:user_request");expect(i.ok).toBe(!0),await expect(r).rejects.toThrow(/chat_cancel:user_request/);const c=e.streams[0];expect(c.finalized?.state).toBe("cancelled"),expect(c.finalized?.stopReason).toBe("user_stop");const p=a.find(e=>"dispatch_error"===e.type);expect(p).toMatchObject({cause:"abort"}),expect(t.size()).toBe(0)}),it("chat_cancel path: a runtime that resolves after abort still cancels the stream",async()=>{let s;const n=new Promise(e=>{s=e}),o=makeRuntime(async({abortSignal:e})=>(s(),await new Promise(t=>{e.addEventListener("abort",()=>t())}),{counts:{tool:1,block:0,final:0}})),r=dispatchImInboundPrompt(baseArgs(o,e,t,a));await n;const i=t.abort(promptRunKey("ch_t1","msg_t1"),"chat_cancel:user_stop");expect(i.ok).toBe(!0),await expect(r).rejects.toThrow(/chat_cancel:user_stop/),expect(e.streams[0].finalized).toMatchObject({state:"cancelled",stopReason:"user_stop"}),expect(a.find(e=>"dispatch_complete"===e.type)).toBeUndefined(),expect(a.find(e=>"dispatch_error"===e.type)).toMatchObject({cause:"abort"}),expect(t.size()).toBe(0)}),it("runtime error path: stream is failed with the runtime's error message",async()=>{const s=makeRuntime(async()=>{throw new Error("LLM exploded")});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a))).rejects.toThrow(/LLM exploded/);const n=e.streams[0];expect(n.finalized?.state).toBe("failed"),expect(n.finalized?.body).toBe("LLM exploded");const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({cause:"runtime_error"}),expect(t.size()).toBe(0)}),it("registers with PromptRunStore between dispatch_start and dispatch_complete",async()=>{let s=-1;const n=makeRuntime(async()=>(s=t.size(),{counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(n,e,t,a)),expect(s).toBe(1),expect(t.size()).toBe(0)}),it("duplicate inboundMessageId returns false and emits dispatch_error with cause=abort",async()=>{t.register(promptRunKey("ch_t1","msg_t1"),{requestId:"im_req_existing",channelId:"ch_t1",abortController:new AbortController,startedAt:Date.now()});const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!1),expect(e.streams).toHaveLength(0);const o=a.find(e=>"dispatch_error"===e.type);expect(o).toMatchObject({error:"duplicate_inbound_message_id",cause:"abort"})}),it("uses inboundMessageId as replyTo by default",async()=>{const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(s,e,t,a)),expect(e.startStreamCalls[0].replyTo).toBe("msg_t1")}),it("honors inReplyTo override",async()=>{const s=makeRuntime(async()=>({counts:{tool:0,block:0,final:0}}));await dispatchImInboundPrompt(baseArgs(s,e,t,a,{inReplyTo:"custom_parent_msg"})),expect(e.startStreamCalls[0].replyTo).toBe("custom_parent_msg")}),it("reasoning happy path: opens an agent_reasoning row, finalizes it completed, backfills thinking",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onReasoningStream({text:"let me think"}),t.onReasoningStream({text:"let me think hard"}),t.onReasoningEnd(),e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(2);const o=e.streams[0],r=e.streams[1];expect(e.startStreamCalls[1].type).toBe("agent_reasoning"),expect(e.startStreamCalls[1].replyTo).toBe("msg_t1"),expect(r.body).toBe("let me think hard"),expect(r.finalized).toMatchObject({state:"completed",stopReason:"end_turn"}),expect(o.finalized?.state).toBe("completed"),expect(o.thinking).toEqual([{text:"let me think hard",state:"done"}])}),it("reasoning timeout path: reasoning row is failed (mirrors the failed reply)",async()=>{const s=makeRuntime(async({replyOptions:e,abortSignal:t})=>{throw e.onReasoningStream({text:"thinking..."}),await new Promise(e=>{t.addEventListener("abort",()=>e())}),new Error(t.reason instanceof Error?t.reason.message:String(t.reason))});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a,{promptTimeoutMs:5}))).rejects.toThrow(/timed out after 5ms/),expect(e.streams).toHaveLength(2);const n=e.streams[0],o=e.streams[1];expect(n.finalized?.state).toBe("failed"),expect(o.finalized?.state).toBe("failed"),expect(o.finalized?.stopReason).toBe("timeout")}),it("reasoning chat_cancel path: reasoning row is cancelled (mirrors the cancelled reply)",async()=>{let s;const n=new Promise(e=>{s=e}),o=makeRuntime(async({replyOptions:e,abortSignal:t})=>(e.onReasoningStream({text:"thinking..."}),s(),await new Promise((e,a)=>{t.addEventListener("abort",()=>{const e=t.reason;a(e instanceof Error?e:new Error(String(e)))})}),{counts:{tool:0,block:0,final:0}})),r=dispatchImInboundPrompt(baseArgs(o,e,t,a));await n,t.abort(promptRunKey("ch_t1","msg_t1"),"chat_cancel:user_request"),await expect(r).rejects.toThrow(/chat_cancel:user_request/),expect(e.streams).toHaveLength(2);const i=e.streams[0],c=e.streams[1];expect(i.finalized?.state).toBe("cancelled"),expect(c.finalized?.state).toBe("cancelled"),expect(c.finalized?.stopReason).toBe("user_stop")}),it("reasoning runtime error path: reasoning row is failed",async()=>{const s=makeRuntime(async({replyOptions:e})=>{throw e.onReasoningStream({text:"thinking..."}),new Error("LLM exploded")});await expect(dispatchImInboundPrompt(baseArgs(s,e,t,a))).rejects.toThrow(/LLM exploded/),expect(e.streams).toHaveLength(2),expect(e.streams[0].finalized?.state).toBe("failed"),expect(e.streams[1].finalized?.state).toBe("failed")}),it("no reasoning chunk: only the reply row is opened (terminateReasoning is a no-op)",async()=>{const s=makeRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(n).toBe(!0),expect(e.streams).toHaveLength(1),expect(e.streams[0].finalized?.state).toBe("completed")}),it("reasoning finalize failure: flags reasoningTerminalWriteFailed, reply unaffected",async()=>{const s=makeRuntime(async({dispatcher:e,replyOptions:t})=>(t.onReasoningStream({text:"thinking..."}),t.onReasoningEnd(),e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=e.messages.startStream;e.messages.startStream=t=>{const a=n(t);return 2===e.streams.length&&(a.failNextTerminal=!0),a};const o=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(o).toBe(!0),expect(e.streams).toHaveLength(2),expect(e.streams[0].finalized?.state).toBe("completed");const r=a.find(e=>"dispatch_complete"===e.type);expect(r).toMatchObject({reasoningTerminalWriteFailed:!0}),expect(r).not.toHaveProperty("terminalWriteFailed"),expect(a.find(e=>"dispatch_error"===e.type)).toBeUndefined()}),it("success-path finalize failure: returns true and flags terminalWriteFailed",async()=>{const s=makeRuntime(async({dispatcher:e})=>(e.sendFinalReply({text:"Answer."}),{counts:{tool:0,block:0,final:1}})),n=e.messages.startStream;e.messages.startStream=t=>{const a=n(t);return 1===e.streams.length&&(a.failNextTerminal=!0),a};const o=await dispatchImInboundPrompt(baseArgs(s,e,t,a));expect(o).toBe(!0);const r=a.find(e=>"dispatch_complete"===e.type);expect(r).toMatchObject({terminalWriteFailed:!0}),expect(a.find(e=>"dispatch_error"===e.type)).toBeUndefined()})});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beeos-claw",
3
- "version": "0.5.48",
3
+ "version": "0.5.49",
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>",