hbf-bot Gotchas
Package-specific pitfalls and non-obvious patterns. Loaded automatically by workflow commands.
Event Routing
MicrosoftChannel.processCustomEvents()is a whitelist:onEventReceived()routestype:"event"activities toprocessCustomEvents(), which only handles specific event names (webchat/reaction,webchat/userLeftChat,webchat/livechatTermination,auth/logout). Unrecognized event names are silently dropped with no log. When adding a new event type, add an explicit branch inprocessCustomEvents()or useAuthEventStep(lifecycle pipeline) for auth-related events.
HTTP Requests
- Use
HttpRequestfromapp/util/HttpRequest.ts, not rawaxios: The codebase wraps axios throughHttpRequest(20+ files use it). OnlyHttpRequest.tsitself should import axios directly.
Multi-Instance Runtime
- hbf-bot runs multiple instances behind a load balancer. Message 1 may hit instance A, message 2 may hit instance B.
- Auth state MUST be in shared storage (MongoDB session, Redis), never in-process memory.
- JWKS key cache is per-instance (1h TTL in OidcService). Each instance fetches independently.
- Redis-based state (oidc:pending, oidc:result, oidc:verifier) is shared and works correctly across instances.
triggerResume()fire-and-forget after OIDC callback creates a synthetic event routed through normal load balancing. Fine because state is in Redis/session.
DataCollector and Live Chat Analytics
- DataCollector only runs on ITC events, not raw LIVECHAT events:
_isEnabled()returns false forEventOrigin.LIVECHAT. Analytics records are created when the ITC-reprocessed event runs through the pipeline. data_collectormust be in the default component tree: Without it in both branches ofdefaultComponentTree.right, postback/echo paths skip DataCollector, losing analytics.messageCounton ChatSession does not equalmessages.length: messageCount tracks a subset. Do not assert equality.
Node Compilation
- Multi-activity resume routing: derive resumeNodeId from the originating node ID:
.find()on all UNIVERSAL nodes picks whichever resume node comes first regardless of which activity initiated. Use${originatingNodeId}-resumepattern. - Nodes that wait for external user action need two-node compilation (prompt + resume): A single node with
nukeNext()/enqueueNext(self)causes an immediate loop. Use the FileUploadNode pattern. - UNIVERSAL intent nodes are always considered:
ConversationManagerLite._process()concatenates UNIVERSAL with detected-intent nodes before context matching. UseinputContextsto scope activation. - AgenticBotHandler bypasses context matching for empty-text events:
postbackConvNodeId = "DEFAULT_NODE"skips UNIVERSAL+context matching. Pre-setblackboard.currentNextbeforeprocessEvent()to route directly. isBotPromptis NOT flow control: It only pushes onto the question stack for back-button tracking.- NodeRunner enqueues
nextBEFORE running the action: Actions can override withnukeNext()/enqueueNext(). - Bot response converter only knows two custom schemas:
MICROSOFT-SUGGESTED-ACTIONSandMICROSOFT-ADAPTIVE-CARD. Unknown schemas fall through toconvertAdaptiveCardMessage()which requirespayload.elements. config.get()returns null for empty YAML keys, not throws:String(null)="null". Check withconfig.has()+ falsy guard.FlowGraphNode.getConversationNodes()base returns empty list: Unrecognized node types silently produce zero ConversationNodes.
Auth
- OIDC config lives in
tenant.settings.security.oidc, not plugins:getOidcConfig(tenant)is the single lookup point. - Auth data resolved via
AuthVariablesResolverfromAuth.*namespace:Auth.isAuthenticated,Auth.rawToken,Auth.refreshTokenare blocked (internal-only). All three blackboard tiers guardauth/auth.*keys. - Auth strategy pattern:
OidcAuthStrategyandPreAuthTokenStrategywrap validation/refresh/logout.AuthenticateUserActionusesgetAuthStrategy(method)to select. - Pre-auth token flow reads
channelData.auth.token: On-change-only validation compares incoming token against storedauth.rawToken. Provider matching byaudequality. - Built-in bot actions have fixed signatures (blackboard only): Cannot add parameters. Use blackboard ephemeral values to pass data.
- Channel metadata shapes vary wildly: WebWidget/Teams = full IActivity, Slack = SlackEvent, WhatsApp = WhatsAppIncomingMessage, Facebook = FbEntry, Zendesk = no metadata. Field access is implicitly channel-specific.
sendAuthEventis a direct WebWidgetClient proactive push, NOT viatenant.processEvent(): WebWidget-only; other channels don't have this concept.