bunqueue v2.9.8
blog · release
bunqueue v2.9.8
Fixes 14 bugs. Streamable HTTP transport (BUNQUEUE_MCP_TRANSPORT=http).
To install bunqueue v2.9.8
Section titled “To install bunqueue v2.9.8”bun add bunqueue@2.9.8npm install bunqueue@2.9.8TAG=2.9.8docker run -p 6789:6789 -p 6790:6790 ghcr.io/egeominotti/bunqueue:$TAGTo upgrade bunqueue
Section titled “To upgrade bunqueue”bun add bunqueue@latestBreaking changes
Section titled “Breaking changes”bunqueue_get_dlqreturns entries instead of jobs. The output is{ queue, reason?, offset, count, hasMore, entries }, with each entry’sjobinside it; the top-leveljobsarray is gone.limitmust be an integer from 1 to 100 (default 20).- Stricter MCP inputs, in both modes.
bunqueue_set_rate_limitandbunqueue_set_concurrencyneed integers of at least 1.bunqueue_add_cronneeds a non-emptyname, an integerrepeatEveryof at least 1, an integerprioritywithin ±1,000,000, and atimezonethat is a known IANA zone and comes with aschedule. Flowoptsreject unknown keys, includingdeduplication,tagsanddurable, instead of dropping them.bunqueue_extend_lockcapsdurationat 24 hours, andlockTtlrequiresowner.bunqueue_move_to_delayedandbunqueue_change_delaycapdelayat one year like the add tools;bunqueue_clean_queuelimit,bunqueue_get_jobsstart/endandbunqueue_get_flowdepth/maxChildrenmust be integers (limit,depthandmaxChildrenat least 1);bunqueue_get_cronandbunqueue_delete_cronreject an empty name. A rejected call changes nothing. bunqueue_wait_for_jobfails with “Job not found” for an unknown id instead of waiting out the timeout, in both modes.
MCP server
Section titled “MCP server”bunqueue-mcp now has 75 tools (73 before) plus 3 opt-in workflow tools. A
client that sets none of the new variables still runs over stdio and sees the
same tools, two new ones and new optional parameters; the output and input
changes are listed under Breaking changes below.
- Streamable HTTP transport (
BUNQUEUE_MCP_TRANSPORT=http). One long-running server serves several MCP clients, one session each, sharing a single queue connection and one set of HTTP handlers. It listens onhttp://127.0.0.1:6791/mcpby default (BUNQUEUE_MCP_HTTP_HOST,_PORT,_PATH) and accepts the bearer tokens inBUNQUEUE_MCP_HTTP_TOKEN, compared in constant time; on a non-loopback address it refuses to start without one.HostandOriginare checked against the bound address andBUNQUEUE_MCP_HTTP_ALLOWED_HOSTS/_ALLOWED_ORIGINSto block DNS rebinding. Sessions are capped (_MAX_SESSIONS, default 100) and closed after 30 idle minutes (_SESSION_TTL_MS), never while a tool call is running. There is no built-in TLS: put a reverse proxy in front. - Workflow approvals from the agent (
BUNQUEUE_MCP_WORKFLOW_DB).bunqueue_list_workflow_executions,bunqueue_get_workflow_executionandbunqueue_signal_workflowread the workflow Engine’s database and deliver human-in-the-loop signals through the same first-writer-wins transaction asengine.signal(); the application’s Engine runs the resumed step. Signals need the Engine connected over TCP with its owndataPath, the MCP server in TCP mode against the same broker, and the file on the same host. With an embedded Engine, or an embedded MCP server, the signal tool is not registered and stderr says why. The server never creates the database: a missing file, or one without the workflow tables, stops startup.BUNQUEUE_MCP_WORKFLOW_QUEUEnames a non-default Engine queue. - Job options.
bunqueue_add_joband eachbunqueue_add_jobs_bulkitem acceptbackoff(a number, or{ type, delay, maxDelay }),timeout,jobId,deduplication({ id, ttl, extend, replace }),removeOnComplete,removeOnFail,durable,lifo,tagsandstallTimeout, next topriority,delayandattempts(new on bulk items). The flow tools accept the sameoptsexceptdeduplication,tagsanddurable.bunqueue_get_joband job listings return the options a job carries. - Lock tokens.
bunqueue_pull_jobandbunqueue_pull_job_batchtakeownerandlockTtl; each job then comes back with atokenthatbunqueue_ack_job,bunqueue_fail_jobandbunqueue_job_heartbeataccept (tokenson the batch tools). A heartbeat with the token renews the lock, andbunqueue_fail_jobtakesunrecoverableto go straight to the DLQ. - DLQ failure reasons and statistics.
bunqueue_get_dlqreturns each entry’sreason, last error, recent attempt history, retry count and retry and expiry times, filters byreasonand pages withoffset. The newbunqueue_get_dlq_statsreturns the total, the entries per reason, the entries due for automatic retry, the expired entries and the oldest and newest entry. Thebunqueue_debug_queueandbunqueue_incident_responseprompts use both instead of a short job list. - Queue limits. The new
bunqueue_get_queue_limitsreads a queue’s rate limit, concurrency limit and active jobs, whether either limit is holding jobs back, and whether the queue is paused.bunqueue_set_rate_limittakesduration(the window in ms, default 1000), so limits per minute or per hour are possible. - Cron options.
bunqueue_add_crontakestimezone,jobName,priority,maxLimit,immediately,skipIfNoWorker,preventOverlap,skipMissedOnRestart,deduplicationand options for the jobs it adds (attempts,backoff,timeout,delay,stallTimeout,removeOnComplete,removeOnFail). Add, list and get returnjobName,priority,timezoneandmaxLimit; the policy flags, deduplication and job options are applied but not returned, because the broker’s cron reply does not carry them. - Load only the tools an agent needs (
BUNQUEUE_MCP_TOOLSETS). The 75 tools are grouped into 11 toolsets (12 with the workflow tools). A list such asqueues,dlqexposes only those groups;dynamicstarts the agent from a catalog and loads groups on demand (bunqueue_enable_toolsets, withbunqueue_call_toolfor clients that do not refresh their tool list). The full list costs about 16,000 tokens of context per request; the catalog with one toolset loaded costs 1,300 to 4,600 (about 1,800 for a typical toolset). On the 11 always-on toolsets, TypeSafe Jev routed 71 of 72 test requests to the right toolset, and 72 of 72 after the description involved in the miss was reworded; the requests, the script and the results are inscripts/mcp-eval/. - Confirm destructive operations (
BUNQUEUE_MCP_CONFIRM=destructive). Every tool gets MCP annotations (read-only, destructive, idempotent, open-world). Nine tools that delete data for good or re-run work in bulk (obliterate_queue,drain_queue,clean_queue,purge_dlq,cancel_job,clear_job_logs,delete_cron,remove_webhook, bulkretry_completed), plussignal_workflowwhen the workflow tools are enabled, first show their concrete impact and run only after the user confirms through the client, or, in clients that cannot ask, after the agent repeats the exact target inconfirm. Previews count paused jobs, andpurge_dlqstates the exact number of entries it deletes. - Decision models (
BUNQUEUE_MCP_DECISION_*). One client for the SystemOne request format serves TypeSafe Jev, Cloudflare Clef and Clef-flash on Workers AI, and any compatible self-hosted endpoint (Clef, Clef-flash, Kev 9B, Laya, DiffusionGemma Jev). It powersbunqueue_find_toolsand an extra check on destructive calls when the client cannot ask the user: the model must judge that the user’s own words ask for exactly that operation. The model can only block a call, and any model error refuses it.
Workflow engine
Section titled “Workflow engine”- A signal racing a
waitFortimeout from another process could be reported as resuming a run that then failed. When a timed gate expired, the worker re-read the run, found no signal, and then wrotefailedwithout checking again. A signal recorded in between from another connection (a second application process, or the MCP server) was accepted withresumed: true, yet the run failed by timeout and was compensated. The expiry is now oneIMMEDIATEtransaction that fails the run only if it is still parked at that gate without the signal, so whichever side commits first wins: a late signal is rejected, and a signal that wins keeps the run going.signal:timeoutis now emitted only after the failure is saved, and never when the signal wins. Regressions:test/repro-workflow-signal-timeout-race.test.ts,test/workflow-signal-timeout-claim.test.ts.
MCP server fixes
Section titled “MCP server fixes”Most of these affected TCP mode (BUNQUEUE_MODE=tcp), where the MCP server
talks to a remote broker. Regressions: test/repro-mcp-tcp-wire.test.ts,
test/repro-mcp-tool-semantics.test.ts,
test/repro-mcp-tcp-handler-longpoll.test.ts,
test/mcp-bin-tcp-handler.test.ts, test/mcp-flow-options.test.ts.
- Broker errors were reported as success in TCP mode. A command the
broker rejected (an invalid job, an unknown id, a wrong lock token, an
authentication error) was treated as successful:
bunqueue_add_jobreturned a job id that did not exist, and acknowledgements, failures and the true/false tools reported success. The broker’s message is now returned as an error; “not found” and “wrong state” still returnsuccess: false. bunqueue_retry_dlqwith ajobIdretried the whole DLQ in TCP mode. The id was sent under a field the broker ignores. The drain, purge and retry counts and the batch heartbeat count read the wrong reply field and showed 0.bunqueue_wait_for_jobreported a timeout as a completion in TCP mode, and in embedded mode waited out the whole timeout for a job that had already completed.- Webhook and worker ids were
"0"in TCP mode.bunqueue_list_webhookswas always empty,bunqueue_remove_webhooksent the wrong field, and registered workers showed no counters. - Job logs, memory statistics and Prometheus metrics were empty in TCP
mode,
bunqueue_get_statsincluded protocol fields, andbunqueue_clear_job_logsignoredkeepLogs. bunqueue_change_delaysent the wrong command in TCP mode. It usedMoveToDelayed, which only accepts active jobs, so changing the delay of a delayed job failed.- A 30-second long poll failed with “Command timeout”.
bunqueue_pull_jobandbunqueue_pull_job_batchaccept atimeoutMsof up to 30 s, the same as the connection’s command timeout. The MCP connection now waits 45 s, so an empty queue returnsjob: null. - HTTP handlers never processed jobs in TCP mode. Their workers always
ran on an embedded queue inside the MCP process. They now connect to the
remote broker, and a TCP-mode MCP server never opens a local database, even
with
DATA_PATHset. A broker the handler cannot reach is reported on stderr instead of crashing the server. - TCP-mode flows could go to an in-process engine. With
BUNQUEUE_EMBEDDED=1in the environment, the flow tools committed flows to a local engine instead of the broker. bunqueue_clean_queuewithoutstatedeleted waiting jobs. The tool promised completed and failed jobs, but the broker read the missing state aswaiting. It now cleans completed and then failed jobs, withlimitcapping the total, and never touches waiting, delayed or active jobs.- Wrong job counts.
bunqueue_count_jobscounted only queued jobs and now counts every state. A paused queue’s ready jobs were counted twice in embedded mode (bunqueue_get_job_counts,bunqueue_get_queue_stats). Counts now includewaiting-childrenin both modes. - Missing job state and progress.
bunqueue_get_jobandbunqueue_get_job_by_custom_idnow return the job’sstate(pulled jobs areactive, DLQ jobsfailed).bunqueue_get_progresssaid “not found” for any job that was not active; it now returns the stored progress of any existing job. - Prioritized jobs could not be listed.
bunqueue_get_jobsrejected theprioritizedstate, which holds every waiting job with a priority; it now also acceptswaiting-childrenandpaused. - Input validation. Invalid input reached the broker or was silently
dropped: empty queue and job names, fractional priorities, attempts and
rate or concurrency limits, unknown flow options, and ids with broken
Unicode that TCP mode stored mangled. These are now rejected before
anything reaches the queue, with the same bounds the broker applies.
Regression:
test/repro-mcp-input-bounds.test.ts.
Documentation
Section titled “Documentation”- Generated API reference pages are
noindex, followand out of the sitemap. 262 of the 383 sitemap URLs were near-identical TypeDoc pages, and Search Console reported most of the sitemap as not indexed, including hand-written guides such as the quickstart and installation pages. The generated pages stay browsable and linked from/reference/, and keep their specific titles, descriptions and self canonicals. Any existingrobots,googlebotorbingbotdirective on them is replaced by exactly onerobotstag, anddocs:validate-discoverynow fails if a current reference page is indexable. The sitemap goes from 383 to 121 URLs. - The home page quickstart runs as written in every language. Deno now
gets
--allow-readand unscoped--allow-sys, so the worker no longer stops at a permission prompt in a terminal. PHP and Go list their minimum toolchain versions. Elixir clones with--depth 1, says whichdeps/0to replace, and installs Hex beforemix deps.getso a fresh machine does not prompt. Both “See it work” steps say that the worker keeps waiting and that Ctrl+C stops it.
Thanks to 1 contributor!
Section titled “Thanks to 1 contributor!”← bunqueue v2.9.7 · Release on GitHub · Commits since v2.9.7 · Full changelog · All posts