Skip to content
Get started
Get started
bunqueue v2.9.8
View Markdown
blog · release

bunqueue v2.9.8

Fixes 14 bugs. Streamable HTTP transport (BUNQUEUE_MCP_TRANSPORT=http).

bun add bunqueue@2.9.8
npm install bunqueue@2.9.8
TAG=2.9.8
docker run -p 6789:6789 -p 6790:6790 ghcr.io/egeominotti/bunqueue:$TAG
bun add bunqueue@latest
  • bunqueue_get_dlq returns entries instead of jobs. The output is { queue, reason?, offset, count, hasMore, entries }, with each entry’s job inside it; the top-level jobs array is gone. limit must be an integer from 1 to 100 (default 20).
  • Stricter MCP inputs, in both modes. bunqueue_set_rate_limit and bunqueue_set_concurrency need integers of at least 1. bunqueue_add_cron needs a non-empty name, an integer repeatEvery of at least 1, an integer priority within ±1,000,000, and a timezone that is a known IANA zone and comes with a schedule. Flow opts reject unknown keys, including deduplication, tags and durable, instead of dropping them. bunqueue_extend_lock caps duration at 24 hours, and lockTtl requires owner. bunqueue_move_to_delayed and bunqueue_change_delay cap delay at one year like the add tools; bunqueue_clean_queue limit, bunqueue_get_jobs start/end and bunqueue_get_flow depth/maxChildren must be integers (limit, depth and maxChildren at least 1); bunqueue_get_cron and bunqueue_delete_cron reject an empty name. A rejected call changes nothing.
  • bunqueue_wait_for_job fails with “Job not found” for an unknown id instead of waiting out the timeout, in both modes.

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 on http://127.0.0.1:6791/mcp by default (BUNQUEUE_MCP_HTTP_HOST, _PORT, _PATH) and accepts the bearer tokens in BUNQUEUE_MCP_HTTP_TOKEN, compared in constant time; on a non-loopback address it refuses to start without one. Host and Origin are checked against the bound address and BUNQUEUE_MCP_HTTP_ALLOWED_HOSTS / _ALLOWED_ORIGINS to 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_execution and bunqueue_signal_workflow read the workflow Engine’s database and deliver human-in-the-loop signals through the same first-writer-wins transaction as engine.signal(); the application’s Engine runs the resumed step. Signals need the Engine connected over TCP with its own dataPath, 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_QUEUE names a non-default Engine queue.
  • Job options. bunqueue_add_job and each bunqueue_add_jobs_bulk item accept backoff (a number, or { type, delay, maxDelay }), timeout, jobId, deduplication ({ id, ttl, extend, replace }), removeOnComplete, removeOnFail, durable, lifo, tags and stallTimeout, next to priority, delay and attempts (new on bulk items). The flow tools accept the same opts except deduplication, tags and durable. bunqueue_get_job and job listings return the options a job carries.
  • Lock tokens. bunqueue_pull_job and bunqueue_pull_job_batch take owner and lockTtl; each job then comes back with a token that bunqueue_ack_job, bunqueue_fail_job and bunqueue_job_heartbeat accept (tokens on the batch tools). A heartbeat with the token renews the lock, and bunqueue_fail_job takes unrecoverable to go straight to the DLQ.
  • DLQ failure reasons and statistics. bunqueue_get_dlq returns each entry’s reason, last error, recent attempt history, retry count and retry and expiry times, filters by reason and pages with offset. The new bunqueue_get_dlq_stats returns the total, the entries per reason, the entries due for automatic retry, the expired entries and the oldest and newest entry. The bunqueue_debug_queue and bunqueue_incident_response prompts use both instead of a short job list.
  • Queue limits. The new bunqueue_get_queue_limits reads 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_limit takes duration (the window in ms, default 1000), so limits per minute or per hour are possible.
  • Cron options. bunqueue_add_cron takes timezone, jobName, priority, maxLimit, immediately, skipIfNoWorker, preventOverlap, skipMissedOnRestart, deduplication and options for the jobs it adds (attempts, backoff, timeout, delay, stallTimeout, removeOnComplete, removeOnFail). Add, list and get return jobName, priority, timezone and maxLimit; 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 as queues,dlq exposes only those groups; dynamic starts the agent from a catalog and loads groups on demand (bunqueue_enable_toolsets, with bunqueue_call_tool for 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 in scripts/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, bulk retry_completed), plus signal_workflow when 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 in confirm. Previews count paused jobs, and purge_dlq states 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 powers bunqueue_find_tools and 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.
  • A signal racing a waitFor timeout 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 wrote failed without checking again. A signal recorded in between from another connection (a second application process, or the MCP server) was accepted with resumed: true, yet the run failed by timeout and was compensated. The expiry is now one IMMEDIATE transaction 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:timeout is 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.

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_job returned 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 return success: false.
  • bunqueue_retry_dlq with a jobId retried 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_job reported 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_webhooks was always empty, bunqueue_remove_webhook sent the wrong field, and registered workers showed no counters.
  • Job logs, memory statistics and Prometheus metrics were empty in TCP mode, bunqueue_get_stats included protocol fields, and bunqueue_clear_job_logs ignored keepLogs.
  • bunqueue_change_delay sent the wrong command in TCP mode. It used MoveToDelayed, 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_job and bunqueue_pull_job_batch accept a timeoutMs of up to 30 s, the same as the connection’s command timeout. The MCP connection now waits 45 s, so an empty queue returns job: 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_PATH set. 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=1 in the environment, the flow tools committed flows to a local engine instead of the broker.
  • bunqueue_clean_queue without state deleted waiting jobs. The tool promised completed and failed jobs, but the broker read the missing state as waiting. It now cleans completed and then failed jobs, with limit capping the total, and never touches waiting, delayed or active jobs.
  • Wrong job counts. bunqueue_count_jobs counted 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 include waiting-children in both modes.
  • Missing job state and progress. bunqueue_get_job and bunqueue_get_job_by_custom_id now return the job’s state (pulled jobs are active, DLQ jobs failed). bunqueue_get_progress said “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_jobs rejected the prioritized state, which holds every waiting job with a priority; it now also accepts waiting-children and paused.
  • 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.
  • Generated API reference pages are noindex, follow and 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 existing robots, googlebot or bingbot directive on them is replaced by exactly one robots tag, and docs:validate-discovery now 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-read and 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 which deps/0 to replace, and installs Hex before mix deps.get so a fresh machine does not prompt. Both “See it work” steps say that the worker keeps waiting and that Ctrl+C stops it.

← bunqueue v2.9.7 · Release on GitHub · Commits since v2.9.7 · Full changelog · All posts