Skip to main content

hbf-bot Gotchas

Package-specific pitfalls and non-obvious patterns. Loaded automatically by workflow commands.

Event Routing

  • MicrosoftChannel.processCustomEvents() is a whitelist: onEventReceived() routes type:"event" activities to processCustomEvents(), 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 in processCustomEvents() or use AuthEventStep (lifecycle pipeline) for auth-related events.

HTTP Requests

  • Use HttpRequest from app/util/HttpRequest.ts, not raw axios: The codebase wraps axios through HttpRequest (20+ files use it). Only HttpRequest.ts itself 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 for EventOrigin.LIVECHAT. Analytics records are created when the ITC-reprocessed event runs through the pipeline.
  • data_collector must be in the default component tree: Without it in both branches of defaultComponentTree.right, postback/echo paths skip DataCollector, losing analytics.
  • messageCount on ChatSession does not equal messages.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}-resume pattern.
  • 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. Use inputContexts to scope activation.
  • AgenticBotHandler bypasses context matching for empty-text events: postbackConvNodeId = "DEFAULT_NODE" skips UNIVERSAL+context matching. Pre-set blackboard.currentNext before processEvent() to route directly.
  • isBotPrompt is NOT flow control: It only pushes onto the question stack for back-button tracking.
  • NodeRunner enqueues next BEFORE running the action: Actions can override with nukeNext() / enqueueNext().
  • Bot response converter only knows two custom schemas: MICROSOFT-SUGGESTED-ACTIONS and MICROSOFT-ADAPTIVE-CARD. Unknown schemas fall through to convertAdaptiveCardMessage() which requires payload.elements.
  • config.get() returns null for empty YAML keys, not throws: String(null) = "null". Check with config.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 AuthVariablesResolver from Auth.* namespace: Auth.isAuthenticated, Auth.rawToken, Auth.refreshToken are blocked (internal-only). All three blackboard tiers guard auth/auth.* keys.
  • Auth strategy pattern: OidcAuthStrategy and PreAuthTokenStrategy wrap validation/refresh/logout. AuthenticateUserAction uses getAuthStrategy(method) to select.
  • Pre-auth token flow reads channelData.auth.token: On-change-only validation compares incoming token against stored auth.rawToken. Provider matching by aud equality.
  • 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.
  • sendAuthEvent is a direct WebWidgetClient proactive push, NOT via tenant.processEvent(): WebWidget-only; other channels don't have this concept.