Known issues
Bunqueue 2.9.4 is now the installed/default target; it retains the 2.9.3 API and schemas described below. Flow safety gates now also apply to the SDK console and direct agent calls: payload replacement, retry, remove and unprocessed-child removal are blocked. Queue/Flow/Workflow bridges now support verified native TCP TLS and private CA configuration; see agent.md.
Verified against the current source (not speculative), each entry cites the exact file so you can confirm or fix it. None of these are catastrophic; the dashboard is fully usable. They're documented here because "professional docs" means being honest about the rough edges, not hiding them.
Bunqueue v2.9.x server-contract constraints
These constraints are in the upstream HTTP contract and cannot be made atomic by a browser client. The dashboard fails closed where it can and names the risk at the point of action:
- Every DLQ retry path is unavailable. The server exposes a read followed by a separate
POST /queues/:q/dlq/retry, but the POST has no atomic precondition for job generation/identity, state or topology. Pinning the server, re-reading an exact failed ID and seeing empty topology is therefore insufficient: that job can disappear and a different job can be recreated under the same ID before the POST. Manual row retry, Jobs bulk retry, queue-wide retry and Copilot retry all fail closed. - Individual DLQ removal is unavailable. Bunqueue 2.9's
Queue.removeDlqJob()durably deletes records, but its input is still only queue + job ID. If a custom ID is reused between observation and deletion, it can target a different generation, so the dashboard does not expose it. - Completed-job requeue is unavailable. The upstream
retryCompletedimplementation resets a completed job but does not reconstruct dependency registration or the ordering guarantees of its original flow. The dashboard never calls the single-ID or queue-wide retry-completed route. - DLQ retention is display-only. Saving a smaller
maxEntriescan immediately evacuate existing entries, whilemaxAgedrives destructive expiry; neither path has an atomic generation/topology check for what it removes. The dashboard shows both server values read-only and omitsmaxAge/maxEntriesfrom every save. Existing auto-retry can only be turned off, never enabled. - Other flow-destructive mutations are not topology-aware. Cancel, Discard, Drain, Clean, Obliterate and DLQ Purge can delete a job that another queue still depends on. Bunqueue exposes neither reverse-dependency inspection nor an atomic conditional mutation, so those paths also fail closed. Session-owned Benchmark cleanup is the only queue-clean exception.
- Cron creation is an upsert.
POST /cronshas no create-only/CAS precondition. The dashboard blocks names visible in the current list and runs a fresh fail-closed preflight immediately before POST, but two clients racing the same absent name can still replace one another. The form and confirmation call the command an upstream, last-writer-wins upsert and require operators to authorize that behavior for a globally unique name. - Rate-limit and concurrency policies are write-only over HTTP. Bunqueue's HTTP surface still exposes PUT/DELETE without matching reads, so the desired- state forms remain explicit replacements rather than editable cached values. Queue Control now complements them with live, target-pinned Bunqueue 2.9.3 Queue SDK readback for the global rate limit, concurrency, remaining TTL, and saturation; write receipts are never presented as server truth.
Resolving the remaining mutation items completely requires generation/state/ topology-conditional APIs, flow-aware retry reconstruction, and create-only cron semantics in Bunqueue itself.
MCP TCP worker ID (Bunqueue 2.9.4)
The public MCP register_worker tool can return success: true and worker ID "0" while the real broker stores a different ID. A heartbeat with the returned ID then reports success: false. Read the worker registry and match a unique name and queues before heartbeat/unregister; inspect the JSON success field, not only MCP isError. The managed browser regression verifies the real ID, heartbeat, dashboard display and removal. See MCP setup.
Documentation direct URLs (fixed in 0.0.45)
The theme's custom view-transition wrapper previously forwarded only the URL to VitePress's router and dropped its initialLoad option. A direct clean URL could return valid HTML but become a 404 during hydration. The wrapper now forwards all router arguments and skips transitions on initial load; real browser tests cover every generated page at desktop and mobile widths plus search and history.
Copilot browser cancellation (fixed in 0.0.45)
AI SDK 7.0.14 creates a telemetry completion promise even in the browser, where its non-Node early return did not attach a rejection handler. Stop during a pending tool confirmation therefore emitted an unhandled AbortError despite correctly cancelling the mutation. patches/ai@7.0.14.patch adds the same local promise handling used by the SDK's Node path. The package version is pinned and frozen installs apply the patch; Docker copies it before dependency installation. The real-browser regression keeps browser-error assertions enabled and proves that Stop leaves the queue unchanged. Recheck this regression before removing the patch when upgrading the SDK.
Adversarial audit pass (v0.0.32)
Every module was re-read against the invariants it assumes, each suspected defect was challenged by an independent reviewer before being accepted, and the 92 that survived were fixed with regression tests (170 → 314 tests). The full list is in the changelog. What matters for operators:
- Network-facing agent access fails closed. Non-loopback binds and reverse-proxied loopback binds require
AGENT_TOKEN; it gates every bridged/agent/*route, including database, logs, config, and status reads. Remote policy is selected by the bind,TRUST_PROXY, forwarding headers, a public request Host, or explicit non-loopback allowed hosts/origins. Only genuinely loopback access keeps zero-configuration reads. The direct:6800listener stays loopback-only and is never the public bridge. - Network-facing admin-API access fails closed. The all-in-one server uses the same remote-policy signals for
/api/*: withoutBUNQUEUE_TOKENit returns403, and with one configured every request needs that exact bearer. Enter it as the Server token in Settings. The header is forwarded unchanged, so a Bunqueue server usingAUTH_TOKENSmust accept the same value. Static deployments do not pass through this boundary and still need upstream/front- proxy authentication. - Wildcard Host policy is explicit. The Host allowlist is enforced on every route, so
BIND_ADDR=0.0.0.0must list every public/LAN name or address inAGENT_ALLOWED_HOSTS(origins inAGENT_ALLOWED_ORIGINScount too). It does not infer DHCP addresses, aliases, container service names, or Kubernetes pod IPs. - Proxied deployments: the
/apiand/agentOrigin gates compare the request's host, not its full origin, precisely so a TLS-terminating reverse proxy (browser sendshttps://…, the binary seeshttp://…) does not 403 every mutation while read-only GETs keep working. A proxy that preservesHostmust first admit that public name viaAGENT_ALLOWED_HOSTSorAGENT_ALLOWED_ORIGINS. A proxy that rewritesHostmust also admit the rewritten raw Host, overwriteX-Forwarded-Host, and setTRUST_PROXY=1; that header is ignored for Origin matching by default, because a direct caller could otherwise declare itself same-origin. Listing the exact public origin inAGENT_ALLOWED_ORIGINScan replace the forwarded value for Origin policy, but never bypasses validation of the raw Host. - Coverage floors are enforced on non-
.tsxcode and were raised to 88% lines / 89% functions, with additional per-module floors for lifecycle, authentication, configuration persistence and database execution. A JSX module enters the lcov denominator merely by being imported, so the aggregate tracked test scope rather than tested behaviour; the overall number is still reported byscripts/check-coverage.ts. React components remain largely uncovered by unit tests — that is a real gap, not a measurement artifact.
Recently fixed (kept here for history)
A security + gate pass resolved these, no longer present:
SQLite deadlines now kill the work. Database browsing, custom queries and Workflow storage reads run in bounded disposable processes. Timeouts and request aborts wait for the child to exit before releasing lifecycle leases; an IPC supervisor also terminates work after abrupt agent death.
Server configuration survives agent restarts. Successful changes are atomically saved to
AGENT_CONFIG_PATH(default.bunqueue-dashboard/config.json). Invalid saved state fails startup explicitly; restarting does not start the broker.Publishing waits for the complete validation gate. Release, npm, Docker and Pages depend on quality, all three Playwright browsers and native execution checks for the five release platforms. Release assets are the tested binaries.
AGENT_TOKENend-to-end. The browser agent client never sent the token, so a token-protected agent 401'd every control action, and the 401 popped the wrong (server) token prompt. The client now sends the agent token, theauth:requiredevent is scoped (server vs agent), and Settings has an Agent token field that remains in memory for the browser session. See agent.md. Tokens are no longer sourced fromVITE_*, where they would be visible in the public bundle.DNS-rebinding read exposure closed. The agent now enforces a Host-header allowlist (loopback +
AGENT_ALLOWED_HOSTS) in addition to the Origin gate, so a page whose DNS was rebound to loopback can no longer read/control/*or/db/*over Origin-less same-origin GETs. The standalone binary applies the same gate to/api,/agentand assets on loopback and network binds.Benchmark accounting is run-scoped. Worker runs first require an empty dedicated per-tab queue, track the exact ids returned by every producer batch, and only ACK/count those ids. If another producer races the preflight, its jobs are returned to waiting and the benchmark stops. The UI does not expose a drain-only mode for arbitrary queues.
Alert channel secrets no longer persisted.
alertsStorekeptwebhook/slacktargets (secret URLs) inlocalStorage; they're now memory-only.agent/andscripts/are typechecked by the build gate (tsconfig.agent.json), whereas the npm bin and agent code used to ship with no typecheck.
A performance + pagination pass resolved these, no longer present:
- Per-poll fan-outs collapsed. OverviewPro (was 8 req/poll), MetricsPro (was 22), DlqPro (was N+2) now issue 2 to 3 requests per poll via
GET /queues/summary; JobsPro is single-queue server-paginated (was up to 25jobs/listper poll). Classic Jobs refuses its all-queue mode above 100 queues rather than issuing up to 10,000 periodic requests; every permitted pool pins one target and is lifecycle-cancelled.usePolledDatais now self-scheduling (at most one fetch in flight, no pile-ups) and pauses while the tab is hidden, except the first fetch, which always runs (same foruseThroughputSeries's first sample): a page opened in a background tab used to sit on "Loading…" (and the sidebar on "connecting") until focused. - Every list is paginated, see the
Paginationcomponent in components.md. Server-paginated where the API supports it (queues, DLQ via offset/limit/total; jobs via offset/limit +hasNext), client-paginated for full-list endpoints (crons, webhooks, workers, activity). usePolledDatarace fixed with a generation guard (last-to-START wins).- MetricsPro/
Metricslatency now reads the real nested per-operation percentiles (push/pull/ack× p50/p95/p99) instead of always-0. - Uptime no longer rendered ~1000× too large (ms→s) on OverviewPro/MetricsPro.
- Topbar titles now cover all Control routes (no more "bunqueue · bunqueue").
bq.call()now throws on HTTP-200-with-{ok:false}(excepthealth()), so failed cancel/purge/retry surface as errors instead of false success.- Responsive: fluid root type (
clamp()), a mobile nav drawer + hamburger, responsive padding. See components.md.
A stability re-check resolved these, no longer present:
StatusBadgeno longer crashes on a missing/undefinedstatus.status.toLowerCase()had no guard; two nav-reachable callers (LogsPro.tsx,JobTimeline.tsx) passede.status/e.statewith no fallback, unlikeJobsPro/JobInspectorwhich already defaulted to'waiting'. Fixed at the root (StatusBadge.tsx) so all six callers are covered: empty/undefined status now renders as "unknown" instead of throwing.AreaChartno longer blanks on aNaN/Infinitypoint.finite()sanitizes each value to0before it reaches the max computation or the SVG path builder, so one bad point degrades to a dip instead of corrupting every series' path data.
UI/UX pass (this change-set)
A four-auditor UI/UX sweep (52 findings) was applied on top of the stability sweep. Highlights: theme-aware semantic status colors (text-success/warning/ danger, the dark-palette 400 shades failed WCAG AA on the light theme),Field now wires label→input (useId), focus-visible rings across the shell and kit, standardized {ok,text} green/red action feedback on every mutating control, destructive confirms name their target and counts (Clean had NO confirm), Enter submits the create forms, honest empty states when a filter, not the data, is empty, live={!error} on Metrics/Diagnostics, DLQ job IDs link to the Job Inspector, /usage and /workers graduated to Pro pages (/cron now serves CronManager; classics remain at *-classic), Settings buffers the server URL (was retargeting all polling per keystroke), and a new Database section: read-only SQLite inspector (agent-side readonly connection, tables, schema/indexes/DDL, sortable grid, query runner with history/EXPLAIN/CSV/JSON export). scripts/dev.ts now spawns services directly instead of via bun run wrappers, which did not forward SIGTERM and were the root cause of the recurring orphaned vite/agent processes.
Database inspector, standalone timeout fixed
- Compiled binaries embed the supervised SQLite reader. The standalone build includes
scripts/serve.ts,agent/dbReadWorker.tsand the backup worker. The executable re-executes itself to isolate each read, so/db/querykeeps the same enforceable 5-second deadline asbun start. Queries remain read-only, statement-allowlisted and capped at 500 rows in every distribution mode.
Stability sweep (adversarially verified, earlier change-set)
A multi-dimension bug hunt (every finding independently verified by refute / reproduce / impact passes before fixing) resolved the following, gate green, with regression tests where practical (test/format.test.ts, test/manager.test.ts):
- Live activity feed no longer misorders bursts under StrictMode. The
setEventsupdater inuseActivityStreammutated its captured batch via.reverse(), an impure updater React invokes twice in dev, flipping a multi-event flush back to the wrong order. The reverse now happens once, outside the updater. formatDurationcan no longer render "1m 60s" / "60.0s", the remainder is derived from a single up-front rounding (119,700 ms → "2m 0s").- JobInspector: a failed lookup now distinguishes 404 ("Job not found", URL param cleared so the deep-link effect can't silently re-load the previous job over the failure) from network/5xx errors (real message shown; a valid job is no longer reported as "removed" when the server is merely down).
- JobsPro / DlqPro / DlqControl stale-view race fixed (QueueControl's tagging pattern): after switching queue/filter/page, the previous view's rows can no longer stay rendered, with live action buttons, under the new selection, so Retry/Cancel can't fire against the wrong entity.
- JobDataEditor no longer wipes unsaved edits on every action-driven job reload, it re-seeds by content, not object identity. Flow jobs are read-only: v2.9.3 replaces the full payload and would otherwise erase the reserved parent/children metadata used by FlowReader.
- ServerControl shows an amber "agent unreachable" banner (and disables lifecycle buttons, freezes the uptime ticker) when the status poll fails after a successful one, it used to keep asserting "Running / healthy" with a live-ticking uptime for a dead agent.
- Agent orphan fix:
agent/index.tsnow handles SIGINT/SIGTERM and stops the managed bunqueue server before exiting (Ctrl-C onbun startused to leave it running, holding :6790 and the SQLite db).scripts/dev.tswaits the agent's full 30 s drain plus 5 s (was 10 s, before that 2 s) so in-flight operations and the SIGTERM→SIGKILL escalation can complete. - Agent log pipe flushes the final unterminated chunk, a crash cause written without a trailing newline used to vanish from Process Logs.
- Agent spawn-failure race: a
start()whose spawn throws while a stalestop()is finalizing now clearsproc/runningConfig(status no longer reports a dead pid + launch config for a stopped server). - ErrorBoundary resets on ANY navigation (
location.key, was pathname only), re-clicking the crashed section's nav item or navigating between/job?id=Xvariants now recovers instead of appearing permanently broken. - CopyButton: falls back to
execCommand('copy')on insecure (plain-HTTP) origins, the documented Docker deployment, and flashes a red ✕ on failure instead of silently doing nothing. - Theme flash fixed: an inline pre-paint script in
index.htmlapplies the persisted light theme before the bundle loads (was a dark→light flash on every visit). react-routerjoined thereact-vendorchunk (the manualChunks regex missed it, in React Router 7 it holds the whole router;react-router-domis a shim), so app-only deploys no longer re-download the router.- Standalone binary proxy fixed:
scripts/serve.tsnow stripscontent-encoding/content-length/transfer-encodingfrom proxied responses (Bun's fetch decompresses bodies but kept the headers, behind any gzip proxy every/apiresponse failed withERR_CONTENT_DECODING_FAILED), and missing/assets/*files 404 (matching the Docker image's Caddy) instead of returning index.html to a stale chunk import. strictPort: true: Vite now fails fast when :5273 is taken instead of silently serving on :5274 whilebun start's banner points at the stale instance.- docker.yml / pages.yml now run the full gate (lint + build + test) before publishing, a commit rejected by CI could previously still ship as
edge/ to the public Pages site.
Audit fix pass (earlier change-set)
A full-component adversarial audit fixed the following. Each was verified, then fixed with the gate (build + lint + bun test) green; the agent + store fixes ship with reproducing tests (test/agent-server.test.ts, test/manager.test.ts, test/sse.test.ts, test/s3store.test.ts).
- Control agent is no longer unauthenticated-RCE-by-design.
agent/now enforces an Origin allowlist and locked CORS (never*, ACAO is reflected only for allowed origins) and rejects any request carrying a disallowedOrigin(403) before it reaches theProcessManager. A malicious tab's cross-originPUT /control/config→POST /control/startcan no longer set + run a command. Non-browser callers (curl) still work. SetAGENT_TOKENfor an extra bearer-token gate on state-changing requests; configure allowed origins viaAGENT_ALLOWED_ORIGINS. Handler logic is factored intoagent/server.ts(unit-tested). See agent.md. - Agent stop/start race fixed. A
stop()awaiting an old process could orphan a process a concurrentstart()brought up (manager reported "stopped" while a server was still running).ProcessManagernow guards everyonExit/stop()mutation by a monotonic process token. ConfigFormscross-queue write fixed.StallForm/DlqConfigFormnowuseEffect(() => setC(config), [config])andQueueControlrenders them withkey={queue}, so switching queue no longer saves queue A's stall/DLQ config onto queue B. Save now surfaces errors inline (was silent + unhandled rejection).useActivityStreamconnection indicator fixed.connectednow flips true on any delivered frame (the handshake carriesdata.connectedwith noevent:line), so an idle-but-live queue no longer shows "Connecting…" forever; and the stream now auto-reconnects (2s backoff) after a clean end / server restart instead of going silently dead.OverviewProbanner reflects connection loss. After the first successful poll, a later failure now shows an amber "Connection lost, showing last known data / Stale" banner instead of a permanent green "Online" over frozen numbers. Recent Activity rows now show the realqueue/jobId(were all "unnamed").setRateLimitnow actually applies.api.setRateLimitsent{max,duration}but the server reads{limit}, so the classicQueueDetailrate-limit control silently no-op'd while showing "Saved". It now sends{limit}; the dead "Duration (ms)" input was removed.api.ts'srequest()now also throws on HTTP-200-{ok:false}(exceptstorage()/health()), matchingbq.call().- S3 secret no longer persisted.
s3Storeusespartializeto keepaccessKeyId/secretAccessKeyin memory only, they are no longer written tolocalStoragein plaintext. AddJobbulk-with-custom-ID now reports the real created count (new Set(ids).size) and caps/validatesCount(≤10000).DlqControlno longer fetches unuseddlqStats(whose failure blanked the whole page).DlqProkeeps thePaginationcontrol mounted when a page-scoped reason/search filter matches nothing (was a navigation trap), and labels its page-scoped sort honestly on multi-page queues.QueueDetailRecent Jobs now shows real Name (data.name) and Duration (startedAt/completedAt) instead of "unknown" or a placeholder dash.Workerssurfaces a "showing first 100 of N" hint when the list is truncated.ServerControlvalidates ports (1 to 65535, HTTP≠TCP) before restart.TopbarguardsdecodeURIComponent(malformed URL no longer crashes the shell).useThroughputSerieshas an in-flight guard (no overlapping polls).Webhooksenable/disable toggle has an accessible name.- App-wide
ErrorBoundary.src/components/ErrorBoundary.tsxwraps the whole shell, so a single render throw shows a recoverable fallback instead of blanking the entire app.
Classic-page correctness pass
- Storage and DLQ shapes now match the live API. The classic client reads
/storagefrom{ ok, data }, andDlqEntryuses the nested{ job, enteredAt, reason, error, attempts[] }shape. The classic S3 page no longer masks disk-full, and a non-empty classic DLQ renders instead of crashing or showing blank identifiers. - Classic timestamps and totals are accurate. Overview/Usage convert uptime milliseconds before formatting, Jobs uses
startedAt/completedAt, and Queues gets its header totals from the global dashboard summary rather than the current page. - Legacy Jobs and Logs no longer advertise unavailable data. Jobs reads an optional display name from job data, renders Cancel unavailable under the flow-safety policy, and refreshes the queue list every 30 seconds. Logs shows the SSE event type instead of a permanently unknown job name.
UX gaps
Fixed: the Alerts page is now routed atsrc/pages/Alerts.tsxis fully built but unreachable./alertswith a Monitoring nav item, and a client-side engine (src/lib/useAlertEngine.ts, mounted app-wide viaAlertEngine) evaluates the enabled rules against live metrics.
Design limitations (not bugs, how bunqueue OSS works)
- S3 operations require the local control agent.
/s3can now apply the whitelisted Bunqueue environment, inspect/list backups, create one on demand, and perform a stop-gated, snapshot-confirmed restore through the exact 2.9.3 CLI./s3-classicremains a read-only environment reference. Static hosting and arbitrary remote targets cannot run commands on a machine they do not manage. - Alerts are evaluated client-side, with real limits.
useAlertEnginenow evaluates the rules in the browser (in-app toast + optional desktop Notification on each fresh threshold crossing), but: (1) it only runs while a tab is open (even backgrounded), so it is not away-from-desk paging; the email/webhook/slack delivery channels still have no backend (bunqueue OSS has no alerting engine, so wire them into your own monitoring or hosted bunqueue Cloud); (2) thep99_latencymetric is global only, because bunqueue exposes latency percentiles keyed by TCP operation (push/pull/ack), not per queue, so a queue-scoped p99 rule evaluates the global max operation p99, not that queue's job latency. - Multiple pages cover overlapping ground on purpose (three DLQ pages, two cron pages,
-classicduplicates), this is the additive convention fromCLAUDE.md, not accidental drift. See pages.md.