Skip to main content

Public packages

@botmesh/baileys

toJid() throws on input it cannot turn into a JID. It returned '@s.whatsapp.net' for an input without digits, and an invalid …@g.us:<user> string for { groupId, userId }. Both now throw a TypeError.
A group participant has no JID of its own: use the group JID for the chat and toJid(userId) for the person. Text edits reach the bot. An edited message used to be dropped. It now arrives as a message with the new text and the original ref.id, like on Telegram. isEditedMessage(ctx) tells it apart. The core dedupe lets an edit through and still drops an identical retry. Only an edit of a message this process has seen, sent in the same chat by the user who sends the edit, is delivered: an edit naming someone else’s message, one of the bot’s own or one from another chat is dropped, and so is an edit whose original is no longer in the provider’s message cache (for example after a restart). To keep the old behaviour:

@botmesh/ai

provider: 'openai' with a baseUrl sends max_tokens. For any baseUrl other than api.openai.com or Azure OpenAI (*.openai.azure.com, *.cognitiveservices.azure.com, or an Azure AI Foundry *.services.ai.azure.com endpoint under /openai), the output-token cap now goes in max_tokens, which OpenAI-compatible servers (vLLM, LM Studio, gateways) expect. If your baseUrl is a proxy in front of OpenAI and you use an o-series or gpt-5 model, set the field back:
Memory options fail fast. An invalid memory.maxFactLength, memory.maxFacts or memory.ttl now throws a RangeError from aiPlugin() instead of failing at plug() (or, for maxFactLength, silently dropping every fact). onStreamEnd has a fixed contract. GenerateInput.onStreamEnd(info) (an AIStreamEndInfo: stopReason, plus usage on Anthropic and Gemini) is called exactly once per stream() call, after the last chunk and before the iteration completes, and only when the stream ends normally: never when it fails, is aborted or the consumer breaks out early. An exception thrown by the callback fails the consumer’s iteration. Every built-in provider and MockAIProvider follow it, including the abort rule when the client ignores signal (an aborted stream skips the callback and rejects with the abort reason); a custom AIProvider that calls onStreamEnd should too (check signal.aborted before calling it). Entries and frozen data. MockAIProvider moved to @botmesh/ai/testing, the plugin’s helpers and the SDK wire type names to @botmesh/ai/internal, and the exported vocabularies, presets and texts are frozen. See Migrating to the stable API.

@botmesh/meta

  • Interactive limits are checked before sending. A buttons or card with no button, repeated button ids or titles (compared after the 20-character cut), a list with no rows, more than 10 rows, or repeated row ids now reject with UnsupportedContentError. Graph rejected them before too, but with an opaque error after the request.
  • An interactive reply that is not a press (for example call_permission_reply) arrives as an unknown message with the payload in ctx.ref.native, not as a button interaction with empty data. A handler that matched those interactions must read the message instead.
  • Media uploads have their own timeout, uploadTimeoutMs (5 minutes by default).
  • New lang option for the labels publishForm() bakes into a Flow (default 'en'): set it to your bot’s BotConfig.lang. publishForm(def, { lang }) overrides it.
  • Deprecated (removal in v3): toProviderError (use toGraphError from @botmesh/graph-core), GraphClient (transport internals), the re-exported verifyChallenge/verifySignature (use provider.verifyChallenge(query) and provider.ingest(), or import them from @botmesh/graph-core) and debug (pass a logger). phoneNumberId is typed string | number.

@botmesh/telegram

  • In webhook mode, ingest() after stop() answers 503 and emits nothing (Telegram redelivers the Update after the restart). Hosts that started the provider again do not need to change anything.
  • File uploads have their own timeout, uploadTimeoutMs (5 minutes by default).
  • Polls are sent non-anonymous and their votes reach the bot. Each poll_answer becomes a 'poll' event with the same shape as on WhatsApp (vote.ref, vote.senderId, vote.options with voters). Members of the chat can see who voted. Channels refuse public-voter polls, so a poll to a channel is retried once as anonymous (no votes) and the channel is remembered. To keep anonymous polls (and receive no votes):
  • pollTimeoutMs replaces pollTimeout (seconds, deprecated): pollTimeout: 30 is pollTimeoutMs: 30_000. toProviderError is now toTelegramError (the old name is a deprecated alias), debug is deprecated (pass a logger), and the wire helpers parseUpdate, parsePollAnswer, toOutbound and MEDIA_UPLOAD are @experimental.

@botmesh/sqlite-store

saveMessage and setCursor throw a TypeError for a message id, chat id, cursor key or cursor value with a lone UTF-16 surrogate (before, SQLite stored it as U+FFFD and two ids could share a row). Escape or drop such characters before storing them. Schema versioning. The database records its layout in PRAGMA user_version (SQLITE_SCHEMA_VERSION, now 2). Opening a file written by an earlier release (version 0 or 1) migrates it in place, keeping every row; nothing to do. Version 2 folds the durable scheduler’s tables (schedules, schedule_fences) and the durability table (durable) into the versioned migrations: they used to be created ad hoc the first time schedules() or durability() was called, and are now created when the file is opened. A file written by a newer release is refused with SQLiteSchemaVersionError (code: 'SQLITE_SCHEMA_TOO_NEW') and left untouched: downgrades are not supported, so back up the file before upgrading if you may need to roll back. Rolling back to the previous alpha. The previous alpha writes and reads schema version 1. Once this release opens a file, the file is at version 2 and the previous alpha refuses it with SQLITE_SCHEMA_TOO_NEW on its next start, even if the app never calls schedules() or durability(). This also applies during a rolling deploy: a process still on the previous alpha that shares the file fails when it restarts, so stop every process on the old version before the first one on the new version opens the file. To roll back, restore the copy taken before the upgrade. No rows were changed by the migration (it only adds tables with CREATE TABLE IF NOT EXISTS), so if you have no copy you can set PRAGMA user_version = 1 by hand with the sqlite3 CLI while no process has the file open, and the previous alpha opens it again. The version bump exists so that later migrations can assume these tables exist; without it they would stay outside the version. New options: busyTimeoutMs (default 5000, PRAGMA busy_timeout) and a logger that takes any LoggerLike. The exported SCHEDULES_SCHEMA and DURABLE_SCHEMA DDL strings are deprecated.

@botmesh/messenger and @botmesh/instagram

Both channels are public from the beta (1.0.0-beta), so their first npm release already has the frozen entry. Inside the monorepo or a fork, an import of a removed name fails to compile: The debug option is deprecated in both: pass a logger instead. More than 3 buttons (or 10 list rows) on Messenger, or more than 13 quick replies on Instagram, reject with UnsupportedContentError instead of being cut.

@botmesh/evolution, @botmesh/wppconnect and @botmesh/wa-gateway-kit

New on npm in the beta (they were private before). If you used them from the monorepo:
  • The webhook parsers, outbound mappers and media tokens (parseWebhook, parseMessage, toOutbound, numberedMenu, keyFromRef, target, jidOf…) and Evolution’s JWT helpers (verifyHs256Jwt, signHs256Jwt) moved to @botmesh/evolution/internal and @botmesh/wppconnect/internal, which have no semver guarantee.
  • WaIdentityStore and normalizeJid are no longer re-exported by the providers: import them from @botmesh/wa-gateway-kit. IdentityPersistence and RestHttpOptions stay on the providers’ main entry.
  • The debug option was removed: pass a logger (pino({ level: 'debug' })).
  • Invalid options throw a TypeError (it was a plain Error; the messages are the same). Also, baseUrl no longer accepts credentials (user:pass@), a query string or a fragment, identityCacheSize must be a positive integer (or Infinity), and http.timeoutMs / http.maxBytes are checked when the provider is built, not on the first request.
  • Only an http(s) URL is handed to the gateway to download. { url } is always a link, as on Meta and Telegram: it accepts http(s) or a base64 data: URI (sent inline), and a local path or file: URL there rejects with UnsupportedContentError instead of being read (it used to be passed as is, and the gateway tried to open that path on its own host). Send a local file as { path } or a plain path string; the bot reads it and sends it as base64.

Plugins promoted to npm

@botmesh/rag, @botmesh/voice, @botmesh/nlu, @botmesh/dialogflow, @botmesh/chatwoot, @botmesh/tickets and @botmesh/moderation are published on npm from the beta (1.0.0-beta). Code that used them from the monorepo (workspace:*) needs these changes:
  • Helpers moved to a /internal entry (no semver; applications rarely need them): @botmesh/rag/internal (splitMarkdownSections, resolveChunkOptions, cosine, hashText, tokenize, resolveEmbedder, createPdfParser), @botmesh/voice/internal (trimPunctuation, resolveStt, resolveTts, OPENAI_TTS_MIMETYPES, elevenLabsMimetype, estimateOggDurationSec, estimateDurationSec, sniffMimetype), @botmesh/dialogflow/internal (normalizeEsResult, normalizeCxResult, toSessionId, parseCredentials, createServiceAccountJwt, DIALOGFLOW_SCOPE, DEFAULT_TOKEN_URI), @botmesh/chatwoot/internal (ChatwootMapping, verifyWebhook, inboundText, outboundText), @botmesh/tickets/internal (renderTemplate, localClock) and @botmesh/moderation/internal (findLinks, findInviteLinks, compileTerms, foldText).
  • @botmesh/dialogflow: dialogflowPlugin is the canonical name and dialogflowContext a deprecated alias. languageCode defaults to 'en' (it was 'es'): set it to your agent’s language. In groups, sessionScope defaults to 'participant' (one Dialogflow session per member, like conversations); set sessionScope: 'chat' to keep one session per group. Mistyped options (minConfidence: 80, an unknown fulfillment…) now throw DialogflowError (INVALID_CONFIG) at bot.plug(), and texts over 256 characters are no longer sent (maxTextLength).
  • Frozen data: DEFAULT_VOICE_LIMITS, SUPPORTED_EXTENSIONS (rag), TICKET_PRIORITIES and TICKET_STATUSES can no longer be mutated; copy them first.
  • @botmesh/chatwoot: a webhook signature without x-chatwoot-timestamp is rejected (it never expired, so a captured request could be replayed). Set webhookAllowUntimedSignature: true only for a sender that cannot add the timestamp.
  • @botmesh/tickets: TicketKeyValue has an optional setIf. A custom storage that implements it switches the desk to shared mode (safe for replicas).

Packages that were private

These notes matter for code that used the packages from the monorepo or a fork before the beta. @botmesh/discord, @botmesh/email and @botmesh/flow-schema are on npm from the beta; @botmesh/tiktok and the product packages are still private.
  • @botmesh/discord: ctx.interaction.id (interactionId) is the bare interaction id; the token stays in the provider, and the alpha "<id>:<token>" form of acknowledgeInteraction() was removed before the first publish (only ids the provider received are known). Interactions the bot does not acknowledge within autoAcknowledgeMs (2 s; the alpha autoAckMs alias was removed) are deferred for it; set autoAcknowledgeMs: false to keep doing it yourself. Sent messages carry allowed_mentions: { parse: ['users'] } unless set (allowedMentions changes it), so @everyone, @here and role mentions no longer ping by default. Message edits (MESSAGE_UPDATE) now arrive as messages with the original ref.id (isEditedMessage(ctx); editedMessages: 'ignore' drops them).
  • Error mappers renamed (old names are deprecated aliases): toTikTokError, toEmailError. toDiscordError has no alias: toProviderError was removed from @botmesh/discord before its first publish.
  • Product stores: InboxStore.erase, ContactStore.erase and DeploymentStore.prune are new required methods, and DeploymentStore.list / InboxStore.listNotes accept an after cursor. A custom store must add them (the shared conformance suites in test-utils/ check them). attachInbox() without tenantId now files conversations under the bot’s own tenant instead of 'local'.
  • @botmesh/flow-schema: publishFlow() takes any registry with a compatible publish() (a DeploymentRegistry fits) and no longer depends on @botmesh/deployments at run time.