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.
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.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
buttonsorcardwith 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 withUnsupportedContentError. 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 anunknownmessage with the payload inctx.ref.native, not as abuttoninteraction 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
langoption for the labelspublishForm()bakes into a Flow (default'en'): set it to your bot’sBotConfig.lang.publishForm(def, { lang })overrides it. - Deprecated (removal in v3):
toProviderError(usetoGraphErrorfrom@botmesh/graph-core),GraphClient(transport internals), the re-exportedverifyChallenge/verifySignature(useprovider.verifyChallenge(query)andprovider.ingest(), or import them from@botmesh/graph-core) anddebug(pass alogger).phoneNumberIdis typedstring | number.
@botmesh/telegram
- In webhook mode,
ingest()afterstop()answers503and 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_answerbecomes a'poll'event with the same shape as on WhatsApp (vote.ref,vote.senderId,vote.optionswithvoters). 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):
pollTimeoutMsreplacespollTimeout(seconds, deprecated):pollTimeout: 30ispollTimeoutMs: 30_000.toProviderErroris nowtoTelegramError(the old name is a deprecated alias),debugis deprecated (pass alogger), and the wire helpersparseUpdate,parsePollAnswer,toOutboundandMEDIA_UPLOADare@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/internaland@botmesh/wppconnect/internal, which have no semver guarantee. WaIdentityStoreandnormalizeJidare no longer re-exported by the providers: import them from@botmesh/wa-gateway-kit.IdentityPersistenceandRestHttpOptionsstay on the providers’ main entry.- The
debugoption was removed: pass alogger(pino({ level: 'debug' })). - Invalid options throw a
TypeError(it was a plainError; the messages are the same). Also,baseUrlno longer accepts credentials (user:pass@), a query string or a fragment,identityCacheSizemust be a positive integer (orInfinity), andhttp.timeoutMs/http.maxBytesare 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 acceptshttp(s)or a base64data:URI (sent inline), and a local path orfile:URL there rejects withUnsupportedContentErrorinstead 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
/internalentry (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:dialogflowPluginis the canonical name anddialogflowContexta deprecated alias.languageCodedefaults to'en'(it was'es'): set it to your agent’s language. In groups,sessionScopedefaults to'participant'(one Dialogflow session per member, like conversations); setsessionScope: 'chat'to keep one session per group. Mistyped options (minConfidence: 80, an unknownfulfillment…) now throwDialogflowError(INVALID_CONFIG) atbot.plug(), and texts over 256 characters are no longer sent (maxTextLength).- Frozen data:
DEFAULT_VOICE_LIMITS,SUPPORTED_EXTENSIONS(rag),TICKET_PRIORITIESandTICKET_STATUSEScan no longer be mutated; copy them first. @botmesh/chatwoot: a webhook signature withoutx-chatwoot-timestampis rejected (it never expired, so a captured request could be replayed). SetwebhookAllowUntimedSignature: trueonly for a sender that cannot add the timestamp.@botmesh/tickets:TicketKeyValuehas an optionalsetIf. 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 ofacknowledgeInteraction()was removed before the first publish (only ids the provider received are known). Interactions the bot does not acknowledge withinautoAcknowledgeMs(2 s; the alphaautoAckMsalias was removed) are deferred for it; setautoAcknowledgeMs: falseto keep doing it yourself. Sent messages carryallowed_mentions: { parse: ['users'] }unless set (allowedMentionschanges it), so@everyone,@hereand role mentions no longer ping by default. Message edits (MESSAGE_UPDATE) now arrive as messages with the originalref.id(isEditedMessage(ctx);editedMessages: 'ignore'drops them).- Error mappers renamed (old names are deprecated aliases):
toTikTokError,toEmailError.toDiscordErrorhas no alias:toProviderErrorwas removed from@botmesh/discordbefore its first publish. - Product stores:
InboxStore.erase,ContactStore.eraseandDeploymentStore.pruneare new required methods, andDeploymentStore.list/InboxStore.listNotesaccept anaftercursor. A custom store must add them (the shared conformance suites intest-utils/check them).attachInbox()withouttenantIdnow files conversations under the bot’s own tenant instead of'local'. @botmesh/flow-schema:publishFlow()takes any registry with a compatiblepublish()(aDeploymentRegistryfits) and no longer depends on@botmesh/deploymentsat run time.