@henry-dev/botkit 0.0.0-stage → 1.0.1

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/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @henry-dev/botkit
2
+
3
+ A deterministic, type-safe conversational state machine framework for chatbots.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@henry-dev/botkit.svg)](https://www.npmjs.com/package/@henry-dev/botkit)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ---
9
+
10
+ ## Key Features
11
+
12
+ - **Deterministic FSM Engine**: Pure, 100% testable state transitions with automatic timeout guards and fallback recovery.
13
+ - **Compiled Pattern Matching**: Regex-compiled keyword normalization, word-boundary safety, and negation guards (`excludeIf`).
14
+ - **Durable State Persistence**: MongoDB / Mongoose adapter with Optimistic Concurrency Control (OCC) to prevent race conditions.
15
+ - **High-Speed Idempotency**: Redis-backed webhook deduplication and BullMQ asynchronous worker queue processing.
16
+ - **Analytics & Evals**: Built-in conversation trace collectors, automated evaluation suite, and node-level drop-off metrics.
17
+
18
+ ---
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @henry-dev/botkit
24
+ ```
25
+
26
+ Or using `pnpm` or `yarn`:
27
+
28
+ ```bash
29
+ pnpm add @henry-dev/botkit
30
+ # or
31
+ yarn add @henry-dev/botkit
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Quick Example
37
+
38
+ ```typescript
39
+ import { Bot, FSMEngine, matchesAny } from "@henry-dev/botkit";
40
+
41
+ // 1. Define the conversation flow
42
+ const bot = new Bot("onboarding-flow", 1);
43
+
44
+ bot.ask("What is your name?")
45
+ .expect(matchesAny(["jina", "naitwa", "*"]))
46
+ .response((answer, ctx) => {
47
+ ctx.set("name", answer.raw);
48
+ })
49
+ .fallback("Sorry, I didn't catch that — what should I call you?")
50
+ .retries(3);
51
+
52
+ const flow = bot.compile();
53
+
54
+ // 2. Instantiate and process transitions
55
+ const fsm = new FSMEngine(flow);
56
+ const state = fsm.loadState("user-123");
57
+ const result = await fsm.transition(state, "My name is Alex");
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Documentation & Interactive Playground
63
+
64
+ For full guides, API references, architecture deep-dives, and an interactive simulator:
65
+
66
+ - **Interactive Simulator**: Test conversational state flows live in browser
67
+ - **Analytics & Evals**: Track drop-offs and evaluate scenario completion rates
68
+ - **Full Documentation**: Explore detailed guides on matchers, persistence, webhooks, and workers
69
+
70
+ ---
71
+
72
+ ## License
73
+
74
+ Released under the [MIT License](LICENSE).
75
+
76
+
77
+
package/dist/index.cjs ADDED
@@ -0,0 +1,7 @@
1
+ var ye=Object.create;var W=Object.defineProperty;var xe=Object.getOwnPropertyDescriptor;var be=Object.getOwnPropertyNames;var ve=Object.getPrototypeOf,Ie=Object.prototype.hasOwnProperty;var Se=(s,e)=>{for(var n in e)W(s,n,{get:e[n],enumerable:!0})},le=(s,e,n,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of be(e))!Ie.call(s,r)&&r!==n&&W(s,r,{get:()=>e[r],enumerable:!(t=xe(e,r))||t.enumerable});return s};var Ne=(s,e,n)=>(n=s!=null?ye(ve(s)):{},le(e||!s||!s.__esModule?W(n,"default",{value:s,enumerable:!0}):n,s)),Ce=s=>le(W({},"__esModule",{value:!0}),s);var Re={};Se(Re,{ActionNodeBuilder:()=>O,AmbiguousSendError:()=>B,Bot:()=>G,CallbackPersistenceAdapter:()=>K,CompileError:()=>w,ConcurrencyLockError:()=>R,Context:()=>v,Evaluator:()=>Z,FSMEngine:()=>k,FlaresendClient:()=>J,FlaresendSendError:()=>M,FlowNodeBuilder:()=>P,GlobalTransitionBuilder:()=>$,HandoffNodeBuilder:()=>A,IdempotencyService:()=>j,InMemoryConcurrencyLeaseService:()=>L,KnownNotSentError:()=>D,MongoosePersistenceAdapter:()=>z,OptimisticConcurrencyError:()=>T,RedisConcurrencyLeaseService:()=>q,TraceCollector:()=>X,classifyFlaresendError:()=>ne,createConcurrencyLeaseService:()=>Y,createExpressAdapter:()=>se,createFSMWorker:()=>we,createFastifyAdapter:()=>V,createLiteralMatcher:()=>ee,createRawHttpAdapter:()=>ie,expressAdapter:()=>me,extractWebhookPayload:()=>re,fastifyAdapter:()=>pe,handleWebhookCore:()=>F,httpAdapter:()=>ge,matchesAny:()=>_,processFSMJob:()=>ce,rawHttpAdapter:()=>ae,registerFlowDefinition:()=>te,runConversationStep:()=>U,verifyWebhookSignature:()=>oe});module.exports=Ce(Re);function ke(s){return s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}var Me=s=>/[\{\}\[\]\(\)\|\+\?\^\$]/.test(s);function ee(s,e){let n=o=>{if(e?.normalize===!1)return o.trim().toLowerCase();let i=o.trim().toLowerCase();return i=i.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()?"'’]/g,""),i=i.replace(/(.)\1{2,}/g,"$1"),i=i.replace(/\s+/g," "),i.trim()},t=o=>{let i=n(o),u=Me(i),f=u?i:ke(i);return(e?.boundary??!0)&&!u?`\\b${f}\\b`:f},r=s.filter(o=>o!=="*"),a=s.includes("*"),l=r.map(o=>{let i=t(o);return{pattern:o,regex:new RegExp(i,"i")}}),h=r.length>0?new RegExp(r.map(t).join("|"),"i"):null,d=e?.excludeIf||[],c=d.length>0?new RegExp(d.map(t).join("|"),"i"):null;return{patterns:s,normalize:n,match:o=>{let i=n(o);if(c&&c.test(i))return{matched:!1,pattern:"",confidence:0};if(h&&h.test(i)){for(let u of l)if(u.regex.test(i))return{matched:!0,pattern:u.pattern,confidence:1}}return a?{matched:!0,pattern:"*",confidence:.1}:{matched:!1,pattern:"",confidence:0}}}}function _(s,e){return ee(s,e)}var w=class extends Error{constructor(e){super(e),this.name="CompileError"}},O=class{constructor(e,n,t){this.bot=e;this.id=n;t&&(t.execute&&(this._actionFn=t.execute),t.timeoutMs&&(this._timeoutMs=t.timeoutMs),typeof t.onSuccess=="string"?this._onSuccess={targetNodeId:t.onSuccess}:t.onSuccess&&(this._onSuccess=t.onSuccess),typeof t.onFailure=="string"?this._onFailure={targetNodeId:t.onFailure}:t.onFailure&&(this._onFailure=t.onFailure),typeof t.onTimeout=="string"?this._onTimeout={targetNodeId:t.onTimeout}:t.onTimeout&&(this._onTimeout=t.onTimeout))}bot;id;_actionFn;_timeoutMs=5e3;_onSuccess;_onFailure;_onTimeout;_onEnter;_onExit;_text="";execute(e){return this._actionFn=e,this}timeout(e){return this._timeoutMs=e,this}text(e){return this._text=e,this}onSuccess(e,n){return this._onSuccess={targetNodeId:e,handler:n},this}onFailure(e,n){return this._onFailure={targetNodeId:e,handler:n},this}onTimeout(e,n){return this._onTimeout={targetNodeId:e,handler:n},this}onEnter(e){return this._onEnter=e,this}onExit(e){return this._onExit=e,this}ask(e){return this.bot.ask(e)}action(e,n){return this.bot.action(e,n)}handoff(e,n){return this.bot.handoff(e,n)}global(e,n){return this.bot.global(e,n)}getId(){return this.id}getText(){return this._text}getActionFn(){return this._actionFn}getTimeoutMs(){return this._timeoutMs}getOnSuccess(){return this._onSuccess}getOnFailure(){return this._onFailure}getOnTimeout(){return this._onTimeout}getOnEnterFn(){return this._onEnter}getOnExitFn(){return this._onExit}},A=class{constructor(e,n,t){this.bot=e;this.id=n;t&&(t.reason&&(this._reason=t.reason),t.timeoutMs!==void 0&&(this._timeoutMs=t.timeoutMs),t.resumeNodeId&&(this._resumeNodeId=t.resumeNodeId),t.fallbackNodeId&&(this._fallbackNodeId=t.fallbackNodeId),t.message&&(this._text=t.message),t.queueMessage!==void 0&&(this._queueMessage=t.queueMessage),t.metadata&&(this._metadata=t.metadata))}bot;id;_reason;_timeoutMs=600*1e3;_resumeNodeId;_fallbackNodeId;_text="Connecting you with an agent. Please hold...";_queueMessage="You are in the queue. An agent will be with you shortly.";_metadata;_onEnter;_onExit;_ignoreGlobals;reason(e){return this._reason=e,this}timeout(e){return this._timeoutMs=e,this}resumeAt(e){return this._resumeNodeId=e,this}fallbackTo(e){return this._fallbackNodeId=e,this}message(e){return this._text=e,this}queueMessage(e){return this._queueMessage=e,this}metadata(e){return this._metadata=e,this}onEnter(e){return this._onEnter=e,this}onExit(e){return this._onExit=e,this}ignoreGlobals(e=!0){return this._ignoreGlobals=e,this}ask(e){return this.bot.ask(e)}action(e,n){return this.bot.action(e,n)}handoff(e,n){return this.bot.handoff(e,n)}global(e,n){return this.bot.global(e,n)}getId(){return this.id}getText(){return this._text}getReason(){return this._reason}getTimeoutMs(){return this._timeoutMs}getResumeNodeId(){return this._resumeNodeId}getFallbackNodeId(){return this._fallbackNodeId}getQueueMessage(){return this._queueMessage}getMetadata(){return this._metadata}getOnEnterFn(){return this._onEnter}getOnExitFn(){return this._onExit}getIgnoreGlobals(){return this._ignoreGlobals}},$=class{constructor(e,n,t){this.bot=e;typeof n=="object"&&n!==null&&"match"in n&&typeof n.match=="function"?(this._matcher=n,this._patterns=[...this._matcher.patterns]):typeof n=="string"?this._patterns=[n]:Array.isArray(n)?this._patterns=[...n]:this._patterns=[],typeof t=="string"?this._targetNodeId=t:t&&(t.id&&(this._id=t.id),t.target&&(this._targetNodeId=t.target),t.targetNodeId&&(this._targetNodeId=t.targetNodeId),t.priority&&(this._priority=t.priority),t.response&&(this._responseFn=t.response),t.matcherOptions&&(this._opts=t.matcherOptions))}bot;_id;_targetNodeId;_patterns=[];_opts;_matcher;_priority="high";_responseFn;id(e){return this._id=e,this}then(e){return this._targetNodeId=e,this}response(e){return this._responseFn=e,this}priority(e){return this._priority=e,this}getId(e){return this._id??e}getTargetNodeId(){return this._targetNodeId}getPatterns(){return this._patterns}getOpts(){return this._opts}getMatcher(){return this._matcher}getPriority(){return this._priority}getResponseFn(){return this._responseFn}ask(e){return this.bot.ask(e)}action(e,n){return this.bot.action(e,n)}handoff(e,n){return this.bot.handoff(e,n)}global(e,n){return this.bot.global(e,n)}},P=class{constructor(e,n,t){this.bot=e;this.defaultId=n;this.text=t}bot;defaultId;text;_customId;_branches=[];_responseFn;_fallbackText="Please try again.";_maxRetries=3;_onMaxRetriesFn;_onEnter;_onExit;_ignoreGlobals;getId(){return this._customId??this.defaultId}id(e){return this._customId=e,this}expect(e,n){let t,r;return typeof e=="object"&&e!==null&&"match"in e&&typeof e.match=="function"?(r=e,t=[...r.patterns]):typeof e=="string"?t=[e]:Array.isArray(e)?t=[...e]:t=[],this._branches.push({patterns:t,opts:n,matcher:r}),this}then(e){if(this._branches.length===0)throw new Error(`Cannot call then("${e}") before expect() on node "${this.getId()}"`);let n=this._branches[this._branches.length-1];return n.targetNodeId=e,this}response(e){return this._responseFn=e,this}fallback(e){return this._fallbackText=e,this}retries(e){return this._maxRetries=e,this}onMaxRetries(e){return this._onMaxRetriesFn=e,this}onEnter(e){return this._onEnter=e,this}onExit(e){return this._onExit=e,this}ignoreGlobals(e=!0){return this._ignoreGlobals=e,this}ask(e){return this.bot.ask(e)}action(e,n){return this.bot.action(e,n)}handoff(e,n){return this.bot.handoff(e,n)}global(e,n){return this.bot.global(e,n)}getBranches(){return this._branches}getResponseFn(){return this._responseFn}getFallbackText(){return this._fallbackText}getMaxRetries(){return this._maxRetries}getOnMaxRetriesFn(){return this._onMaxRetriesFn}getOnEnterFn(){return this._onEnter}getOnExitFn(){return this._onExit}getIgnoreGlobals(){return this._ignoreGlobals}},G=class{constructor(e,n=1){this.flowId=e;this.version=n}flowId;version;builders=[];actionBuilders=[];handoffBuilders=[];globalBuilders=[];ask(e){let n=`node_${this.builders.length}`,t=new P(this,n,e);return this.builders.push(t),t}action(e,n){let t=new O(this,e,n);return this.actionBuilders.push(t),t}handoff(e,n){let t=new A(this,e,n);return this.handoffBuilders.push(t),t}global(e,n){let t=new $(this,e,n);return this.globalBuilders.push(t),t}escalate(e){e.set("escalated",!0)}compile(){let e=new Set,n=[];for(let o of this.builders){let i=o.getId();if(e.has(i))throw new w(`Duplicate node id "${i}".`);e.add(i),n.push(i)}for(let o of this.actionBuilders){let i=o.getId();if(e.has(i))throw new w(`Duplicate node id "${i}".`);e.add(i),n.push(i)}for(let o of this.handoffBuilders){let i=o.getId();if(e.has(i))throw new w(`Duplicate node id "${i}".`);e.add(i),n.push(i)}let t=new Set,r=this.globalBuilders.map((o,i)=>{let u=o.getId(`global_${i}`);if(t.has(u))throw new w(`Duplicate global transition id "${u}".`);t.add(u);let f=o.getTargetNodeId();if(!f)throw new w(`Global transition "${u}" for pattern(s) [${o.getPatterns().join(", ")}] is missing an explicit .then() target.`);if(!e.has(f))throw new w(`Unknown node "${f}" referenced by global transition "${u}". Known node IDs: ${n.join(", ")}.`);let p=o.getMatcher()??_(o.getPatterns(),o.getOpts());return{id:u,matcher:p,patterns:o.getPatterns(),targetNodeId:f,priority:o.getPriority(),responseFn:o.getResponseFn()}}),a=this.builders.map((o,i)=>{let u=o.getBranches();if(u.length===0)throw new w(`Node "${o.getId()}" is missing an expect() matcher`);if(u.length>1){for(let x of u)if(!x.targetNodeId)throw new w(`branch for pattern(s) [${x.patterns.join(", ")}] is missing an explicit .then() target \u2014 required once a node has multiple branches`)}let f=i<this.builders.length-1?n[i+1]:void 0,p=[],m=[],g=[],y=[];for(let x of u){let I=x.targetNodeId;if(!I&&u.length===1&&f&&(I=f),I&&!e.has(I))throw new w(`Unknown node "${I}" referenced by transition from node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);p.push(...x.patterns),m.push({targetNodeId:I??"",patterns:x.patterns});let C=x.matcher??_(x.patterns,x.opts);x.patterns.includes("*")?y.push(C):g.push(C)}let b={patterns:p,normalize:x=>x.trim().toLowerCase(),match:x=>{for(let I of g){let C=I.match(x);if(C.matched)return C}for(let I of y){let C=I.match(x);if(C.matched)return C}return{matched:!1,pattern:"",confidence:0}}};return{id:o.getId(),text:o.text,matcher:b,transitions:m,fallbackText:o.getFallbackText(),maxRetries:o.getMaxRetries(),onMaxRetriesFn:o.getOnMaxRetriesFn(),responseFn:o.getResponseFn(),onEnter:o.getOnEnterFn(),onExit:o.getOnExitFn(),ignoreGlobals:o.getIgnoreGlobals()}}),l=this.actionBuilders.map(o=>{let i=o.getActionFn();if(!i)throw new w(`Action node "${o.getId()}" is missing an execute() handler.`);let u=o.getOnSuccess(),f=o.getOnFailure(),p=o.getOnTimeout();if(!u&&!f)throw new w(`Action node "${o.getId()}" must define at least an onSuccess or onFailure transition target.`);if(u&&!e.has(u.targetNodeId))throw new w(`Unknown node "${u.targetNodeId}" referenced by onSuccess transition on action node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);if(f&&!e.has(f.targetNodeId))throw new w(`Unknown node "${f.targetNodeId}" referenced by onFailure transition on action node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);if(p&&!e.has(p.targetNodeId))throw new w(`Unknown node "${p.targetNodeId}" referenced by onTimeout transition on action node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);let m=[];return u&&m.push({targetNodeId:u.targetNodeId,patterns:["success"]}),f&&m.push({targetNodeId:f.targetNodeId,patterns:["failure"]}),p&&m.push({targetNodeId:p.targetNodeId,patterns:["timeout"]}),{id:o.getId(),text:o.getText(),matcher:{patterns:["*"],normalize:g=>g,match:()=>({matched:!0,pattern:"*",confidence:1})},transitions:m,fallbackText:"",maxRetries:0,onEnter:o.getOnEnterFn(),onExit:o.getOnExitFn(),isAction:!0,action:{execute:i,timeoutMs:o.getTimeoutMs(),onSuccess:u,onFailure:f,onTimeout:p}}}),h=[...a,...l],d=this.handoffBuilders.map(o=>{let i=o.getResumeNodeId(),u=o.getFallbackNodeId();if(i&&!e.has(i))throw new w(`Unknown node "${i}" referenced by resumeAt on handoff node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);if(u&&!e.has(u))throw new w(`Unknown node "${u}" referenced by fallbackTo on handoff node "${o.getId()}". Known node IDs: ${n.join(", ")}.`);let f=[];return i&&f.push({targetNodeId:i,patterns:["resume"]}),u&&f.push({targetNodeId:u,patterns:["timeout"]}),{id:o.getId(),text:o.getText(),matcher:{patterns:["*"],normalize:p=>p,match:()=>({matched:!0,pattern:"*",confidence:1})},transitions:f,fallbackText:"",maxRetries:0,onEnter:o.getOnEnterFn(),onExit:o.getOnExitFn(),ignoreGlobals:o.getIgnoreGlobals(),isHandoff:!0,handoff:{reason:o.getReason(),timeoutMs:o.getTimeoutMs(),resumeNodeId:i,fallbackNodeId:u,queueMessage:o.getQueueMessage(),metadata:o.getMetadata()}}}),c=[...h,...d];return{flowId:this.flowId,version:this.version,nodes:c,globals:r.length>0?r:void 0}}};var v=class{data;constructor(e={}){this.data=e}set(e,n){this.data[e]=n}get(e){return this.data[e]}has(e){return Object.prototype.hasOwnProperty.call(this.data,e)}delete(e){delete this.data[e]}clear(){this.data={}}all(){return{...this.data}}};async function S(s,e=5e3){return new Promise((n,t)=>{let r=setTimeout(()=>{t(new Error("Handler execution timed out"))},e);Promise.resolve(s()).then(a=>{clearTimeout(r),n(a)}).catch(a=>{clearTimeout(r),t(a)})})}var k=class{constructor(e){this.flow=e}flow;createInitialState(e){return{waUserId:e,flowId:this.flow.flowId,flowVersion:this.flow.version,currentNodeId:this.flow.nodes[0]?.id||"",context:{},retryCount:0,status:"active"}}async executeInitialEnter(e){if(e.status!=="active")return e;let n=this.flow.nodes.find(t=>t.id===e.currentNodeId);if(n&&n.onEnter){let t=new v({...e.context});return await this.safelyExecuteHook(n.onEnter,t,{fromNodeId:void 0,toNodeId:n.id,isInitial:!0},`onEnter (initial) on node "${n.id}"`),{...e,context:t.all()}}return e}async executeActionNode(e){let n=this.flow.nodes.find(r=>r.id===e.currentNodeId);if(!n||!n.isAction)return{nextState:e,outboundText:null};let t=new v({...e.context});return await this.resolveDestination(e,n,n.id,"",t)}isGlobalIgnored(e,n){if(e.ignoreGlobals===!0)return!0;if(Array.isArray(e.ignoreGlobals)){let t=e.ignoreGlobals;if(t.includes(n.id)||n.patterns&&n.patterns.some(r=>t.includes(r)))return!0}return!1}async safelyExecuteHook(e,n,t,r){if(e)try{await S(()=>e(n,t))}catch(a){console.error(`[FSMEngine] ${r} hook failed or timed out:`,a)}}async resolveDestination(e,n,t,r,a,l,h=0){if(h>10)return console.error("[FSMEngine] Maximum action chaining depth exceeded (depth > 10). Breaking loop."),{nextState:{...e,context:a.all(),status:"escalated"},outboundText:"An unexpected error occurred. Connecting you with an agent."};let d=this.flow.nodes.find(o=>o.id===t);if(!d)return{nextState:{...e,context:a.all(),status:"completed"},outboundText:null,matchedGlobalId:l};if(await this.safelyExecuteHook(d.onEnter,a,{fromNodeId:n.id,toNodeId:d.id,triggerText:r,isInitial:!1},`onEnter on node "${d.id}"`),d.isAction&&d.action){let o=d.action,i=o.timeoutMs??5e3,u=!1,f=null,p=!1,m=null;try{f=await S(()=>o.execute(a),i),u=!0}catch(g){g?.message?.includes("timed out")&&(p=!0),m=g}if(u){if(o.onSuccess?.handler)try{await S(()=>o.onSuccess.handler(f,a),i)}catch(y){console.error(`[FSMEngine] onSuccess handler on action node "${d.id}" failed:`,y)}let g=o.onSuccess?.targetNodeId;return await this.safelyExecuteHook(d.onExit,a,{fromNodeId:d.id,toNodeId:g,triggerText:r},`onExit on action node "${d.id}"`),g?await this.resolveDestination(e,d,g,r,a,l,h+1):{nextState:{...e,context:a.all(),status:"completed"},outboundText:null,matchedGlobalId:l}}else if(p){let g=o.onTimeout??o.onFailure;if(g?.handler)try{await S(()=>g.handler(m,a),i)}catch(b){console.error(`[FSMEngine] onTimeout handler on action node "${d.id}" failed:`,b)}let y=g?.targetNodeId;return await this.safelyExecuteHook(d.onExit,a,{fromNodeId:d.id,toNodeId:y,triggerText:r},`onExit on action node "${d.id}"`),y?await this.resolveDestination(e,d,y,r,a,l,h+1):{nextState:{...e,context:a.all(),status:"completed"},outboundText:null,matchedGlobalId:l}}else{let g=o.onFailure;if(g?.handler)try{await S(()=>g.handler(m,a),i)}catch(b){console.error(`[FSMEngine] onFailure handler on action node "${d.id}" failed:`,b)}let y=g?.targetNodeId;return await this.safelyExecuteHook(d.onExit,a,{fromNodeId:d.id,toNodeId:y,triggerText:r},`onExit on action node "${d.id}"`),y?await this.resolveDestination(e,d,y,r,a,l,h+1):{nextState:{...e,context:a.all(),status:"completed"},outboundText:null,matchedGlobalId:l}}}if(d.isHandoff){let o=d.handoff,i={state:"pending",reason:o?.reason,requestedAt:Date.now(),resumeNodeId:o?.resumeNodeId,fallbackNodeId:o?.fallbackNodeId,timeoutMs:o?.timeoutMs??600*1e3,queueMessage:o?.queueMessage,metadata:o?.metadata?{...o.metadata}:void 0};return{nextState:{...e,currentNodeId:d.id,retryCount:0,context:a.all(),status:"handoff",handoff:i},outboundText:d.text,matchedGlobalId:l}}return{nextState:{...e,currentNodeId:d.id,retryCount:0,context:a.all(),status:"active"},outboundText:d.text,matchedGlobalId:l}}async executeGlobalTransition(e,n,t,r,a,l){if(t.responseFn)try{await S(()=>t.responseFn({raw:a,pattern:r},l))}catch(h){console.error(`[FSMEngine] Global response handler for "${t.id}" failed or timed out:`,h)}return await this.safelyExecuteHook(n.onExit,l,{fromNodeId:n.id,toNodeId:t.targetNodeId,triggerText:a},`onExit on node "${n.id}"`),await this.resolveDestination(e,n,t.targetNodeId,a,l,t.id)}async transition(e,n){if(e.status==="completed"||e.status==="escalated"||e.status==="abandoned")return{nextState:e,outboundText:null};if(e.status==="handoff"){let o=this.flow.nodes.find(p=>p.id===e.currentNodeId);if(!o)return{nextState:e,outboundText:null};let i=new v({...e.context}),f=(this.flow.globals??[]).filter(p=>!this.isGlobalIgnored(o,p));for(let p of f.filter(m=>m.priority==="high")){let m;try{m=p.matcher.match(n)}catch{m={matched:!1,pattern:"",confidence:0}}if(m.matched){let g=e.handoff?{...e.handoff,state:"resolved",resolvedAt:Date.now()}:void 0,y={...e,status:"active",handoff:g};return await this.executeGlobalTransition(y,o,p,m.pattern,n,i)}}for(let p of f.filter(m=>m.priority==="low")){let m;try{m=p.matcher.match(n)}catch{m={matched:!1,pattern:"",confidence:0}}if(m.matched){let g=e.handoff?{...e.handoff,state:"resolved",resolvedAt:Date.now()}:void 0,y={...e,status:"active",handoff:g};return await this.executeGlobalTransition(y,o,p,m.pattern,n,i)}}if(e.handoff?.state==="pending"){let p=e.handoff.queueMessage!==void 0?e.handoff.queueMessage:"You are in the queue. An agent will be with you shortly.";return{nextState:e,outboundText:p}}return{nextState:e,outboundText:null}}let t=this.flow.nodes.find(o=>o.id===e.currentNodeId);if(!t)return{nextState:e,outboundText:null};let r=new v({...e.context}),l=(this.flow.globals??[]).filter(o=>!this.isGlobalIgnored(t,o)),h=l.filter(o=>o.priority==="high"),d=l.filter(o=>o.priority==="low");for(let o of h){let i;try{i=o.matcher.match(n)}catch(u){console.error(`[FSMEngine] Global matcher "${o.id}" threw:`,u),i={matched:!1,pattern:"",confidence:0}}if(i.matched)return await this.executeGlobalTransition(e,t,o,i.pattern,n,r)}let c;try{c=t.matcher.match(n)}catch(o){console.error("Matcher threw an error:",o),c={matched:!1,pattern:"",confidence:0}}if(c.matched){let o=!0;if(t.responseFn)try{await S(()=>t.responseFn({raw:n,pattern:c.pattern},r))}catch(i){console.error("Response handler failed or timed out:",i),o=!1}if(o){let i=t.transitions[0];if(t.transitions.length>1)if(c.pattern!=="*"){let f=t.transitions.find(p=>p.patterns&&p.patterns.includes(c.pattern));if(f)i=f;else{let p=t.transitions.find(m=>m.patterns&&m.patterns.includes("*"));p&&(i=p)}}else{let f=t.transitions.find(p=>p.patterns&&p.patterns.includes("*"));f&&(i=f)}let u=i?.targetNodeId;return await this.safelyExecuteHook(t.onExit,r,{fromNodeId:t.id,toNodeId:u||void 0,triggerText:n},`onExit on node "${t.id}"`),u?await this.resolveDestination(e,t,u,n,r):{nextState:{...e,context:r.all(),status:"completed"},outboundText:null}}}for(let o of d){let i;try{i=o.matcher.match(n)}catch(u){console.error(`[FSMEngine] Global matcher "${o.id}" threw:`,u),i={matched:!1,pattern:"",confidence:0}}if(i.matched)return await this.executeGlobalTransition(e,t,o,i.pattern,n,r)}return this.handleFallback(e,t,r)}async handleFallback(e,n,t){let r=e.retryCount+1;if(r<=n.maxRetries){let a="Please try again.";if(typeof n.fallbackText=="function")try{let h=await S(()=>n.fallbackText(t));typeof h=="string"&&(a=h)}catch(h){console.error("Fallback generator function failed or timed out:",h)}else typeof n.fallbackText=="string"&&(a=n.fallbackText);return{nextState:{...e,retryCount:r,context:t.all()},outboundText:a}}else{let a="Let me connect you with a human \u2014 someone will be with you shortly.";if(n.onMaxRetriesFn)try{let h=await S(()=>n.onMaxRetriesFn(t));typeof h=="string"&&(a=h)}catch(h){console.error("onMaxRetries handler failed or timed out:",h)}return{nextState:{...e,retryCount:r,context:t.all(),status:"escalated"},outboundText:a}}}connectAgent(e,n,t){if(e.status!=="handoff"||!e.handoff)throw new Error(`Cannot connect agent: state status is "${e.status}", not "handoff".`);let r={...e.handoff,state:"connected",assignedAgentId:n,connectedAt:Date.now(),metadata:t?{...e.handoff.metadata,...t}:e.handoff.metadata};return{...e,handoff:r}}async resolveHandoff(e,n){if(e.status!=="handoff"||!e.handoff)throw new Error(`Cannot resolve handoff: state status is "${e.status}", not "handoff".`);let t=this.flow.nodes.find(d=>d.id===e.currentNodeId),r=new v({...e.context,...n?.contextUpdates||{}});n?.resolutionNotes&&r.set("handoff_resolution_notes",n.resolutionNotes);let a=n?.resumeNodeId??e.handoff.resumeNodeId;t&&await this.safelyExecuteHook(t.onExit,r,{fromNodeId:t.id,toNodeId:a,triggerText:"agent_resolve"},`onExit on handoff node "${t.id}"`);let l={...e.handoff,state:"resolved",resolvedAt:Date.now()},h={...e,status:"active",context:r.all(),handoff:l};return a?await this.resolveDestination(h,t??{id:e.currentNodeId},a,"agent_resolve",r):{nextState:{...h,status:"completed"},outboundText:null}}async checkHandoffTimeout(e,n=Date.now()){if(e.status!=="handoff"||!e.handoff||e.handoff.state!=="pending")return null;let t=e.handoff.timeoutMs??600*1e3;if(n-e.handoff.requestedAt<t)return null;let r=this.flow.nodes.find(c=>c.id===e.currentNodeId),a=new v({...e.context}),l={...e.handoff,state:"timed_out"},h=e.handoff.fallbackNodeId;r&&await this.safelyExecuteHook(r.onExit,a,{fromNodeId:r.id,toNodeId:h,triggerText:"handoff_timeout"},`onExit on handoff node "${r.id}"`);let d={...e,status:"active",context:a.all(),handoff:l};return h?await this.resolveDestination(d,r??{id:e.currentNodeId},h,"handoff_timeout",a):{nextState:{...d,status:"escalated"},outboundText:"Live agent request timed out. Please try again later or reach out via email."}}getAvailableCommands(e){let n=this.flow.nodes.find(r=>r.id===e.currentNodeId),t=[];if(e.status==="completed"||e.status==="escalated"||e.status==="abandoned")return{currentNodeId:e.currentNodeId,status:e.status,handoffState:e.handoff?.state,commands:[]};if(n&&this.flow.globals){for(let r of this.flow.globals)if(!this.isGlobalIgnored(n,r))for(let a of r.patterns||[r.id])t.push({kind:"global",command:a,targetNodeId:r.targetNodeId,description:`Global interrupt to "${r.targetNodeId}"`})}if(e.status==="handoff")return e.handoff?.state==="pending"?(t.push({kind:"handoff",command:"agent:connect",description:"Connect available human agent"}),t.push({kind:"handoff",command:"agent:timeout",targetNodeId:e.handoff?.fallbackNodeId,description:"Trigger handoff timeout fallback"})):e.handoff?.state==="connected"&&t.push({kind:"handoff",command:"agent:resolve",targetNodeId:e.handoff?.resumeNodeId,description:"Resolve live session and resume bot flow"}),{currentNodeId:e.currentNodeId,status:e.status,handoffState:e.handoff?.state,commands:t};if(n)if(n.isAction)t.push({kind:"action",command:"action:execute",targetNodeId:n.action?.onSuccess?.targetNodeId,description:"Execute async action handler"});else for(let r of n.transitions)for(let a of r.patterns||["*"])t.push({kind:"local",command:a,targetNodeId:r.targetNodeId,description:`Transition to "${r.targetNodeId}"`});return{currentNodeId:e.currentNodeId,status:e.status,handoffState:e.handoff?.state,commands:t}}getLegalTransitions(e){return this.getAvailableCommands(e)}};async function U(s,e,n){let t=new k(n.flow),r=await n.persistence.load(s);r||(r=t.createInitialState(s),r=await t.executeInitialEnter(r));let a=await t.transition(r,e);return await n.persistence.save(a.nextState),{nextState:a.nextState,result:a}}var N=Ne(require("mongoose"),1),T=class extends Error{constructor(n,t){super(`Optimistic concurrency violation for user ${n}. Expected version: ${t}`);this.waUserId=n;this.expectedVersion=t;this.name="OptimisticConcurrencyError"}waUserId;expectedVersion},ue=new Map;function te(s){let e=`${s.flowId}:${s.version}`;ue.set(e,s)}var Fe=new N.Schema({waUserId:{type:String,required:!0,unique:!0,index:!0},flowId:{type:String,required:!0},flowVersion:{type:Number,required:!0},currentNodeId:{type:String,required:!0},context:{type:N.Schema.Types.Mixed,default:{}},retryCount:{type:Number,required:!0,default:0},status:{type:String,required:!0,enum:["active","escalated","completed","abandoned","handoff"],default:"active"},handoff:{type:N.Schema.Types.Mixed},version:{type:Number,required:!0,default:0}}),Te=new N.Schema({id:{type:String,required:!0},text:{type:String,required:!0},patterns:[{type:String}],fallbackText:{type:N.Schema.Types.Mixed},maxRetries:{type:Number,required:!0}}),fe=new N.Schema({flowId:{type:String,required:!0},version:{type:Number,required:!0},nodes:[Te]});fe.index({flowId:1,version:1},{unique:!0});var z=class{ConversationStateModel;FlowDefinitionModel;constructor(e=N.default.connection){this.ConversationStateModel=e.models.ConversationState||e.model("ConversationState",Fe),this.FlowDefinitionModel=e.models.FlowDefinition||e.model("FlowDefinition",fe)}async load(e){let n=await this.ConversationStateModel.findOne({waUserId:e});return n?{waUserId:n.waUserId,flowId:n.flowId,flowVersion:n.flowVersion,currentNodeId:n.currentNodeId,context:n.context instanceof Map?Object.fromEntries(n.context):n.context,retryCount:n.retryCount,status:n.status,version:n.version}:null}async save(e){let n=e.version;if(n===void 0)try{await new this.ConversationStateModel({waUserId:e.waUserId,flowId:e.flowId,flowVersion:e.flowVersion,currentNodeId:e.currentNodeId,context:e.context,retryCount:e.retryCount,status:e.status,version:1}).save()}catch(t){throw t.code===11e3?new T(e.waUserId,0):t}else if((await this.ConversationStateModel.updateOne({waUserId:e.waUserId,version:n},{$set:{flowId:e.flowId,flowVersion:e.flowVersion,currentNodeId:e.currentNodeId,context:e.context,retryCount:e.retryCount,status:e.status,version:n+1}},{runValidators:!0})).matchedCount===0)throw new T(e.waUserId,n)}async loadFlowDefinition(e,n){let t=`${e}:${n}`,r=ue.get(t);if(r)return r;throw new Error(`Flow "${e}" (v${n}) is not loaded/registered in the in-memory compiledRegistry. Active flows must be registered on application startup by calling registerFlowDefinition() or compiling a flow with bot.compile() to preserve execution callbacks and handlers.`)}async saveFlowDefinition(e){te(e);let n=e.nodes.map(t=>({id:t.id,text:t.text,patterns:t.matcher.patterns,fallbackText:typeof t.fallbackText=="string"?t.fallbackText:"[Dynamic Handler]",maxRetries:t.maxRetries}));await this.FlowDefinitionModel.updateOne({flowId:e.flowId,version:e.version},{$set:{flowId:e.flowId,version:e.version,nodes:n}},{upsert:!0,runValidators:!0})}},K=class{constructor(e){this.options=e}options;async load(e){return this.options.load(e)}async save(e){return this.options.save(e)}async loadFlowDefinition(e,n){return this.options.loadFlowDefinition(e,n)}async saveFlowDefinition(e){return this.options.saveFlowDefinition(e)}};var j=class{constructor(e){this.redis=e}redis;async checkAndLock(e,n=21600){if(!e)throw new Error("wamid is required for idempotency check");let t=`dedup:${e}`;return await this.redis.set(t,"1","EX",n,"NX")==="OK"}async release(e){if(e){let n=`dedup:${e}`;await this.redis.del(n)}}};var he=require("flaresend"),M=class extends Error{isAmbiguous;statusCode;code;constructor(e,n,t,r){super(e),this.name="FlaresendSendError",this.isAmbiguous=n,this.statusCode=t,this.code=r}},D=class extends M{constructor(e,n,t){super(e,!1,n,t),this.name="KnownNotSentError"}},B=class extends M{constructor(e,n,t){super(e,!0,n,t),this.name="AmbiguousSendError"}};function ne(s){if(s instanceof M)return s;let e=s?.message||String(s),n=typeof s?.status=="number"?s.status:typeof s?.statusCode=="number"?s.statusCode:typeof s?.response?.status=="number"?s.response.status:void 0,t=s?.code||s?.errno;return n!==void 0&&n>=400&&n<500||t==="ENOTFOUND"||t==="ECONNREFUSED"||t==="EADDRNOTAVAIL"?new D(`Flaresend rejected request (Status: ${n??"N/A"}, Code: ${t??"N/A"}): ${e}`,n,t):new B(`Unconfirmed delivery status (Status: ${n??"N/A"}, Code: ${t??"N/A"}): ${e}`,n,t)}var J=class{sdk=null;isMock;constructor(e={}){this.isMock=!!e.mock;let n=e.apiKey||process.env.FLARESEND_API_KEY||"";if(!this.isMock){if(!n)throw new Error("FlaresendClient: FLARESEND_API_KEY is missing and mock mode is disabled. Provide a valid API key or explicitly set options.mock = true.");this.sdk=new he.Flaresend(n,{baseUrl:e.baseUrl})}}async sendMessage(e,n){if(this.isMock||!this.sdk)return console.warn(`[FlaresendClient MOCK] Sending message to ${e}: ${n}`),{success:!0,messageId:`mock-fs-${Date.now()}`};try{let t=await this.sdk.messages.send({recipients:[e],type:"text",text:n});return{success:t.success,messageId:t.successful_recipients?.[0]}}catch(t){let r=ne(t);throw r.isAmbiguous?console.warn(`[AMBIGUOUS_SEND_FAILURE] Flaresend send to ${e} resulted in ambiguous error:`,r.message):console.error(`[KNOWN_NOT_SENT_FAILURE] Flaresend send to ${e} failed before transmission:`,r.message),r}}};var Q=require("crypto");function re(s){let e=[],n=[];if(!s)return{messages:e,statuses:n};if(Array.isArray(s.messages))for(let t of s.messages)t&&t.id&&t.from&&e.push({id:t.id,from:t.from,text:t.text?.body||t.text||"",timestamp:Number(t.timestamp)||Date.now()});if(Array.isArray(s.statuses))for(let t of s.statuses)t&&t.id&&n.push({id:t.id,status:t.status||"",recipient_id:t.recipient_id||t.recipient||"",timestamp:Number(t.timestamp)||Date.now()});if(Array.isArray(s.entry)){for(let t of s.entry)if(Array.isArray(t.changes))for(let r of t.changes){let a=r.value;if(a){if(Array.isArray(a.messages))for(let l of a.messages)l&&l.id&&l.from&&e.push({id:l.id,from:l.from,text:l.text?.body||l.text||"",timestamp:Number(l.timestamp)||Date.now()});if(Array.isArray(a.statuses))for(let l of a.statuses)l&&l.id&&n.push({id:l.id,status:l.status||"",recipient_id:l.recipient_id||l.recipient||"",timestamp:Number(l.timestamp)||Date.now()})}}}return{messages:e,statuses:n}}function E(s){if(!s.webhookSecret&&!s.allowUnsigned)throw new Error("Webhook security violation: webhookSecret is not configured, and allowUnsigned is not set to true. You must explicitly provide webhookSecret or set allowUnsigned: true to allow unverified payloads.")}function oe(s,e,n){if(!e||!n)return!1;let t=e.startsWith("sha256=")?e.substring(7):e,r=(0,Q.createHmac)("sha256",n);r.update(s);let a=r.digest("hex");try{return(0,Q.timingSafeEqual)(Buffer.from(t,"hex"),Buffer.from(a,"hex"))}catch{return!1}}async function F(s,e){E(e);let{idempotencyService:n,queue:t,webhookSecret:r}=e;if(r&&!e.allowUnsigned){let c=s.headers["x-hub-signature-256"]||s.headers["x-flaresend-signature"],o=s.rawBody;if(!o)throw new Error("Webhook signature verification failed on configuration: webhookSecret is active, but raw request body capture (e.g. req.rawBody) is missing. Please configure a raw-body capture middleware/plugin to preserve exact wire bytes.");if(!oe(o,c,r))return{status:401,body:{error:"Unauthorized: Invalid webhook signature"}}}let{messages:a,statuses:l}=re(s.body);if(a.length===0&&l.length===0)return{status:400,body:{error:"Bad Request: Invalid payload structure or missing entries"}};let h=0,d=0;for(let c of a)if(await n.checkAndLock(c.id))try{await t.add("whatsapp-message",{wamid:c.id,waUserId:c.from,messageText:c.text,timestamp:c.timestamp}),h++}catch(i){throw await n.release(c.id),i}else d++;return{status:200,body:{status:"success",processed:{enqueuedMessages:h,duplicateMessages:d,statusUpdates:l.length}}}}function V(s){return E(s),async(e,n)=>{let t=e.rawBody;if(Buffer.isBuffer(t)?t=t.toString("utf8"):typeof t!="string"&&(t=void 0),s.webhookSecret&&!s.allowUnsigned&&!t)throw new Error("Webhook signature verification failed on configuration: webhookSecret is active, but raw request body capture (e.g. request.rawBody) is missing. Please configure a raw-body capture plugin (such as fastify-raw-body) to preserve exact wire bytes.");let r={body:e.body,rawBody:t,headers:e.headers},a=await F(r,s);return n.code(a.status).send(a.body)}}function pe(s){return V(s)}function se(s){return E(s),async(e,n)=>{try{let t=e.rawBody;Buffer.isBuffer(t)?t=t.toString("utf8"):typeof t!="string"&&(t=void 0);let r={body:e.body,rawBody:t,headers:e.headers},a=await F(r,s);n.status(a.status).json(a.body)}catch(t){n.status(500).json({error:t.message})}}}function me(s){return se(s)}function Ee(s){return new Promise((e,n)=>{let t="";s.on("data",r=>{t+=r}),s.on("end",()=>{e(t)}),s.on("error",r=>{n(r)})})}function ie(s){return E(s),async(e,n)=>{try{let t=await Ee(e),r=null;if(t)try{r=JSON.parse(t)}catch{r=t}let a={body:r,rawBody:t,headers:e.headers},l=await F(a,s);n.writeHead(l.status,{"Content-Type":"application/json"}),n.end(JSON.stringify(l.body))}catch(t){n.writeHead(500,{"Content-Type":"application/json"}),n.end(JSON.stringify({error:t.message}))}}}function ae(s){return ie(s)}var ge=ae;var H=require("bullmq");var de=require("crypto"),R=class extends Error{constructor(n,t){super(t||`Failed to acquire concurrency lease for user "${n}" within timeout.`);this.waUserId=n;this.name="ConcurrencyLockError"}waUserId},L=class{locks=new Map;async acquire(e,n){let t=n?.ttlMs??1e4,r=n?.acquireTimeoutMs??5e3,a=n?.retryIntervalMs??50,h=`${n?.keyPrefix??"fsm:lock:"}${e}`,d=(0,de.randomUUID)(),c=Date.now();for(;;){let o=Date.now(),i=this.locks.get(h);if(!i||i.expiresAt<=o)return this.locks.set(h,{token:d,expiresAt:o+t}),{leaseKey:h,token:d,waUserId:e,acquiredAt:o,ttlMs:t,release:async()=>{let f=this.locks.get(h);return f&&f.token===d?(this.locks.delete(h),!0):!1}};if(Date.now()-c>=r)throw new R(e,`Failed to acquire in-memory concurrency lease for user "${e}" within ${r}ms.`);await new Promise(u=>setTimeout(u,a))}}async isLocked(e,n="fsm:lock:"){let t=`${n}${e}`,r=this.locks.get(t);return r?r.expiresAt>Date.now():!1}clear(){this.locks.clear()}},q=class s{constructor(e){this.redisClient=e}redisClient;static UNLOCK_SCRIPT=`
2
+ if redis.call("get", KEYS[1]) == ARGV[1] then
3
+ return redis.call("del", KEYS[1])
4
+ else
5
+ return 0
6
+ end
7
+ `;async acquire(e,n){let t=n?.ttlMs??1e4,r=n?.acquireTimeoutMs??5e3,a=n?.retryIntervalMs??50,h=`${n?.keyPrefix??"fsm:lock:"}${e}`,d=(0,de.randomUUID)(),c=Date.now();for(;;){try{let i=await this.redisClient.set(h,d,"PX",t,"NX");if(i==="OK"||i===1){let u=Date.now();return{leaseKey:h,token:d,waUserId:e,acquiredAt:u,ttlMs:t,release:async()=>{try{return await this.redisClient.eval(s.UNLOCK_SCRIPT,1,h,d)===1}catch(f){return console.error(`[RedisConcurrencyLease] Failed to release lease for user ${e}:`,f),!1}}}}}catch(i){console.error(`[RedisConcurrencyLease] Redis error while acquiring lock for ${e}:`,i)}if(Date.now()-c>=r)throw new R(e,`Failed to acquire Redis concurrency lease for user "${e}" within ${r}ms.`);let o=Math.floor(Math.random()*20);await new Promise(i=>setTimeout(i,a+o))}}async isLocked(e,n="fsm:lock:"){let t=`${n}${e}`;return await this.redisClient.get(t)!==null}};function Y(s){return s&&typeof s.set=="function"?new q(s):new L}async function ce(s,e){let{waUserId:n,messageText:t}=s;if(!e.flow.nodes||e.flow.nodes.length===0)throw new Error(`[FSM Worker] Flow definition "${e.flow.flowId}" is invalid: no nodes defined.`);let r=null;e.leaseService&&(r=await e.leaseService.acquire(n,e.leaseOptions));try{let{nextState:a,result:l}=await U(n,t,{persistence:e.persistence,flow:e.flow}),h=a,d=!1;if(l.outboundText)try{let c=await e.flaresendClient.sendMessage(n,l.outboundText);if(d=!0,c?.messageId)try{let o=await e.persistence.load(n);o&&(o.context.lastOutboundMessageId=c.messageId,await e.persistence.save(o),h=o)}catch(o){console.warn(`Failed to persist lastOutboundMessageId for ${n}`,o)}}catch(c){let o=c?.isAmbiguous??!0;if(o?console.warn(`[AMBIGUOUS_SEND_FAILURE] Outbound send failed with unconfirmed delivery status for user ${n}. State saved successfully. Delegating to resend queue with conservative initial backoff.`,c):console.error(`[KNOWN_NOT_SENT_FAILURE] Outbound send failed before transmission for user ${n}. State saved successfully. Delegating to resend queue with standard initial backoff.`,c),e.resendQueue){let i=o?5e3:1e3;await e.resendQueue.add("resend-message",{waUserId:n,outboundText:l.outboundText,isAmbiguous:o},{attempts:3,backoff:{type:"exponential",delay:i}})}else throw c}return{nextState:h,sentReply:d,result:l}}finally{if(r)try{await r.release()}catch(a){console.warn(`[FSM Worker] Failed to release concurrency lease for user ${n}:`,a)}}}function we(s){let e=s.queueName||"webhook-messages",n=s.resendQueueName||`${e}-resends`,t=new H.Queue(n,{connection:s.connection}),r=new H.Worker(n,async d=>{let{waUserId:c,outboundText:o,isAmbiguous:i}=d.data;i?console.warn(`[Resend Worker AMBIGUOUS_RETRY] Attempting delivery retry for unconfirmed send (user: ${c}, job: ${d.id})`):console.log(`[Resend Worker STANDARD_RETRY] Attempting delivery retry for known-not-sent message (user: ${c}, job: ${d.id})`);let u=await s.flaresendClient.sendMessage(c,o);if(u?.messageId)try{let f=await s.persistence.load(c);f&&(f.context.lastOutboundMessageId=u.messageId,await s.persistence.save(f))}catch(f){console.warn(`[Resend Worker] Failed to update lastOutboundMessageId in state for user ${c}`,f)}},{connection:s.connection,concurrency:s.concurrency||1});r.on("completed",d=>{console.log(`[Resend Worker] Resend job ${d.id} completed successfully.`)}),r.on("failed",(d,c)=>{console.error(`[Resend Worker] Resend job ${d?.id} failed after maximum retry attempts. Error:`,c)});let a=s.leaseService??(s.enableConcurrencyLease!==!1&&s.connection?Y(s.connection):void 0),l=new H.Worker(e,async d=>{console.log(`[FSM Worker] Received queued job ${d.id} for user: ${d.data.waUserId}`);let{wamid:c,waUserId:o,messageText:i,timestamp:u}=d.data;if(!o||typeof i!="string")throw new Error(`[FSM Worker] Invalid job payload: ${JSON.stringify(d.data)}`);return await ce({wamid:c,waUserId:o,messageText:i,timestamp:u},{persistence:s.persistence,flow:s.flow,flaresendClient:s.flaresendClient,resendQueue:t,leaseService:a,leaseOptions:s.leaseOptions})},{connection:s.connection,concurrency:s.concurrency||1});l.on("completed",d=>{console.log(`[FSM Worker] Job ${d.id} completed successfully.`)}),l.on("failed",(d,c)=>{console.error(`[FSM Worker] Job ${d?.id} failed with error:`,c)});let h=l.close.bind(l);return l.close=async()=>{console.log("[FSM Worker] Initiating graceful shutdown of FSM worker and companion resend system..."),await Promise.all([h(),r.close(),t.close()])},l}var X=class{traces=[];listeners=[];constructor(e){e&&(this.traces=[...e])}logTurn(e){let n={...e,id:`turn_${Date.now()}_${Math.random().toString(36).substring(2,7)}`,timestamp:Date.now()};return this.traces.push(n),this.listeners.forEach(t=>t(n)),n}onTurn(e){return this.listeners.push(e),()=>{this.listeners=this.listeners.filter(n=>n!==e)}}getTraces(){return[...this.traces]}getSessions(){let e=new Map;for(let t of this.traces)e.has(t.sessionId)||e.set(t.sessionId,[]),e.get(t.sessionId).push(t);let n=[];return e.forEach((t,r)=>{t.sort((h,d)=>h.timestamp-d.timestamp);let a=t[t.length-1],l=t.reduce((h,d)=>h+(d.retryCount>0?1:0),0);n.push({sessionId:r,flowId:t[0].flowId,startTime:t[0].timestamp,endTime:a.timestamp,turns:t,finalStatus:a.status,totalRetries:l})}),n.sort((t,r)=>r.endTime-t.endTime)}computeMetrics(e){let n=this.getSessions(),t=n.length;if(t===0)return{totalSessions:0,completedSessions:0,escalatedSessions:0,activeSessions:0,containmentRate:100,completionRate:0,escalationRate:0,avgTurnsPerSession:0,totalRetries:0,nodeStats:{}};let r=0,a=0,l=0,h=0,d=0,c={};if(e)for(let p of e.nodes)c[p.id]={visits:0,retries:0,dropOffs:0,unhandledInputs:[],patternBreakdown:{}};for(let p of n){h+=p.turns.length,p.finalStatus==="completed"?r++:p.finalStatus==="escalated"?a++:l++;for(let m=0;m<p.turns.length;m++){let g=p.turns[m],y=m===p.turns.length-1;c[g.fromNodeId]||(c[g.fromNodeId]={visits:0,retries:0,dropOffs:0,unhandledInputs:[],patternBreakdown:{}});let b=c[g.fromNodeId];b.visits++,g.retryCount>0&&(b.retries++,b.unhandledInputs.includes(g.rawInput)||b.unhandledInputs.push(g.rawInput));let x=g.matchedPattern||"[unmatched]";b.patternBreakdown[x]=(b.patternBreakdown[x]||0)+1,y&&p.finalStatus!=="completed"&&b.dropOffs++}}let o=t>0?Math.round((t-a)/t*100):100,i=Math.round(r/t*100),u=Math.round(a/t*100),f=Number((h/t).toFixed(1));return{totalSessions:t,completedSessions:r,escalatedSessions:a,activeSessions:l,containmentRate:o,completionRate:i,escalationRate:u,avgTurnsPerSession:f,totalRetries:d,nodeStats:c}}clear(){this.traces=[]}};var Z=class{constructor(e){this.flow=e}flow;async runScenario(e){let n=Date.now(),t=new k(this.flow),r=t.createInitialState(`eval_user_${e.id}`);e.initialContext&&(r={...r,context:{...e.initialContext}});let a=[],l=new Set([r.currentNodeId]),h=!0;for(let d=0;d<e.steps.length;d++){let c=e.steps[d],o=await t.transition(r,c.input);r=o.nextState,l.add(r.currentNodeId);let i=!0,u=[];if(c.expectedNodeId&&r.currentNodeId!==c.expectedNodeId&&(i=!1,u.push(`Expected node "${c.expectedNodeId}", but transitioned to "${r.currentNodeId}"`)),c.expectedOutboundPattern){let f=o.outboundText??"";typeof c.expectedOutboundPattern=="string"?f.toLowerCase().includes(c.expectedOutboundPattern.toLowerCase())||(i=!1,u.push(`Outbound text "${f}" did not contain expected substring "${c.expectedOutboundPattern}"`)):c.expectedOutboundPattern instanceof RegExp&&(c.expectedOutboundPattern.test(f)||(i=!1,u.push(`Outbound text "${f}" did not match regex pattern ${c.expectedOutboundPattern}`)))}if(c.expectedContext)for(let[f,p]of Object.entries(c.expectedContext))r.context[f]!==p&&(i=!1,u.push(`Context key "${f}" expected value ${JSON.stringify(p)}, got ${JSON.stringify(r.context[f])}`));c.expectEscalated&&r.status!=="escalated"&&(i=!1,u.push(`Expected conversation to be escalated, but status was "${r.status}"`)),c.expectCompleted&&r.status!=="completed"&&(i=!1,u.push(`Expected conversation to be completed, but status was "${r.status}"`)),i||(h=!1),a.push({stepIndex:d+1,input:c.input,actualNodeId:r.currentNodeId,expectedNodeId:c.expectedNodeId,actualOutbound:o.outboundText,expectedOutboundPattern:c.expectedOutboundPattern,actualStatus:r.status,actualContext:{...r.context},passed:i,failureReason:u.join("; ")})}return{scenarioId:e.id,scenarioName:e.name,passed:h,stepResults:a,durationMs:Date.now()-n,visitedNodeIds:Array.from(l)}}async runSuite(e){let n=Date.now(),t=[],r=new Set;for(let f of e){let p=await this.runScenario(f);t.push(p),p.visitedNodeIds.forEach(m=>r.add(m))}let a=e.length,l=t.filter(f=>f.passed).length,h=a-l,d=a>0?Math.round(l/a*100):0,c=this.flow.nodes.map(f=>f.id),o=c.filter(f=>r.has(f)),i=c.filter(f=>!r.has(f)),u=c.length>0?Math.round(o.length/c.length*100):100;return{totalScenarios:a,passedScenarios:l,failedScenarios:h,passRate:d,nodeCoveragePercentage:u,coveredNodeIds:o,uncoveredNodeIds:i,scenarioResults:t,totalDurationMs:Date.now()-n}}};0&&(module.exports={ActionNodeBuilder,AmbiguousSendError,Bot,CallbackPersistenceAdapter,CompileError,ConcurrencyLockError,Context,Evaluator,FSMEngine,FlaresendClient,FlaresendSendError,FlowNodeBuilder,GlobalTransitionBuilder,HandoffNodeBuilder,IdempotencyService,InMemoryConcurrencyLeaseService,KnownNotSentError,MongoosePersistenceAdapter,OptimisticConcurrencyError,RedisConcurrencyLeaseService,TraceCollector,classifyFlaresendError,createConcurrencyLeaseService,createExpressAdapter,createFSMWorker,createFastifyAdapter,createLiteralMatcher,createRawHttpAdapter,expressAdapter,extractWebhookPayload,fastifyAdapter,handleWebhookCore,httpAdapter,matchesAny,processFSMJob,rawHttpAdapter,registerFlowDefinition,runConversationStep,verifyWebhookSignature});