beeos-claw 0.5.60 → 0.5.62
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.
- package/dist/acp-gateway/bridge-core-cron-timeout.test.js +1 -1
- package/dist/acp-gateway/bridge-core-session-mutation-lifecycle.test.js +1 -1
- package/dist/acp-gateway/bridge-core.js +1 -1
- package/dist/acp-server-chat-abort.test.js +1 -0
- package/dist/acp-server.js +1 -1
- package/dist/gateway-client-chat-abort.test.js +1 -0
- package/dist/gateway-client.js +1 -1
- package/dist/im-inbound-handler.js +1 -1
- package/dist/im-inbound-handler.test.js +1 -1
- package/dist/im-runtime-dispatch.js +1 -1
- package/dist/im-runtime-dispatch.test.js +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/im-reply-sink.js +1 -1
- package/dist/runtime/runtime-restart-scheduler.js +1 -0
- package/dist/runtime/runtime-restart-scheduler.test.js +1 -0
- package/dist/runtime/session-mutation-coordinator.js +1 -1
- package/dist/runtime/session-mutation-coordinator.test.js +1 -1
- package/dist/test-fixtures/im-runtime.fixture.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0;const g=(t,n,r)=>{const o=u;return e.stream.appendToolUse(t,n,r,"streaming"),u+=1,o},m=()=>(d+=1,`tool_${e.requestId}_${d}`),c=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},p=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},f=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:n,index:r}]=t;return l.splice(r,1),n},h=(e,t)=>{i.set(e,t)},I=e=>i.get(e)??e,y=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),v=(t,n,r,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==n?n:i.name,l=!y(i.arguments)&&y(r)?r:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,{type:"tool_use",id:i.id,name:s,arguments:l,state:i.state}),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}const l={id:t,name:n,arguments:r,partIndex:g(t,n,r),state:"streaming",itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},$=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,{type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done"}),t.state="done")},x=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(x).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${x(t)}`).join(",")}}`},b=(e,t,n)=>{const r=i.get(e);if(r)return v(r,t??"tool",n??{},!1);if(s.has(e))return v(e,t??"tool",n??{},!1);const o=p(t);return o?(h(e,o),v(o,t??"tool",n??{},!1)):v(e,t??"tool",n??{},!1)},_=(e,t)=>{const n=i.get(e);if(n)return v(n,t,{},!1);if(s.has(e))return v(e,t,{},!1);const r=(e=>{const t=a.filter(t=>{const n=s.get(t);return!(void 0===n||e&&n.name!==e||n.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(r)return h(e,r),v(r,t,{},!1);const o=f(t);return v(e,t,o?.arguments??{},!1)},k={tool:0,block:0,final:0};let S=!1;const R=(t,n)=>{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):!S||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{n()}catch(n){const r=n instanceof Error?n.message:String(n);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${r}`)}})};return{dispatcher:{sendToolResult:t=>(R("tool_progress",()=>{const n=resolveRuntimeToolCallId(t),r=resolveRuntimeToolName(t),o=n?void 0:r?p(r):p(),i=n||o?void 0:f(r);if(!n&&!o&&!i)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${r??"unknown"} pending=${a.length}`);const l=n?b(n,r,resolveRuntimeToolArguments(t)):v(o??m(),r??s.get(o??"")?.name??i.name,y(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):i?.arguments??{},!1);k.tool+=1,((t,n,r)=>{const o=`${r?"1":"0"}:${x(n)}`;if(t.progressSignature===o)return;const s={type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:n,...r?{is_error:!0}:{}}};if(void 0===t.progressPartIndex){const n=u;return e.stream.addPart(s),t.progressPartIndex=n,t.progressSignature=o,void(u+=1)}e.stream.replacePart(t.progressPartIndex,s),t.progressSignature=o})(l,(e=>{const t=[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e));return isNonEmptyString(e.text)&&t.length>0?{text:e.text,media_urls:t}:isNonEmptyString(e.text)?e.text:1===t.length?t[0]:t.length>1?{media_urls:t}:!0===e.isError?"Tool execution failed.":""})(t),!0===t.isError)}),!0),sendBlockReply:n=>(R("block_reply",()=>{k.block+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),sendFinalReply:n=>(R("final_reply",()=>{k.final+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),waitForIdle:()=>e.callbackQueue.waitForIdle(),getQueuedCounts:()=>({...k}),markComplete:t=>{if(1===l.length){const e=l.shift();v(m(),e.name,e.arguments,!1)}for(const n of s.values())try{"success"===t&&$(n),c(n.id)}catch(t){const r=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${n.id} error=${r}`)}l.length=0,S=!0}},replyOptions:{abortSignal:e.abortSignal,onAgentRunStart:t=>{e.onAgentRunStart?.(t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:n=>{R("partial",()=>{const r=extractBodyUpdate(t,n.text);switch(r.kind){case"append":e.stream.appendBody(r.text);break;case"replace":e.stream.setBody(r.text)}})},onReasoningStream:t=>{R("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!n&&(n=e.openReasoningStream?.(),!n))return;const s=t.text;""===o&&""!==r&&(n.appendBody("\n"),r+="\n"),s!==o&&(s.startsWith(o)?(n.appendBody(s.slice(o.length)),o=s):o.startsWith(s)||(n.setBody(r+s),o=s))})},onReasoningEnd:()=>{R("reasoning_end",()=>{var t;""!==o&&(t=o,e.stream.appendThinking(t,"done"),u+=1,r+=o,o="")})},onAssistantMessageStart:()=>{e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)},onToolStart:t=>{R("tool_start",()=>{const n="string"==typeof t.phase?t.phase:"start",r=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===n){if(o){const e=I(o);s.has(e)&&v(e,r,resolveRuntimeToolArguments(t),!0)}return}const i=resolveRuntimeToolArguments(t);if(o){const e=b(o,r,i);return e.toolStartObserved=!0,void(a.includes(e.id)||"streaming"!==e.state||a.push(e.id))}const d=(u=r,a.map(e=>s.get(e)).filter(e=>void 0!==e&&(!u||e.name===u)&&e.itemEventObserved));var u;if(1===d.length){return void(v(d[0].id,r,i,!0).toolStartObserved=!0)}d.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${r} candidates=${d.length}`):l.push({name:r,arguments:i})})},onItemEvent:e=>{R("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const n="string"==typeof e.phase?e.phase.toLowerCase():"update",r=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!r||!o)return;if("update"===n){const e=I(r),t=s.get(e);return void(t&&(v(t.id,o,{},!0).itemEventObserved=!0))}const i=_(r,o);i.itemEventObserved=!0,a.includes(i.id)||"streaming"!==i.state||a.push(i.id)})}}}}
|
|
1
|
+
import{resolveRuntimeToolArguments,resolveRuntimeToolCallId,resolveRuntimeToolName}from"./tool-envelope-metadata.js";const isNonEmptyString=e=>"string"==typeof e&&e.length>0,extractBodyUpdate=(e,t)=>{if(!isNonEmptyString(t))return{kind:"noop"};if(t===e.text)return{kind:"noop"};if(t.startsWith(e.text)){const n=t.slice(e.text.length);return e.text=t,{kind:"append",text:n}}return e.text.startsWith(t)?{kind:"noop"}:(e.text=t,{kind:"replace",text:t})};export function createImReplySink(e){const t={text:""};let n,r="",o="";const s=new Map,i=new Map,a=[],l=[];let d=0,u=0;const g=(t,n,r)=>{const o=u;return e.stream.appendToolUse(t,n,r,"streaming"),u+=1,o},m=()=>(d+=1,`tool_${e.requestId}_${d}`),c=e=>{const t=a.indexOf(e);t>=0&&a.splice(t,1)},p=e=>{const t=e?a.filter(t=>s.get(t)?.name===e):a;return 1===t.length?t[0]:void 0},f=e=>{const t=l.map((e,t)=>({hint:e,index:t})).filter(({hint:t})=>!e||t.name===e);if(1!==t.length)return;const[{hint:n,index:r}]=t;return l.splice(r,1),n},h=(e,t)=>{i.set(e,t)},I=e=>i.get(e)??e,y=e=>null!=e&&("object"!=typeof e||(Array.isArray(e)?e.length>0:Object.keys(e).length>0)),v=(t,n,r,o)=>{const i=s.get(t);if(i){const s="tool"===i.name&&"tool"!==n?n:i.name,l=!y(i.arguments)&&y(r)?r:i.arguments;return s===i.name&&l===i.arguments||(e.stream.replacePart(i.partIndex,{type:"tool_use",id:i.id,name:s,arguments:l,state:i.state}),i.name=s,i.arguments=l),o&&!a.includes(t)&&"streaming"===i.state&&a.push(t),i}const l={id:t,name:n,arguments:r,partIndex:g(t,n,r),state:"streaming",itemEventObserved:!1,toolStartObserved:!1};return s.set(t,l),o&&a.push(t),l},$=t=>{"done"!==t.state&&(e.stream.replacePart(t.partIndex,{type:"tool_use",id:t.id,name:t.name,arguments:t.arguments,state:"done"}),t.state="done")},x=e=>{if(null===e||"object"!=typeof e)return JSON.stringify(e)??String(e);if(Array.isArray(e))return`[${e.map(x).join(",")}]`;return`{${Object.entries(e).sort(([e],[t])=>e.localeCompare(t)).map(([e,t])=>`${JSON.stringify(e)}:${x(t)}`).join(",")}}`},b=(e,t,n)=>{const r=i.get(e);if(r)return v(r,t??"tool",n??{},!1);if(s.has(e))return v(e,t??"tool",n??{},!1);const o=p(t);return o?(h(e,o),v(o,t??"tool",n??{},!1)):v(e,t??"tool",n??{},!1)},_=(e,t)=>{const n=i.get(e);if(n)return v(n,t,{},!1);if(s.has(e))return v(e,t,{},!1);const r=(e=>{const t=a.filter(t=>{const n=s.get(t);return!(void 0===n||e&&n.name!==e||n.itemEventObserved)});return 1===t.length?t[0]:void 0})(t);if(r)return h(e,r),v(r,t,{},!1);const o=f(t);return v(e,t,o?.arguments??{},!1)},k={tool:0,block:0,final:0};let S=!1;const R=(t,n)=>{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):!S||(e.logger.debug?.(`[im-sink] ${t} ignored after mark_complete requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`),!1))(t))try{n()}catch(n){const r=n instanceof Error?n.message:String(n);e.logger.warn?.(`[im-sink] ${t} mapping failed requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId} error=${r}`)}})};return{dispatcher:{sendToolResult:t=>(R("tool_progress",()=>{const n=resolveRuntimeToolCallId(t),r=resolveRuntimeToolName(t),o=n?void 0:r?p(r):p(),i=n||o?void 0:f(r);if(!n&&!o&&!i)return void e.logger.debug?.(`[im-sink] ambiguous id-less tool progress ignored requestId=${e.requestId} channel=${e.channelId} tool=${r??"unknown"} pending=${a.length}`);const l=n?b(n,r,resolveRuntimeToolArguments(t)):v(o??m(),r??s.get(o??"")?.name??i.name,y(resolveRuntimeToolArguments(t))?resolveRuntimeToolArguments(t):i?.arguments??{},!1);k.tool+=1,((t,n,r)=>{const o=`${r?"1":"0"}:${x(n)}`;if(t.progressSignature===o)return;const s={type:"custom",kind:"tool_progress",data:{tool_use_id:t.id,content:n,...r?{is_error:!0}:{}}};if(void 0===t.progressPartIndex){const n=u;return e.stream.addPart(s),t.progressPartIndex=n,t.progressSignature=o,void(u+=1)}e.stream.replacePart(t.progressPartIndex,s),t.progressSignature=o})(l,(e=>{const t=[e.mediaUrl,...e.mediaUrls??[]].filter(e=>isNonEmptyString(e));return isNonEmptyString(e.text)&&t.length>0?{text:e.text,media_urls:t}:isNonEmptyString(e.text)?e.text:1===t.length?t[0]:t.length>1?{media_urls:t}:!0===e.isError?"Tool execution failed.":""})(t),!0===t.isError)}),!0),sendBlockReply:n=>(R("block_reply",()=>{k.block+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),sendFinalReply:n=>(R("final_reply",()=>{k.final+=1,isNonEmptyString(n.text)&&(e.stream.setBody(n.text),t.text=n.text)}),!0),waitForIdle:()=>e.callbackQueue.waitForIdle(),getQueuedCounts:()=>({...k}),markComplete:t=>{if(1===l.length){const e=l.shift();v(m(),e.name,e.arguments,!1)}for(const n of s.values())try{"success"===t&&$(n),c(n.id)}catch(t){const r=t instanceof Error?t.message:String(t);e.logger.warn?.(`[im-sink] tool finalization failed requestId=${e.requestId} toolUseId=${n.id} error=${r}`)}l.length=0,S=!0}},replyOptions:{...e.runId?{runId:e.runId}:{},abortSignal:e.abortSignal,onAgentRunStart:t=>{e.onAgentRunStart?.(t),e.logger.info?.(`[im-sink] run_start requestId=${e.requestId} runId=${t} channel=${e.channelId} in_reply_to=${e.inboundMessageId} origin=${e.origin??"im"}`)},onPartialReply:n=>{R("partial",()=>{const r=extractBodyUpdate(t,n.text);switch(r.kind){case"append":e.stream.appendBody(r.text);break;case"replace":e.stream.setBody(r.text)}})},onReasoningStream:t=>{R("reasoning",()=>{if(!isNonEmptyString(t.text))return;if(!n&&(n=e.openReasoningStream?.(),!n))return;const s=t.text;""===o&&""!==r&&(n.appendBody("\n"),r+="\n"),s!==o&&(s.startsWith(o)?(n.appendBody(s.slice(o.length)),o=s):o.startsWith(s)||(n.setBody(r+s),o=s))})},onReasoningEnd:()=>{R("reasoning_end",()=>{var t;""!==o&&(t=o,e.stream.appendThinking(t,"done"),u+=1,r+=o,o="")})},onAssistantMessageStart:()=>{e.logger.debug?.(`[im-sink] assistant_message_start requestId=${e.requestId} channel=${e.channelId} in_reply_to=${e.inboundMessageId}`)},onToolStart:t=>{R("tool_start",()=>{const n="string"==typeof t.phase?t.phase:"start",r=resolveRuntimeToolName(t)??"tool",o=resolveRuntimeToolCallId(t);if("update"===n){if(o){const e=I(o);s.has(e)&&v(e,r,resolveRuntimeToolArguments(t),!0)}return}const i=resolveRuntimeToolArguments(t);if(o){const e=b(o,r,i);return e.toolStartObserved=!0,void(a.includes(e.id)||"streaming"!==e.state||a.push(e.id))}const d=(u=r,a.map(e=>s.get(e)).filter(e=>void 0!==e&&(!u||e.name===u)&&e.itemEventObserved));var u;if(1===d.length){return void(v(d[0].id,r,i,!0).toolStartObserved=!0)}d.length>1?e.logger.debug?.(`[im-sink] ambiguous id-less tool start ignored requestId=${e.requestId} channel=${e.channelId} tool=${r} candidates=${d.length}`):l.push({name:r,arguments:i})})},onItemEvent:e=>{R("item_event",()=>{const t="string"==typeof e.kind?e.kind.toLowerCase():"";if(t&&"tool"!==t)return;const n="string"==typeof e.phase?e.phase.toLowerCase():"update",r=resolveRuntimeToolCallId(e)??("string"==typeof e.itemId&&e.itemId.trim()?e.itemId.trim():void 0),o=resolveRuntimeToolName(e)??("string"==typeof e.title&&e.title.trim()?e.title.trim():void 0);if(!r||!o)return;if("update"===n){const e=I(r),t=s.get(e);return void(t&&(v(t.id,o,{},!0).itemEventObserved=!0))}const i=_(r,o);i.itemEventObserved=!0,a.includes(i.id)||"streaming"!==i.state||a.push(i.id)})}}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync}from"node:fs";import{join}from"node:path";export const DEFAULT_RUNTIME_RESTART_DRAIN_MS=1e3;const DEFAULT_POD_AGENT_DIR="/tmp/agent";export function resolveSupervisorPidFile(e=process.env){const r=e.POD_AGENT_DIR?.trim()||"/tmp/agent";return join(r,"supervisor.pid")}const formatError=e=>e instanceof Error?e.message:String(e);export class RuntimeRestartScheduler{drainMs;supervisorPidFile;currentPid;readSupervisorPid;signalProcess;exitProcess;setTimer;scheduled=!1;constructor(e={}){this.drainMs=e.drainMs??1e3,this.supervisorPidFile=e.supervisorPidFile??resolveSupervisorPidFile(),this.currentPid=e.currentPid??process.pid,this.readSupervisorPid=e.readSupervisorPid??(e=>readFileSync(e,"utf8")),this.signalProcess=e.signalProcess??((e,r)=>process.kill(e,r)),this.exitProcess=e.exitProcess??(e=>process.exit(e)),this.setTimer=e.setTimer??((e,r)=>setTimeout(e,r))}schedule(e,r){return this.scheduled?(r.debug?.(`[runtime-restart] restart already scheduled; deduplicated sessionKey=${e.sessionKey} operationId=${e.operationId}`),!1):(this.scheduled=!0,r.error?.(`[runtime-restart] gateway owner restart scheduled after terminal drain drain_ms=${this.drainMs} sessionKey=${e.sessionKey} operationId=${e.operationId} reason=${e.reason}`),this.setTimer(()=>this.restartGatewayOwner(e,r),this.drainMs),!0)}restartGatewayOwner(e,r){try{const s=this.parseSupervisorPid(this.readSupervisorPid(this.supervisorPidFile));return this.signalProcess(s,0),this.signalProcess(s,"SIGHUP"),void r.error?.(`[runtime-restart] sent SIGHUP to gateway supervisor pid=${s} pid_file=${this.supervisorPidFile} sessionKey=${e.sessionKey} operationId=${e.operationId}`)}catch(e){r.warn?.(`[runtime-restart] supervisor restart unavailable; falling back to process.exit(1) pid_file=${this.supervisorPidFile} error=${formatError(e)}`)}this.exitProcess(1)}parseSupervisorPid(e){const r=e.trim();if(!/^[1-9]\d*$/.test(r))throw new Error("invalid supervisor pid file");const s=Number(r);if(!Number.isSafeInteger(s)||s<=1)throw new Error("invalid supervisor pid");if(s===this.currentPid)throw new Error("supervisor pid points to current process");return s}}const processRuntimeRestartScheduler=new RuntimeRestartScheduler;export function scheduleProcessRuntimeRestart(e,r){return processRuntimeRestartScheduler.schedule(e,r)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{describe,expect,it,vi}from"vitest";import{DEFAULT_RUNTIME_RESTART_DRAIN_MS,resolveSupervisorPidFile,RuntimeRestartScheduler}from"./runtime-restart-scheduler.js";const recovery=(e,t)=>({code:"session_recovery_required",sessionKey:e,operationId:t,reason:"quiescence_timeout",surface:"message_service"});describe("RuntimeRestartScheduler",()=>{it("resolves the supervisor contract from POD_AGENT_DIR with /tmp/agent as default",()=>{expect(resolveSupervisorPidFile({})).toBe("/tmp/agent/supervisor.pid"),expect(resolveSupervisorPidFile({POD_AGENT_DIR:"/run/custom-agent"})).toBe("/run/custom-agent/supervisor.pid")}),it("waits for the terminal drain, then SIGHUPs the pid-file supervisor once",()=>{let e;const t=vi.fn((t,r)=>(e=t,1)),r=vi.fn(()=>"4242\n"),i=vi.fn(),s=vi.fn(),o=new RuntimeRestartScheduler({supervisorPidFile:"/tmp/agent/supervisor.pid",currentPid:9e3,readSupervisorPid:r,signalProcess:i,exitProcess:s,setTimer:t});expect(o.schedule(recovery("agent:main:a","op-a"),{})).toBe(!0),expect(o.schedule(recovery("agent:main:b","op-b"),{})).toBe(!1),expect(t).toHaveBeenCalledTimes(1),expect(t).toHaveBeenCalledWith(expect.any(Function),DEFAULT_RUNTIME_RESTART_DRAIN_MS),expect(r).not.toHaveBeenCalled(),expect(i).not.toHaveBeenCalled(),expect(s).not.toHaveBeenCalled(),e(),expect(r).toHaveBeenCalledWith("/tmp/agent/supervisor.pid"),expect(i.mock.calls).toEqual([[4242,0],[4242,"SIGHUP"]]),expect(s).not.toHaveBeenCalled()}),it("falls back to process.exit(1) only after the drain when the pid file is unusable",()=>{let e;const t=vi.fn(),r=vi.fn();new RuntimeRestartScheduler({supervisorPidFile:"/tmp/agent/supervisor.pid",readSupervisorPid:()=>{throw new Error("ENOENT")},signalProcess:t,exitProcess:r,setTimer:t=>(e=t,1)}).schedule(recovery("agent:main:fallback","op-fallback"),{}),expect(r).not.toHaveBeenCalled(),e(),expect(t).not.toHaveBeenCalled(),expect(r).toHaveBeenCalledOnce(),expect(r).toHaveBeenCalledWith(1)}),it("falls back to process.exit(1) when the supervisor cannot accept SIGHUP",()=>{let e;const t=vi.fn((e,t)=>{if("SIGHUP"===t)throw new Error("EPERM")}),r=vi.fn();new RuntimeRestartScheduler({currentPid:9e3,readSupervisorPid:()=>"4242\n",signalProcess:t,exitProcess:r,setTimer:t=>(e=t,1)}).schedule(recovery("agent:main:signal-fallback","op-signal"),{}),e(),expect(t.mock.calls).toEqual([[4242,0],[4242,"SIGHUP"]]),expect(r).toHaveBeenCalledOnce(),expect(r).toHaveBeenCalledWith(1)})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;constructor(e,r,t
|
|
1
|
+
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;onRecoveryRequired;constructor(e,r,o,t={}){this.gate=e,this.quiescence=r,this.logger=o,this.observationTimeoutMs=t.observationTimeoutMs??13e4,this.cancellationTerminalObservationTimeoutMs=t.cancellationTerminalObservationTimeoutMs??3e4,this.setTimer=t.setTimer??((e,r)=>{const o=setTimeout(e,r);return o.unref?.(),o}),this.clearTimer=t.clearTimer??(e=>clearTimeout(e)),this.onRecoveryRequired=t.onRecoveryRequired}tryStartInteractive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"attachable"),o}tryStartExclusive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"exclusive"),o}async acquireInternal(e){const r=await this.gate.acquireInternal(e);return this.register(r,"internal","attachable"),r}bindRun(e,r){if(!e||!r)return;const o=this.states.get(e.operationId);o&&"recovery_required"!==o.phase&&(o.runId=r)}attachOrStartExternal(e,r,o){const t=this.stateForSession(e.sessionKey);if(t)return t.recovery?{ok:!1,reason:"session_busy",recovery:t.recovery}:"exclusive"===t.attachmentPolicy?{ok:!1,reason:"session_busy"}:t.runId&&o&&t.runId!==o?{ok:!1,reason:"run_conflict",recovery:this.fence(t,"run_conflict")}:(!t.runId&&o&&(t.runId=o),{ok:!0,ownership:"observer",ownerOperationId:t.token.operationId,attached:!0});const n=this.tryStartInteractive(e,r);return n.ok?(o&&this.bindRun(n.token,o),{ok:!0,ownership:"owner",token:n.token,attached:!1}):{ok:!1,reason:n.reason}}requestCancel(e,r){const o=this.requireState(e);if(o.recovery)return Promise.resolve({ok:!1,recovery:o.recovery});if(o.cancelPromise)return o.cancelPromise;if(o.terminalKind||o.convergencePromise)return Promise.resolve({ok:!1,reason:"terminal_already_observed"});o.phase="cancelling",o.cancelObservationPromise=new Promise(e=>{o.resolveCancelObservation=e});const t=o.cancelObservationPromise;o.cancelObservationTimer=this.setTimer(()=>{o.terminalKind||o.recovery||this.fence(o,"terminal_not_observed")},this.cancellationTerminalObservationTimeoutMs);const n=Promise.resolve().then(()=>r.chatAbort(e.sessionKey,o.runId)).then(r=>(r.aborted?(o.cancelAcknowledged=!0,this.logger.debug?.(`[session-mutation] remote abort acknowledged sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`)):this.logger.debug?.(`[session-mutation] remote abort not acknowledged; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:r.aborted,terminalObservation:t})).catch(r=>(this.logger.error?.(`[session-mutation] remote abort transport failed; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} error=${r instanceof Error?r.message:String(r)}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:!1,terminalObservation:t}));return o.cancelPromise=n,n}requestCancelForSession(e,r){const o=this.stateForSession(e);return o?"exclusive"===o.attachmentPolicy?Promise.resolve({ok:!1,reason:"not_cancellable"}):this.requestCancel(o.token,r):Promise.resolve({ok:!1,reason:"no_active_mutation"})}converge(e,r){const o=this.requireState(e);return o.convergencePromise||(o.terminalKind=r,this.resolveTerminalObserved(o),o.convergencePromise=this.proveConvergence(o)),o.convergencePromise}release(e){const r=this.states.get(e.operationId);r&&r.token.sessionKey===e.sessionKey&&"ready_to_release"===r.phase&&(this.clearCancellationTimer(r),this.gate.release(r.token),this.states.delete(e.operationId))}releaseBeforeRuntime(e){if(!e)return;const r=this.states.get(e.operationId);r?.recovery||(r&&this.clearCancellationTimer(r),this.gate.release(e),this.states.delete(e.operationId))}requireRecovery(e,r,o){const t=this.requireState(e);return o&&(t.terminalKind=o),this.fence(t,r)}snapshot(e){for(const r of this.states.values())if(r.token.sessionKey===e)return{sessionKey:e,operationId:r.token.operationId,surface:r.surface,attachmentPolicy:r.attachmentPolicy,phase:r.phase,...r.runId?{runId:r.runId}:{},...r.terminalKind?{terminalKind:r.terminalKind}:{},cancelAcknowledged:r.cancelAcknowledged,...r.recovery?{recovery:r.recovery}:{}}}gateSnapshot(e){return this.gate.snapshot(e)}fenceActive(e){for(const r of this.states.values())r.recovery||this.fence(r,e)}register(e,r,o){this.states.set(e.operationId,{token:e,surface:r,attachmentPolicy:o,phase:"active",cancelAcknowledged:!1})}requireState(e){const r=this.states.get(e.operationId);if(!r||r.token.sessionKey!==e.sessionKey)throw new Error("session_mutation_not_registered");return r}stateForSession(e){for(const r of this.states.values())if(r.token.sessionKey===e)return r}async proveConvergence(e){if(e.recovery)return{ok:!1,recovery:e.recovery};if(!e.terminalKind)return{ok:!1,recovery:this.fence(e,"terminal_not_observed")};if(e.recovery)return{ok:!1,recovery:e.recovery};let r;e.phase="awaiting_quiescence";try{r=await this.quiescence.waitForQuiescence(e.token.sessionKey,this.observationTimeoutMs)}catch{return e.recovery?{ok:!1,recovery:e.recovery}:{ok:!1,recovery:this.fence(e,"quiescence_failed")}}if(e.recovery)return{ok:!1,recovery:e.recovery};if(!r.quiescent){const o="session_unresolved"===r.reason?"session_unresolved":"quiescence_timeout";return{ok:!1,recovery:this.fence(e,o,r)}}return e.phase="ready_to_release",this.logger.debug?.(`[session-mutation] convergence proven sessionKey=${e.token.sessionKey} operationId=${e.token.operationId} terminal=${e.terminalKind} elapsed_ms=${r.elapsedMs}`),{ok:!0,permit:{sessionKey:e.token.sessionKey,operationId:e.token.operationId}}}fence(e,r,o){if(e.recovery)return e.recovery;const t={code:"session_recovery_required",sessionKey:e.token.sessionKey,operationId:e.token.operationId,reason:r,surface:e.surface,...e.terminalKind?{terminalKind:e.terminalKind}:{},...o?{quiescence:o}:{}};e.recovery=t,e.phase="recovery_required",this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!1,recovery:t}),e.resolveCancelObservation=void 0,this.logger.error?.(`[session-mutation] recovery required sessionKey=${t.sessionKey} operationId=${t.operationId} reason=${t.reason} surface=${t.surface}`);try{this.onRecoveryRequired?.(t)}catch(e){this.logger.error?.(`[session-mutation] recovery restart scheduling failed sessionKey=${t.sessionKey} operationId=${t.operationId} error=${e instanceof Error?e.message:String(e)}`)}return t}resolveTerminalObserved(e){e.recovery||(this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!0,terminalObserved:!0}),e.resolveCancelObservation=void 0)}clearCancellationTimer(e){e.cancelObservationTimer&&(this.clearTimer(e.cancelObservationTimer),e.cancelObservationTimer=void 0)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"./session-mutation-coordinator.js";const deferred=()=>{let e;return{promise:new Promise(t=>{e=t}),resolve:e}};describe("SessionMutationCoordinator",()=>{it("requires both awaited cancel acknowledgement and terminal quiescence",async()=>{const e=new SessionWriteGate,t=deferred(),n=deferred(),a=new SessionMutationCoordinator(e,{waitForQuiescence:vi.fn(async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:5}))},{}),o=a.tryStartInteractive({sessionKey:"agent:main:one"},"acp");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-1");const s=a.requestCancel(o.token,{chatAbort:vi.fn(async()=>{await n.promise})}),r=a.converge(o.token,"cancelled");await Promise.resolve(),expect(a.snapshot("agent:main:one")?.cancelAcknowledged).toBe(!1),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),n.resolve(),await expect(s).resolves.toMatchObject({ok:!0,acknowledged:!0}),expect(a.snapshot("agent:main:one")?.phase).toBe("awaiting_quiescence"),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),t.resolve();const i=await r;expect(i.ok).toBe(!0),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),i.ok&&a.release(i.permit),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!0)}),it("fails closed and exposes recovery-required when quiescence times out",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:20})},{},{observationTimeoutMs:20}),n=t.tryStartInteractive({sessionKey:"agent:main:fenced"},"message_service");if(!n.ok)throw new Error("expected lease");await expect(t.converge(n.token,"timeout")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_timeout",terminalKind:"timeout"}}),expect(t.snapshot("agent:main:fenced")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:fenced"}).ok).toBe(!1)}),it("fences as quiescence_failed when the observer rejects",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>{throw new Error("observer failed")}},{}),n=t.tryStartInteractive({sessionKey:"agent:main:observer-failed"},"acp");if(!n.ok)throw new Error("expected lease");await expect(t.converge(n.token,"error")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_failed"}}),expect(t.snapshot("agent:main:observer-failed")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:observer-failed"}).ok).toBe(!1)}),it("blocks the same gateway session while allowing different sessions concurrently",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"acp").ok).toBe(!0),expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"message_service").ok).toBe(!1),expect(t.tryStartInteractive({sessionKey:"agent:main:b"},"message_service").ok).toBe(!0)}),it("releases a normally completed mutation only after the caller commits its permit",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:normal"},"acp");if(!n.ok)throw new Error("expected lease");const a=await t.converge(n.token,"completed");expect(a.ok).toBe(!0),expect(e.snapshot("agent:main:normal").active).toBeDefined(),a.ok&&t.release(a.permit),expect(e.snapshot("agent:main:normal").active).toBeUndefined()}),it.each(["acp","message_service"])("applies identical cancel convergence for the %s surface",async e=>{const t=new SessionWriteGate,n=vi.fn().mockResolvedValue(void 0),a=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),o=a.tryStartInteractive({sessionKey:`agent:main:${e}`},e);if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-shared-contract"),await expect(a.requestCancel(o.token,{chatAbort:n})).resolves.toMatchObject({ok:!0,acknowledged:!0});const s=await a.converge(o.token,"cancelled");expect(s.ok).toBe(!0),expect(n).toHaveBeenCalledWith(`agent:main:${e}`,"run-shared-contract"),expect(a.snapshot(`agent:main:${e}`)).toMatchObject({surface:e,phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!0})}),it("requests cancellation from the current owner by session key",async()=>{const e=new SessionWriteGate,t=vi.fn().mockResolvedValue(void 0),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:cancel-by-session"},"message_service");if(!a.ok)throw new Error("expected owner");n.bindRun(a.token,"run-owner"),await expect(n.requestCancelForSession("agent:main:cancel-by-session",{chatAbort:t})).resolves.toMatchObject({ok:!0,acknowledged:!0}),expect(t).toHaveBeenCalledWith("agent:main:cancel-by-session","run-owner"),expect(n.snapshot("agent:main:cancel-by-session")).toMatchObject({operationId:a.token.operationId,phase:"cancelling",cancelAcknowledged:!0});const o=await n.converge(a.token,"cancelled");o.ok&&n.release(o.permit)}),it("reports no active mutation when cancellation has no owner",async()=>{const e=new SessionMutationCoordinator(new SessionWriteGate,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});await expect(e.requestCancelForSession("agent:main:no-owner",{chatAbort:vi.fn()})).resolves.toEqual({ok:!1,reason:"no_active_mutation"})}),it("fences and resolves recovery when cancel ACK is not followed by a terminal",async()=>{const e=new SessionWriteGate;let t;const n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:e=>(t=e,{unref:()=>{}}),clearTimer:()=>{}}),a=n.tryStartInteractive({sessionKey:"agent:main:no-terminal"},"acp");if(!a.ok)throw new Error("expected lease");const o=await n.requestCancel(a.token,{chatAbort:async()=>{}});if(!o.ok)throw new Error("expected cancel ACK");t(),await expect(o.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"terminal_not_observed"}}),expect(n.snapshot("agent:main:no-terminal")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:no-terminal"}).ok).toBe(!1)}),it("clears the cancel observation timer when terminal arrives",async()=>{const e=new SessionWriteGate,t={unref:()=>{}},n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:()=>t,clearTimer:n}),o=a.tryStartInteractive({sessionKey:"agent:main:terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>{}});if(!s.ok)throw new Error("expected cancel ACK");const r=a.converge(o.token,"cancelled");await expect(s.terminalObservation).resolves.toEqual({ok:!0,terminalObserved:!0}),expect(n).toHaveBeenCalledWith(t),await expect(r).resolves.toMatchObject({ok:!0})}),it("rejects late cancel after terminal convergence has started",async()=>{const e=new SessionWriteGate,t=deferred(),n=vi.fn().mockResolvedValue(void 0),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),o=a.tryStartInteractive({sessionKey:"agent:main:late-cancel"},"acp");if(!o.ok)throw new Error("expected lease");const s=a.converge(o.token,"completed");await expect(a.requestCancel(o.token,{chatAbort:n})).resolves.toEqual({ok:!1,reason:"terminal_already_observed"}),expect(n).not.toHaveBeenCalled(),t.resolve(),await expect(s).resolves.toMatchObject({ok:!0})}),it("keeps disconnect recovery fenced when quiescence resolves concurrently",async()=>{const e=new SessionWriteGate,t=deferred(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:disconnect-race"},"acp");if(!a.ok)throw new Error("expected lease");const o=n.converge(a.token,"completed");await Promise.resolve(),n.fenceActive("gateway_disconnected"),t.resolve(),await expect(o).resolves.toMatchObject({ok:!1,recovery:{reason:"gateway_disconnected"}}),expect(n.snapshot("agent:main:disconnect-race")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:disconnect-race"}).ok).toBe(!1)}),it.each([void 0,"run-shared"])("attaches an external run to an existing Message Service owner (owner run=%s)",e=>{const t=new SessionWriteGate,n=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:shared-owner"},"message_service");if(!a.ok)throw new Error("expected owner");e&&n.bindRun(a.token,e);const o=n.attachOrStartExternal({sessionKey:"agent:main:shared-owner"},"acp","run-shared");expect(o).toEqual({ok:!0,ownership:"observer",ownerOperationId:a.token.operationId,attached:!0}),expect(n.snapshot("agent:main:shared-owner")?.runId).toBe("run-shared")}),it("keeps the owner lease active when an attached observer reaches terminal first",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{}),a=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"message_service","run-shared");if(!a.ok||"owner"!==a.ownership)throw new Error("expected owner");const o=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"acp","run-shared");expect(o).toMatchObject({ok:!0,ownership:"observer",attached:!0}),expect(n.snapshot("agent:main:observer-terminal")?.phase).toBe("active"),expect(e.snapshot("agent:main:observer-terminal").active?.operationId).toBe(a.token.operationId),expect(t).not.toHaveBeenCalled();const s=await n.converge(a.token,"completed");expect(s.ok).toBe(!0),s.ok&&n.release(s.permit),expect(e.snapshot("agent:main:observer-terminal").active).toBeUndefined()}),it("returns an owner token when no mutation exists",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}).attachOrStartExternal({sessionKey:"agent:main:new-external"},"acp","run-new");expect(t).toMatchObject({ok:!0,ownership:"owner",attached:!1,token:{sessionKey:"agent:main:new-external"}})}),it("rejects external attachment and cancellation without mutating an exclusive owner",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartExclusive({sessionKey:"agent:main:exclusive"},"acp");if(!n.ok)throw new Error("expected exclusive owner");expect(t.attachOrStartExternal({sessionKey:"agent:main:exclusive"},"message_service","run-external")).toEqual({ok:!1,reason:"session_busy"}),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"}),expect(t.snapshot("agent:main:exclusive")?.runId).toBeUndefined(),expect(e.snapshot("agent:main:exclusive").active?.operationId).toBe(n.token.operationId);const a=vi.fn();await expect(t.requestCancelForSession("agent:main:exclusive",{chatAbort:a})).resolves.toEqual({ok:!1,reason:"not_cancellable"}),expect(a).not.toHaveBeenCalled(),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"})}),it("fences an existing owner when an external run id conflicts",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:conflict"},"message_service");if(!n.ok)throw new Error("expected owner");t.bindRun(n.token,"run-owner"),expect(t.attachOrStartExternal({sessionKey:"agent:main:conflict"},"acp","run-external")).toMatchObject({ok:!1,reason:"run_conflict",recovery:{reason:"run_conflict"}}),expect(t.snapshot("agent:main:conflict")?.phase).toBe("recovery_required")})});
|
|
1
|
+
import{describe,expect,it,vi}from"vitest";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"./session-mutation-coordinator.js";const deferred=()=>{let e;return{promise:new Promise(t=>{e=t}),resolve:e}};describe("SessionMutationCoordinator",()=>{it("allows terminal quiescence to release before the remote abort observation returns",async()=>{const e=new SessionWriteGate,t=deferred(),n=deferred(),a=new SessionMutationCoordinator(e,{waitForQuiescence:vi.fn(async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:5}))},{}),o=a.tryStartInteractive({sessionKey:"agent:main:one"},"acp");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-1");const s=a.requestCancel(o.token,{chatAbort:vi.fn(async()=>(await n.promise,{aborted:!0,runIds:["run-1"]}))}),r=a.converge(o.token,"cancelled");await Promise.resolve(),expect(a.snapshot("agent:main:one")?.cancelAcknowledged).toBe(!1),expect(a.snapshot("agent:main:one")?.phase).toBe("awaiting_quiescence"),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),t.resolve();const c=await r;expect(c.ok).toBe(!0),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),c.ok&&a.release(c.permit),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!0),n.resolve(),await expect(s).resolves.toMatchObject({ok:!0,acknowledged:!0})}),it("fails closed and exposes recovery-required when quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:20})},{},{observationTimeoutMs:20,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:fenced"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.converge(a.token,"timeout")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_timeout",terminalKind:"timeout"}}),expect(n.snapshot("agent:main:fenced")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:fenced"}).ok).toBe(!1),expect(t).toHaveBeenCalledOnce(),expect(t).toHaveBeenCalledWith(expect.objectContaining({reason:"quiescence_timeout",sessionKey:"agent:main:fenced"}))}),it("fences as quiescence_failed when the observer rejects",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>{throw new Error("observer failed")}},{}),n=t.tryStartInteractive({sessionKey:"agent:main:observer-failed"},"acp");if(!n.ok)throw new Error("expected lease");await expect(t.converge(n.token,"error")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_failed"}}),expect(t.snapshot("agent:main:observer-failed")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:observer-failed"}).ok).toBe(!1)}),it("blocks the same gateway session while allowing different sessions concurrently",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"acp").ok).toBe(!0),expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"message_service").ok).toBe(!1),expect(t.tryStartInteractive({sessionKey:"agent:main:b"},"message_service").ok).toBe(!0)}),it("releases a normally completed mutation only after the caller commits its permit",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:normal"},"acp");if(!n.ok)throw new Error("expected lease");const a=await t.converge(n.token,"completed");expect(a.ok).toBe(!0),expect(e.snapshot("agent:main:normal").active).toBeDefined(),a.ok&&t.release(a.permit),expect(e.snapshot("agent:main:normal").active).toBeUndefined()}),it.each(["acp","message_service"])("applies identical cancel convergence for the %s surface",async e=>{const t=new SessionWriteGate,n=vi.fn(),a=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-shared-contract"]}),o=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{},{onRecoveryRequired:n}),s=o.tryStartInteractive({sessionKey:`agent:main:${e}`},e);if(!s.ok)throw new Error("expected lease");o.bindRun(s.token,"run-shared-contract"),await expect(o.requestCancel(s.token,{chatAbort:a})).resolves.toMatchObject({ok:!0,acknowledged:!0});const r=await o.converge(s.token,"cancelled");expect(r.ok).toBe(!0),expect(a).toHaveBeenCalledWith(`agent:main:${e}`,"run-shared-contract"),expect(o.snapshot(`agent:main:${e}`)).toMatchObject({surface:e,phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!0}),expect(n).not.toHaveBeenCalled()}),it("treats aborted=false as observation and releases after terminal quiescence",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{debug:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-missed"},"message_service");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-missed");const s={chatAbort:vi.fn().mockResolvedValue({aborted:!1,runIds:[]})};await expect(a.requestCancel(o.token,s)).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"cancelling",cancelAcknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort not acknowledged; continuing terminal/quiescence convergence"));const r=await a.converge(o.token,"cancelled");expect(r.ok).toBe(!0),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!1}),expect(t).not.toHaveBeenCalled(),r.ok&&a.release(r.permit),expect(e.snapshot("agent:main:abort-missed").active).toBeUndefined(),expect(s.chatAbort).toHaveBeenCalledTimes(1)}),it("treats remote abort transport errors as observation and still converges",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{error:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-transport-error"},"message_service");if(!o.ok)throw new Error("expected lease");await expect(a.requestCancel(o.token,{chatAbort:async()=>{throw new Error("gateway unavailable")}})).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort transport failed; continuing terminal/quiescence convergence"));const s=await a.converge(o.token,"cancelled");expect(s.ok).toBe(!0),expect(t).not.toHaveBeenCalled(),s.ok&&a.release(s.permit)}),it("restarts once when aborted=false is not followed by a terminal",async()=>{const e=new SessionWriteGate,t=vi.fn();let n;const a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:3e4,onRecoveryRequired:t,setTimer:e=>(n=e,{unref:()=>{}}),clearTimer:()=>{}}),o=a.tryStartInteractive({sessionKey:"agent:main:false-no-terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!1,runIds:[]})});if(!s.ok)throw new Error("expected accepted cancel");expect(s.acknowledged).toBe(!1),n(),n(),await expect(s.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{reason:"terminal_not_observed"}}),expect(t).toHaveBeenCalledOnce()}),it("restarts once when aborted=false reaches terminal but lock quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:13e4})},{},{observationTimeoutMs:13e4,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:false-lock-timeout"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.requestCancel(a.token,{chatAbort:async()=>({aborted:!1,runIds:[]})})).resolves.toMatchObject({ok:!0,acknowledged:!1}),await expect(n.converge(a.token,"cancelled")).resolves.toMatchObject({ok:!1,recovery:{reason:"quiescence_timeout"}}),expect(t).toHaveBeenCalledOnce()}),it("requests cancellation from the current owner by session key",async()=>{const e=new SessionWriteGate,t=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-owner"]}),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:cancel-by-session"},"message_service");if(!a.ok)throw new Error("expected owner");n.bindRun(a.token,"run-owner"),await expect(n.requestCancelForSession("agent:main:cancel-by-session",{chatAbort:t})).resolves.toMatchObject({ok:!0,acknowledged:!0}),expect(t).toHaveBeenCalledWith("agent:main:cancel-by-session","run-owner"),expect(n.snapshot("agent:main:cancel-by-session")).toMatchObject({operationId:a.token.operationId,phase:"cancelling",cancelAcknowledged:!0});const o=await n.converge(a.token,"cancelled");o.ok&&n.release(o.permit)}),it("reports no active mutation when cancellation has no owner",async()=>{const e=new SessionMutationCoordinator(new SessionWriteGate,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});await expect(e.requestCancelForSession("agent:main:no-owner",{chatAbort:vi.fn()})).resolves.toEqual({ok:!1,reason:"no_active_mutation"})}),it("fences and resolves recovery when cancel ACK is not followed by a terminal",async()=>{const e=new SessionWriteGate;let t;const n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:e=>(t=e,{unref:()=>{}}),clearTimer:()=>{}}),a=n.tryStartInteractive({sessionKey:"agent:main:no-terminal"},"acp");if(!a.ok)throw new Error("expected lease");const o=await n.requestCancel(a.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!o.ok)throw new Error("expected cancel ACK");t(),await expect(o.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"terminal_not_observed"}}),expect(n.snapshot("agent:main:no-terminal")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:no-terminal"}).ok).toBe(!1)}),it("clears the cancel observation timer when terminal arrives",async()=>{const e=new SessionWriteGate,t={unref:()=>{}},n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:()=>t,clearTimer:n}),o=a.tryStartInteractive({sessionKey:"agent:main:terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!s.ok)throw new Error("expected cancel ACK");const r=a.converge(o.token,"cancelled");await expect(s.terminalObservation).resolves.toEqual({ok:!0,terminalObserved:!0}),expect(n).toHaveBeenCalledWith(t),await expect(r).resolves.toMatchObject({ok:!0})}),it("rejects late cancel after terminal convergence has started",async()=>{const e=new SessionWriteGate,t=deferred(),n=vi.fn().mockResolvedValue({aborted:!0,runIds:[]}),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),o=a.tryStartInteractive({sessionKey:"agent:main:late-cancel"},"acp");if(!o.ok)throw new Error("expected lease");const s=a.converge(o.token,"completed");await expect(a.requestCancel(o.token,{chatAbort:n})).resolves.toEqual({ok:!1,reason:"terminal_already_observed"}),expect(n).not.toHaveBeenCalled(),t.resolve(),await expect(s).resolves.toMatchObject({ok:!0})}),it("keeps disconnect recovery fenced when quiescence resolves concurrently",async()=>{const e=new SessionWriteGate,t=deferred(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:disconnect-race"},"acp");if(!a.ok)throw new Error("expected lease");const o=n.converge(a.token,"completed");await Promise.resolve(),n.fenceActive("gateway_disconnected"),t.resolve(),await expect(o).resolves.toMatchObject({ok:!1,recovery:{reason:"gateway_disconnected"}}),expect(n.snapshot("agent:main:disconnect-race")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:disconnect-race"}).ok).toBe(!1)}),it.each([void 0,"run-shared"])("attaches an external run to an existing Message Service owner (owner run=%s)",e=>{const t=new SessionWriteGate,n=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:shared-owner"},"message_service");if(!a.ok)throw new Error("expected owner");e&&n.bindRun(a.token,e);const o=n.attachOrStartExternal({sessionKey:"agent:main:shared-owner"},"acp","run-shared");expect(o).toEqual({ok:!0,ownership:"observer",ownerOperationId:a.token.operationId,attached:!0}),expect(n.snapshot("agent:main:shared-owner")?.runId).toBe("run-shared")}),it("keeps the owner lease active when an attached observer reaches terminal first",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{}),a=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"message_service","run-shared");if(!a.ok||"owner"!==a.ownership)throw new Error("expected owner");const o=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"acp","run-shared");expect(o).toMatchObject({ok:!0,ownership:"observer",attached:!0}),expect(n.snapshot("agent:main:observer-terminal")?.phase).toBe("active"),expect(e.snapshot("agent:main:observer-terminal").active?.operationId).toBe(a.token.operationId),expect(t).not.toHaveBeenCalled();const s=await n.converge(a.token,"completed");expect(s.ok).toBe(!0),s.ok&&n.release(s.permit),expect(e.snapshot("agent:main:observer-terminal").active).toBeUndefined()}),it("returns an owner token when no mutation exists",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}).attachOrStartExternal({sessionKey:"agent:main:new-external"},"acp","run-new");expect(t).toMatchObject({ok:!0,ownership:"owner",attached:!1,token:{sessionKey:"agent:main:new-external"}})}),it("rejects external attachment and cancellation without mutating an exclusive owner",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartExclusive({sessionKey:"agent:main:exclusive"},"acp");if(!n.ok)throw new Error("expected exclusive owner");expect(t.attachOrStartExternal({sessionKey:"agent:main:exclusive"},"message_service","run-external")).toEqual({ok:!1,reason:"session_busy"}),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"}),expect(t.snapshot("agent:main:exclusive")?.runId).toBeUndefined(),expect(e.snapshot("agent:main:exclusive").active?.operationId).toBe(n.token.operationId);const a=vi.fn();await expect(t.requestCancelForSession("agent:main:exclusive",{chatAbort:a})).resolves.toEqual({ok:!1,reason:"not_cancellable"}),expect(a).not.toHaveBeenCalled(),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"})}),it("fences an existing owner when an external run id conflicts",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:conflict"},"message_service");if(!n.ok)throw new Error("expected owner");t.bindRun(n.token,"run-owner"),expect(t.attachOrStartExternal({sessionKey:"agent:main:conflict"},"acp","run-external")).toMatchObject({ok:!1,reason:"run_conflict",recovery:{reason:"run_conflict"}}),expect(t.snapshot("agent:main:conflict")?.phase).toBe("recovery_required")})});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{PromptRunStore}from"../runtime/prompt-run-store.js";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"../runtime/session-mutation-coordinator.js";export const deferred=()=>{let e,t;return{promise:new Promise((s,n)=>{e=s,t=n}),resolve:e,reject:t}};export class TestMessageStream{body="";isTerminated=!1;terminal;async opened(){return{id:"test-envelope",state:"streaming"}}appendBody(e){this.body+=e}setBody(e){this.body=e}addPart(){}replacePart(){}appendThinking(){}appendToolUse(){}appendToolResult(){}async finalize(e){return this.terminal={state:"completed",stopReason:e?.stopReason??"end_turn",body:e?.body??this.body},this.isTerminated=!0,{id:"test-envelope",state:"completed"}}async fail(e){return this.terminal={state:"failed",stopReason:e?.stopReason??"error",body:e?.body??this.body},this.isTerminated=!0,{id:"test-envelope",state:"failed"}}async cancel(e){return this.terminal={state:"cancelled",stopReason:e?.stopReason??"user_stop",body:this.body},this.isTerminated=!0,{id:"test-envelope",state:"cancelled"}}}export class TestMessageClient{streams=[];startStreamCalls=[];messages={startStream:e=>{this.startStreamCalls.push({conversationId:e.conversationId,replyTo:e.replyTo,type:e.type});const t=new TestMessageStream;return this.streams.push(t),t}};asMessageClient(){return this}}export const createRuntime=e=>({channel:{reply:{dispatchReplyFromConfig:async t=>{const s=t.replyOptions;if(!s?.abortSignal)throw new Error("test runtime expected an abort signal");return e({invocation:t,dispatcher:t.dispatcher,replyOptions:s,abortSignal:s.abortSignal})}}}});const createLogger=()=>({debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}});export const createImDispatchFixture=e=>{const t=new TestMessageClient,s=[],n=new PromptRunStore,o=e?.sessionWriteGate??new SessionWriteGate,r=e?.sessionMutationCoordinator??new SessionMutationCoordinator(o,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});return{client:t,events:s,promptRunStore:n,args:(e,o={})=>({runtime:e,client:t.asMessageClient(),logger:createLogger(),promptRunStore:n,sessionMutationCoordinator:r,abortTransport:{chatAbort:async()=>{}},origin:"im",onLifecycleEvent:e=>s.push(e),prompt:{channelId:"channel-test",inboundMessageId:"message-test",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"test prompt"}},...o})}};
|
|
1
|
+
import{PromptRunStore}from"../runtime/prompt-run-store.js";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"../runtime/session-mutation-coordinator.js";export const deferred=()=>{let e,t;return{promise:new Promise((s,n)=>{e=s,t=n}),resolve:e,reject:t}};export class TestMessageStream{body="";isTerminated=!1;terminal;async opened(){return{id:"test-envelope",state:"streaming"}}appendBody(e){this.body+=e}setBody(e){this.body=e}addPart(){}replacePart(){}appendThinking(){}appendToolUse(){}appendToolResult(){}async finalize(e){return this.terminal={state:"completed",stopReason:e?.stopReason??"end_turn",body:e?.body??this.body},this.isTerminated=!0,{id:"test-envelope",state:"completed"}}async fail(e){return this.terminal={state:"failed",stopReason:e?.stopReason??"error",body:e?.body??this.body},this.isTerminated=!0,{id:"test-envelope",state:"failed"}}async cancel(e){return this.terminal={state:"cancelled",stopReason:e?.stopReason??"user_stop",body:this.body},this.isTerminated=!0,{id:"test-envelope",state:"cancelled"}}}export class TestMessageClient{streams=[];startStreamCalls=[];messages={startStream:e=>{this.startStreamCalls.push({conversationId:e.conversationId,replyTo:e.replyTo,type:e.type});const t=new TestMessageStream;return this.streams.push(t),t}};asMessageClient(){return this}}export const createRuntime=e=>({channel:{reply:{dispatchReplyFromConfig:async t=>{const s=t.replyOptions;if(!s?.abortSignal)throw new Error("test runtime expected an abort signal");return e({invocation:t,dispatcher:t.dispatcher,replyOptions:s,abortSignal:s.abortSignal})}}}});const createLogger=()=>({debug:()=>{},info:()=>{},warn:()=>{},error:()=>{}});export const createImDispatchFixture=e=>{const t=new TestMessageClient,s=[],n=new PromptRunStore,o=e?.sessionWriteGate??new SessionWriteGate,r=e?.sessionMutationCoordinator??new SessionMutationCoordinator(o,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});return{client:t,events:s,promptRunStore:n,args:(e,o={})=>({runtime:e,client:t.asMessageClient(),logger:createLogger(),promptRunStore:n,sessionMutationCoordinator:r,abortTransport:{chatAbort:async(e,t)=>({aborted:!0,runIds:t?[t]:[]})},origin:"im",onLifecycleEvent:e=>s.push(e),prompt:{channelId:"channel-test",inboundMessageId:"message-test",selfPrincipalId:"agent-test",sessionKey:"agent:main:im:channel-test",dispatchInput:{message:"test prompt"}},...o})}};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.62",
|
|
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>",
|