Skip to content

feat(duckdb): migrate duckdbClient onto WorkerBus v2, delete v1 worker - #287

Merged
qnbs merged 13 commits into
mainfrom
feat/worker-v2-duckdb-migration
Jul 30, 2026
Merged

feat(duckdb): migrate duckdbClient onto WorkerBus v2, delete v1 worker#287
qnbs merged 13 commits into
mainfrom
feat/worker-v2-duckdb-migration

Conversation

@qnbs

@qnbs qnbs commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Second of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication (ADR-0014). Stacked on #286 (parity gate) β€” base branch is that PR's branch and will auto-retarget to main once it merges. Plan: .claude/plans/worker-generation-v1-vs-enumerated-fox.md.

  • services/duckdb/duckdbClient.ts's public API (init/query/exec/shutdown/terminate/setOpfsFallbackHandler) is unchanged β€” its 5 real consumers (useDuckDb.ts, duckdbAnalytics.ts, duckdbMigration.ts, ragVectorMigration.ts, telemetryService.ts) need no changes. Only the internals swapped from a raw new Worker(duckdbWorker.ts) to the shared WorkerBus v2 'duckdb' pool via a new ensureDuckDbPool() β€” mirrors ensureWebLlmPool()'s decoupling from enableWorkerBusV2 (analytics is core, not experimental infra; toggling that flag off must not silently break it).
  • The v2 worker returns raw values and throws on failure (v1 wrapped everything in {ok,rows,error}) β€” the adapter in duckdbClient.ts's send() translates at that one boundary instead of touching ~20 call sites across 5 files.
  • WorkerBus's default 2-retry is disabled per-call (retryPolicy: {maxRetries:0}) to avoid stacking under duckdbAnalytics.ts's existing withDuckDbRetry() app-level retry (up to 9 attempts otherwise).

Two things surfaced while wiring this up, both fixed here:

  • WorkerBus.shutdown() tears down all 4 pools with no way to scope to just 'duckdb' β€” added WorkerBus.terminatePool(poolId) so duckdbClient.terminate() doesn't kill in-flight inference/webllm/plugin work on the shared bus.
  • send() is async and awaits ensureDuckDbPool() before a task handle exists; a first draft attached the AbortSignal listener after that await, which can lose a same-tick abort β€” unlike v1's synchronous Promise-executor version, where the listener was always attached before send() returned. Fixed by attaching the listener synchronously up front and applying it once the handle is ready; added a regression test for exactly this timing.

workers/duckdbWorker.ts deleted. features/featureCatalog.ts's enableDuckDbAnalytics entry retargeted to workers/v2/duckdb.worker.ts.

Test plan

  • pnpm exec vitest run tests/unit/duckdbClient.test.ts tests/unit/duckdbWorkerHandler.test.ts packages/worker-bus/tests/workerBus.test.ts tests/unit/ragVectorMigration.test.ts tests/unit/duckdbMigration.test.ts tests/unit/duckdbAnalytics.test.ts tests/unit/services/ai/telemetryService.test.ts tests/unit/hooks/useDuckDb.test.ts β€” 128/128 passing (rewritten duckdbClient.test.ts + 2 new WorkerBus.terminatePool tests + all 5 higher-level consumer suites unaffected, since the public API didn't change)
  • pnpm run typecheck (exact CI command) β€” clean
  • pnpm run lint (Biome) β€” clean via pre-commit hook
  • CI quality gate green (full coverage run β€” not run locally per this repo's low-end-hardware CI-cloud-first policy)

πŸ€– Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved DuckDB analytics by moving DuckDB execution onto a shared worker-pool with pool-aware lifecycle control.
    • Added support to terminate a specific DuckDB pool without shutting down the rest of the service.
    • Enhanced automatic recovery when DuckDB isn’t initialized, with clearer user-facing failures.
  • Bug Fixes
    • Improved abort/cancellation behavior for in-flight DuckDB tasks and isolated subscriber errors.
    • Improved OPFS fallback handling with stage-based notifications and friendlier circuit-open messaging.
  • Documentation
    • Updated published test-count metrics in the README.

qnbs and others added 3 commits July 29, 2026 19:21
…ests

Neither workers/v2/duckdb.worker.ts's handlers nor
workers/v2/inference.worker.ts's handleInference were exported or
unit-tested against real logic (only webllm.worker.ts had that,
tests/unit/webllmWorkerHandler.test.ts) despite both already being
registered as live WorkerBus v2 pools with zero production callers β€”
docs/adr/0014-worker-generation-duplication.md's deferred migration
needs these to actually match v1 before any consumer cuts over.

Testing surfaced a real gap: initDuckDb()'s OPFS-unavailable catch
block silently swallowed the failure, unlike v1's out-of-band
OPFS_FALLBACK message that lets the UI warn users their analytics
won't persist. Fixed by threading ctx.emitProgress into initDuckDb()
and emitting an 'opfs-fallback' progress stage from the catch block β€”
reuses the existing progress channel WebLLM already uses for download
progress, no new message-protocol/schema change needed.

First PR of the ADR-0014 consolidation sprint (5 PRs total, see
.claude/plans/worker-generation-v1-vs-enumerated-fox.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…S-v3 format

CodeRabbit review on #286 found a real bug: initDuckDb()'s OPFS catch
block overwrote `connection` with the fallback connection without
closing the first (partially-attached) one, leaking a DuckDB
connection on every OPFS-attach failure. Fixed by tracking the OPFS
attempt in its own local variable and closing it in the catch path
before falling back. New regression test asserts close() is called.

Also collapsed several multi-line QNBS-v3 comments in
workers/v2/duckdb.worker.ts, workers/v2/inference.worker.ts, and both
new test files to the required single bracketed-line format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Second of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication
(ADR-0014). services/duckdb/duckdbClient.ts's public API (init/query/
exec/shutdown/terminate/setOpfsFallbackHandler) is unchanged β€” its 5
real consumers (useDuckDb.ts, duckdbAnalytics.ts, duckdbMigration.ts,
ragVectorMigration.ts, telemetryService.ts) need no changes. Only the
internals swapped from a raw `new Worker(duckdbWorker.ts)` to routing
through the shared WorkerBus v2 'duckdb' pool via a new
ensureDuckDbPool() (mirrors ensureWebLlmPool()'s decoupling from the
enableWorkerBusV2 flag β€” analytics is core, not experimental infra).

The v2 worker returns raw values and throws on failure, unlike v1's
{ok,rows,error} wrapper β€” the adapter in duckdbClient.ts's send()
translates at that one boundary instead of touching ~20 call sites
across 5 files. WorkerBus's default 2-retry is disabled per-call
(retryPolicy: {maxRetries:0}) to avoid stacking under
duckdbAnalytics.ts's existing withDuckDbRetry() app-level retry.

Two things surfaced while wiring this up, both fixed here:
- WorkerBus.shutdown() tears down all 4 pools with no way to scope to
  just 'duckdb' β€” added WorkerBus.terminatePool(poolId) so
  duckdbClient.terminate() doesn't kill in-flight inference/webllm/
  plugin work on the shared bus.
- send() is async and awaits ensureDuckDbPool() before a task handle
  exists; naively attaching the AbortSignal listener after that await
  (like a first draft did) can lose a same-tick abort, unlike v1's
  synchronous Promise-executor version. Fixed by attaching the
  listener synchronously up front and applying it once the handle is
  ready β€” regression-tested.

workers/duckdbWorker.ts deleted; features/featureCatalog.ts's
enableDuckDbAnalytics entry retargeted to workers/v2/duckdb.worker.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldscript-studio Ready Ready Preview Jul 30, 2026 12:15am

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0ab09916-3d23-4f72-ae68-1add074748f5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
πŸ“ Walkthrough

Walkthrough

DuckDB transport moved from a dedicated worker to WorkerBus v2. WorkerBus now supports pool detection, targeted termination, and task cancellation. DuckDB handlers support OPFS fallback progress and parameterized SQL, with expanded tests and updated catalog documentation.

Changes

DuckDB v2 migration and handler coverage

Layer / File(s) Summary
Worker pool lifecycle
packages/worker-bus/src/workerBus.ts, services/workerBusManager.ts, packages/worker-bus/tests/*, tests/unit/workerBusManager.test.ts
WorkerBus tracks task pools, exposes pool presence, cancels and terminates selected pools, and restores the DuckDB pool when needed.
DuckDB handler behavior
workers/v2/duckdb.worker.ts, tests/unit/duckdbWorkerHandler.test.ts
The v2 handler reports OPFS fallback progress, cleans up failed OPFS connections, supports parameterized SQL, and validates initialization, execution, and shutdown behavior.
DuckDB client bus integration
services/duckdb/duckdbClient.ts, tests/unit/duckdbClient.test.ts
DuckDB requests use WorkerBus tasks with cancellation, progress handling, error normalization, one-time initialization retry, latency reporting, and pool-scoped termination.
Supporting validation and catalog alignment
tests/unit/inferenceWorkerHandlerV2.test.ts, features/featureCatalog.ts, README.md
Inference mocks reset between tests, the catalog points to the v2 DuckDB worker, and README test metrics are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WorkerBus
  participant DuckDBWorker
  participant DuckDB
  Client->>WorkerBus: enqueue db.duckdb.query
  WorkerBus->>DuckDBWorker: dispatch query task
  DuckDBWorker->>DuckDB: run SQL with params
  DuckDB-->>DuckDBWorker: return rows
  DuckDBWorker-->>WorkerBus: return task result and progress
  WorkerBus-->>Client: return response and latency
Loading

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly summarizes the main change: moving duckdbClient to WorkerBus v2 and removing the v1 worker.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches πŸ’‘ 1
πŸ“ Generate docstrings πŸ’‘
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/worker-v2-duckdb-migration

Comment @coderabbitai help to get the list of available commands.

Base automatically changed from feat/worker-v2-parity-gate to main July 29, 2026 18:10
@codeant-ai

codeant-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: e90fa604
Scan Time: 2026-07-30 00:16:21 UTC

βœ… Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets βœ… PASSED 0 secrets found
Duplicate Code βœ… PASSED 0.0% duplicated
SAST βœ… PASSED No security issues
Bugs βœ… PASSED Rating S: No bugs
IAC βœ… PASSED No IAC issues

View Full Results

qnbs and others added 2 commits July 29, 2026 20:15
…ailure

CodeRabbit's second pass on #286 flagged this as an outside-diff-range
comment (not a resolvable inline thread) on bcc41c2's own fix: the
new `await opfsConnection?.close()` call could itself reject, which
would skip emitProgress('opfs-fallback', ...) and the fallback
newDb.connect(), and replace the original ATTACH error with the close
failure β€” turning a graceful degrade into a hard init failure.

Wrapped the cleanup in its own try/catch (best-effort, logged via
console.warn, never blocks the fallback path) so a failing close()
can't prevent the fallback connection or swallow the real error. New
regression test asserts both still happen when close() rejects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
tests/unit/inferenceWorkerHandlerV2.test.ts (1)

28-30: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Reset mock implementations between tests.

vi.clearAllMocks() clears call history but can preserve mockPipelineFactory’s implementation. Use vi.resetAllMocks() (or mockPipelineFactory.mockReset()) so future tests cannot inherit behavior from earlier cases.

Suggested change
  beforeEach(() => {
-   vi.clearAllMocks();
+   vi.resetAllMocks();
  });
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/inferenceWorkerHandlerV2.test.ts` around lines 28 - 30, Update the
beforeEach setup in inferenceWorkerHandlerV2 tests to reset mock
implementations, replacing vi.clearAllMocks() with vi.resetAllMocks() or
resetting mockPipelineFactory directly. Preserve the existing per-test mock
cleanup while ensuring implementations cannot leak between tests.

Source: Coding guidelines

services/duckdb/duckdbClient.ts (1)

54-63: πŸš€ Performance & Scalability | πŸ”΅ Trivial | ⚑ Quick win

Abort listener is never detached on completion.

{ once: true } only removes the listener after it fires. A caller reusing one AbortSignal across many queries (a common hook/thunk pattern) accumulates one listener per call for the signal's lifetime. Capture the removal and run it in a finally.

♻️ Sketch
-  signal?.addEventListener(
-    'abort',
-    () => {
-      abortedEarly = true;
-      handleRef?.cancel('Aborted');
-    },
-    { once: true },
-  );
+  // QNBS-v3: [Detached in the finally below so callers reusing one AbortSignal don't accumulate listeners.]
+  const onAbort = () => {
+    abortedEarly = true;
+    handleRef?.cancel('Aborted');
+  };
+  signal?.addEventListener('abort', onAbort, { once: true });

…and wrap the try/catch result mapping with finally { signal?.removeEventListener('abort', onAbort); }.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/duckdb/duckdbClient.ts` around lines 54 - 63, Update the abort
handling around abortedEarly and handleRef to define a named onAbort listener,
retain the existing cancellation behavior, and remove that listener in a finally
block after the query’s try/catch result mapping completes. Ensure cleanup
occurs for successful, failed, and aborted queries while preserving the current
abort semantics.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker-bus/src/workerBus.ts`:
- Around line 206-211: Update WorkerBus.terminatePool to cancel or reject all
tasks routed to poolId with POOL_TERMINATED before removing the pool, ensuring
awaiting callers settle; in services/workerBusManager.ts, update the DuckDB
initialization path to re-register the duckdb pool when _bus exists but that
pool is absent, rather than returning solely because the bus is non-null.

In `@services/duckdb/duckdbClient.ts`:
- Around line 71-86: The DuckDB worker can respawn without its connection
initialized, causing subsequent QUERY or EXEC tasks to fail. Update the worker
initialization flow around db.duckdb.init so every newly spawned worker
establishes its connection before handling tasks, or configure the pool to
remain warm instead of tearing down with minWorkers: 0; preserve existing task
dispatch behavior.

In `@workers/v2/duckdb.worker.ts`:
- Around line 96-110: Update handleQuery and handleExec to preserve the client
payload’s params instead of silently ignoring them. Use DuckDB’s
prepared-statement binding flow with the requested SQL and parameter values, or
explicitly reject requests containing params before executing; ensure both
handlers avoid passing unbound client SQL directly to connection.query.
- Around line 79-88: Update the OPFS fallback handling in initDuckDb so failures
from opfsConnection?.close() are ignored and cannot abort the catch block.
Ensure emitProgress('opfs-fallback', ...) and newDb.connect() still execute when
cleanup rejects, while preserving the existing error reporting.

---

Nitpick comments:
In `@services/duckdb/duckdbClient.ts`:
- Around line 54-63: Update the abort handling around abortedEarly and handleRef
to define a named onAbort listener, retain the existing cancellation behavior,
and remove that listener in a finally block after the query’s try/catch result
mapping completes. Ensure cleanup occurs for successful, failed, and aborted
queries while preserving the current abort semantics.

In `@tests/unit/inferenceWorkerHandlerV2.test.ts`:
- Around line 28-30: Update the beforeEach setup in inferenceWorkerHandlerV2
tests to reset mock implementations, replacing vi.clearAllMocks() with
vi.resetAllMocks() or resetting mockPipelineFactory directly. Preserve the
existing per-test mock cleanup while ensuring implementations cannot leak
between tests.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f28cfe7d-1a5d-4202-b94f-d551bf81eddb

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between ea879e4 and bc371b3.

πŸ“’ Files selected for processing (11)
  • features/featureCatalog.ts
  • packages/worker-bus/src/workerBus.ts
  • packages/worker-bus/tests/workerBus.test.ts
  • services/duckdb/duckdbClient.ts
  • services/workerBusManager.ts
  • tests/unit/duckdbClient.test.ts
  • tests/unit/duckdbWorkerHandler.test.ts
  • tests/unit/inferenceWorkerHandlerV2.test.ts
  • workers/duckdbWorker.ts
  • workers/v2/duckdb.worker.ts
  • workers/v2/inference.worker.ts
πŸ’€ Files with no reviewable changes (1)
  • workers/duckdbWorker.ts

Comment thread packages/worker-bus/src/workerBus.ts
Comment thread services/duckdb/duckdbClient.ts Outdated
Comment thread workers/v2/duckdb.worker.ts
Comment thread workers/v2/duckdb.worker.ts
…QL param binding

Three real findings from CodeRabbit's review of #287 (duckdbClient
migration onto WorkerBus v2), all confirmed against current code
before fixing:

1. CRITICAL β€” params silently dropped (workers/v2/duckdb.worker.ts).
   duckdbClient.send() enqueues {sql, params}, but handleQuery/
   handleExec only read req.sql and called connection.query(sql)
   directly, ignoring params entirely and leaving an unbound-query/
   SQL-injection surface flagged by static analysis. This was already
   live: services/ai/telemetryService.ts's writeToDuckDb() passes real
   bound params ([taskType, backend, modelId, latencyMs, success])
   that were being silently discarded. Fixed via DuckDB-WASM prepared
   statements (connection.prepare(sql) + stmt.query(...params) +
   stmt.close()) when params is non-empty; unchanged direct
   connection.query(sql) otherwise. Needed a narrow structural type
   (PreparableConnection) since tsgo doesn't resolve
   AsyncDuckDBConnection.prepare() through this package's nested
   `export *` chain, despite it being present in the package's own
   .d.ts β€” same class of tsgo/external-package gap CLAUDE.md already
   documents for the transformers.js path alias.

2. MAJOR β€” DuckDB pool worker losing its connection on respawn
   (services/duckdb/duckdbClient.ts). The duckdb pool's minWorkers:0
   lets its worker idle-terminate; a freshly spawned replacement
   worker's module state has no `connection` until INIT runs again on
   it, so the next QUERY/EXEC after a respawn failed with "DuckDB not
   initialized". Fixed by transparently re-sending INIT once and
   retrying the original call when send() sees that specific error
   message (bounded to one retry, INIT/SHUTDOWN excluded from the
   check to avoid any recursive loop).

3. MAJOR β€” terminatePool() had no lifecycle contract
   (packages/worker-bus/src/workerBus.ts +
   services/workerBusManager.ts). Removing a pool didn't settle tasks
   already routed to it β€” their result promises would await a RESULT
   message from a worker that no longer exists. Fixed by tracking
   task->pool assignment (new taskPools map, set/cleared around
   runTask's pool-acquire phase) and cancelling matching tasks before
   deletion. Separately, ensureDuckDbPool() assumed a non-null bus
   always has every pool, so any call after duckdbClient.terminate()
   would fail with NO_POOL forever; added WorkerBus.hasPool() and a
   re-registration path (duckDbPoolOptions()/reRegisterDuckDbPool(),
   shared with doInitWorkerBus() to avoid option drift between the two
   registration sites).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qnbs

qnbs commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
workers/v2/duckdb.worker.ts (1)

79-93: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Route the OPFS-close-failure warning through services/logger.ts instead of raw console.warn.

The best-effort try/catch fix itself is correct (matches the previously-flagged requirement that a rejecting close() must not block the fallback or replace the original error). However, line 84 logs via console.warn directly rather than the shared logger, diverging from the convention this same PR already follows in services/workerBusManager.ts (log.info/log.error).

♻️ Proposed fix
+import { log } from '../../services/logger';
...
       } catch (closeErr) {
-        console.warn('[duckdb.worker] Failed to close partial OPFS connection', closeErr);
+        log.warn('[duckdb.worker] Failed to close partial OPFS connection', closeErr);
       }

As per coding guidelines, "technical details may only be emitted through services/logger.ts using Biome-compliant warn or error."

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@workers/v2/duckdb.worker.ts` around lines 79 - 93, Replace the raw
console.warn in the OPFS close-error catch within the worker’s fallback flow
with the shared logger from services/logger.ts, using its Biome-compliant warn
or error method and preserving the existing message and closeErr details.

Source: Coding guidelines

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@workers/v2/duckdb.worker.ts`:
- Around line 79-93: Replace the raw console.warn in the OPFS close-error catch
within the worker’s fallback flow with the shared logger from
services/logger.ts, using its Biome-compliant warn or error method and
preserving the existing message and closeErr details.

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10cd09b6-f42b-471b-b59f-40b60c9de8d8

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between bc371b3 and 36c5f7c.

πŸ“’ Files selected for processing (9)
  • README.md
  • packages/worker-bus/src/workerBus.ts
  • packages/worker-bus/tests/workerBus.test.ts
  • services/duckdb/duckdbClient.ts
  • services/workerBusManager.ts
  • tests/unit/duckdbClient.test.ts
  • tests/unit/duckdbWorkerHandler.test.ts
  • tests/unit/workerBusManager.test.ts
  • workers/v2/duckdb.worker.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • services/duckdb/duckdbClient.ts
  • packages/worker-bus/src/workerBus.ts
  • tests/unit/duckdbWorkerHandler.test.ts

qnbs and others added 2 commits July 29, 2026 23:15
Squash-merging PR286 created a new commit on main with no shared
ancestry recognized against this stacked branch's pre-squash history,
causing an add/add and content conflict on the 2 files PR286 and
PR287 both touch. This branch's version is a strict superset (has
everything main has, plus PR287's own params-binding fix on top) β€”
resolved by keeping HEAD throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.

πŸ“’ Thoughts on this report? Let us know!

qnbs added a commit that referenced this pull request Jul 29, 2026
CodeRabbit review on #291 correctly flagged that the Known Technical
Debt line I edited (only updating the file path to
workers/v2/inference.worker.ts) still carried a pre-existing
recommendation to "restore @ts-expect-error" if the transformers.js
path alias breaks β€” in direct tension with this repo's own
suppression-ratchet policy documented a few sections above in the
same file. Reworded to point at fixing the alias/type declaration
directly instead, consistent with the structural-type workaround this
same sprint used for a different tsgo/external-package resolution gap
(workers/v2/duckdb.worker.ts's PreparableConnection, PR #287) rather
than reaching for a suppression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added 2 commits July 29, 2026 23:51
Matches the log.warn/log.error convention services/workerBusManager.ts
already uses in this PR β€” services/logger.ts is worker-safe (guarded
window access, lazy Tauri import), so raw console.warn was an
unnecessary inconsistency, not a technical constraint.

Addresses CodeRabbit nitpick on PR #287.
- duckdbClient.ts: cover the String(err) fallback branch in
  errorMessage(), the opfs-fallback message ?? default, and the
  reinit.ok === false path (INIT retry itself failing).
- workerBus.ts: add direct hasPool() coverage (true/false/after
  terminatePool) and a throwing-subscriber test that reaches
  emit()'s isolation catch via the circuit-breaker-open event path
  (the only two call sites of emit() in the class).

Addresses the Codecov "patch coverage" report on PR #287 (4 lines
missing coverage across these two files).
@qnbs

qnbs commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@CodeAnt-AI review

@qnbs

qnbs commented Jul 29, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

qnbs and others added 2 commits July 30, 2026 01:42
A caller that reuses one AbortSignal across many queries (a common
hook/thunk pattern) would otherwise accumulate one 'abort' listener per
call for the signal's whole lifetime, since {once:true} only self-removes
when the signal actually fires.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eWorkerHandlerV2 tests

clearAllMocks clears call history but not mockImplementation/mockResolvedValue,
so a test could inherit mockPipelineFactory's return value from an earlier
test instead of setting its own explicitly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/duckdbClient.test.ts (1)

343-351: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Reset the OPFS handler in afterEach instead of at the end of the test body.

If an assertion throws, line 350 never runs and the handler bleeds into later tests.

♻️ Proposed cleanup
-    expect(onFallback).toHaveBeenCalledWith('OPFS unavailable');
-    duckdbClient.setOpfsFallbackHandler(null);
+    expect(onFallback).toHaveBeenCalledWith('OPFS unavailable');

Add once per suite:

afterEach(() => {
  duckdbClient.setOpfsFallbackHandler(null);
});
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/duckdbClient.test.ts` around lines 343 - 351, Move the
setOpfsFallbackHandler(null) cleanup from the test body into a suite-level
afterEach hook so it runs even when assertions fail. Keep the test’s handler
setup and expectations unchanged, and remove the redundant end-of-test reset.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/inferenceWorkerHandlerV2.test.ts`:
- Around line 29-32: Replace the three-line rationale immediately before
vi.resetAllMocks() with a single-line comment using the exact format `//
QNBS-v3: [Grund / Impact / Kreativer Mehrwert]`, while preserving the rationale
that resetAllMocks clears implementations and prevents test state leakage.

---

Nitpick comments:
In `@tests/unit/duckdbClient.test.ts`:
- Around line 343-351: Move the setOpfsFallbackHandler(null) cleanup from the
test body into a suite-level afterEach hook so it runs even when assertions
fail. Keep the test’s handler setup and expectations unchanged, and remove the
redundant end-of-test reset.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abc8b240-bc74-46ae-ab89-e35a1a4c0a63

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 36c5f7c and 6e0c431.

πŸ“’ Files selected for processing (5)
  • packages/worker-bus/tests/workerBus.test.ts
  • services/duckdb/duckdbClient.ts
  • tests/unit/duckdbClient.test.ts
  • tests/unit/inferenceWorkerHandlerV2.test.ts
  • workers/v2/duckdb.worker.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/worker-bus/tests/workerBus.test.ts

Comment thread tests/unit/inferenceWorkerHandlerV2.test.ts Outdated
- duckdbClient.test.ts: move OPFS fallback-handler reset into a suite-level
  afterEach so it still runs when an assertion throws mid-test, instead of
  three end-of-test reset calls that could be skipped.
- inferenceWorkerHandlerV2.test.ts: collapse the QNBS-v3 rationale comment
  to the required single-line format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@qnbs

qnbs commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qnbs
qnbs merged commit 021cdf9 into main Jul 30, 2026
26 checks passed
@qnbs
qnbs deleted the feat/worker-v2-duckdb-migration branch July 30, 2026 00:41
qnbs added a commit that referenced this pull request Jul 30, 2026
* test(worker-bus): export v2 duckdb/inference handlers + parity gate tests

Neither workers/v2/duckdb.worker.ts's handlers nor
workers/v2/inference.worker.ts's handleInference were exported or
unit-tested against real logic (only webllm.worker.ts had that,
tests/unit/webllmWorkerHandler.test.ts) despite both already being
registered as live WorkerBus v2 pools with zero production callers β€”
docs/adr/0014-worker-generation-duplication.md's deferred migration
needs these to actually match v1 before any consumer cuts over.

Testing surfaced a real gap: initDuckDb()'s OPFS-unavailable catch
block silently swallowed the failure, unlike v1's out-of-band
OPFS_FALLBACK message that lets the UI warn users their analytics
won't persist. Fixed by threading ctx.emitProgress into initDuckDb()
and emitting an 'opfs-fallback' progress stage from the catch block β€”
reuses the existing progress channel WebLLM already uses for download
progress, no new message-protocol/schema change needed.

First PR of the ADR-0014 consolidation sprint (5 PRs total, see
.claude/plans/worker-generation-v1-vs-enumerated-fox.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): close leaked OPFS connection on ATTACH failure + QNBS-v3 format

CodeRabbit review on #286 found a real bug: initDuckDb()'s OPFS catch
block overwrote `connection` with the fallback connection without
closing the first (partially-attached) one, leaking a DuckDB
connection on every OPFS-attach failure. Fixed by tracking the OPFS
attempt in its own local variable and closing it in the catch path
before falling back. New regression test asserts close() is called.

Also collapsed several multi-line QNBS-v3 comments in
workers/v2/duckdb.worker.ts, workers/v2/inference.worker.ts, and both
new test files to the required single bracketed-line format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(duckdb): migrate duckdbClient onto WorkerBus v2, delete v1 worker

Second of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication
(ADR-0014). services/duckdb/duckdbClient.ts's public API (init/query/
exec/shutdown/terminate/setOpfsFallbackHandler) is unchanged β€” its 5
real consumers (useDuckDb.ts, duckdbAnalytics.ts, duckdbMigration.ts,
ragVectorMigration.ts, telemetryService.ts) need no changes. Only the
internals swapped from a raw `new Worker(duckdbWorker.ts)` to routing
through the shared WorkerBus v2 'duckdb' pool via a new
ensureDuckDbPool() (mirrors ensureWebLlmPool()'s decoupling from the
enableWorkerBusV2 flag β€” analytics is core, not experimental infra).

The v2 worker returns raw values and throws on failure, unlike v1's
{ok,rows,error} wrapper β€” the adapter in duckdbClient.ts's send()
translates at that one boundary instead of touching ~20 call sites
across 5 files. WorkerBus's default 2-retry is disabled per-call
(retryPolicy: {maxRetries:0}) to avoid stacking under
duckdbAnalytics.ts's existing withDuckDbRetry() app-level retry.

Two things surfaced while wiring this up, both fixed here:
- WorkerBus.shutdown() tears down all 4 pools with no way to scope to
  just 'duckdb' β€” added WorkerBus.terminatePool(poolId) so
  duckdbClient.terminate() doesn't kill in-flight inference/webllm/
  plugin work on the shared bus.
- send() is async and awaits ensureDuckDbPool() before a task handle
  exists; naively attaching the AbortSignal listener after that await
  (like a first draft did) can lose a same-tick abort, unlike v1's
  synchronous Promise-executor version. Fixed by attaching the
  listener synchronously up front and applying it once the handle is
  ready β€” regression-tested.

workers/duckdbWorker.ts deleted; features/featureCatalog.ts's
enableDuckDbAnalytics entry retargeted to workers/v2/duckdb.worker.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): make OPFS-connection cleanup best-effort on ATTACH failure

CodeRabbit's second pass on #286 flagged this as an outside-diff-range
comment (not a resolvable inline thread) on bcc41c2's own fix: the
new `await opfsConnection?.close()` call could itself reject, which
would skip emitProgress('opfs-fallback', ...) and the fallback
newDb.connect(), and replace the original ATTACH error with the close
failure β€” turning a graceful degrade into a hard init failure.

Wrapped the cleanup in its own try/catch (best-effort, logged via
console.warn, never blocks the fallback path) so a failing close()
can't prevent the fallback connection or swallow the real error. New
regression test asserts both still happen when close() rejects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(ai): migrate localEmbeddingService onto WorkerBus v2

Third of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication
(ADR-0014). services/ai/localEmbeddingService.ts's public API
(embedText/embedBatch/cosineSimilarity/clearEmbeddingCache) is
unchanged β€” its consumers (localRagService.ts, crossProjectIndexService.ts,
ragPromptAssembly.ts, ragVectorMigration.ts, proForgeMemoryBank.ts,
loraEvaluationService.ts, loraDatasetBuilder.ts, inferenceGateway.ts)
need no changes. Internals swapped from a dedicated
`new Worker(inference.worker.ts)` instance to the shared WorkerBus v2
'inference' pool via ensureInferencePool() (same decoupling-from-
enableWorkerBusV2 pattern as ensureWebLlmPool/ensureDuckDbPool).

v2's handler already throws on failure (matching embedText's existing
throw-on-failure contract), so no response-shape translation is
needed here β€” just swap the transport. Deleted ~70 lines of hand-rolled
health-check/restart/backoff logic (30s ping, exponential backoff up
to 5 attempts), superseded by WorkerPool's generic health check.

Capped the shared 'inference' pool's maxWorkers to 2 (was
MAX_WORKERS_INFERENCE=4) in workerBusManager.ts: each pool replica
independently loads its own transformers.js pipeline with no
cross-replica cache sharing, so 4 concurrent workers under a burst
(e.g. embedBatch's micro-batching) could mean 4x the model memory
footprint.

workers/inference.worker.ts (v1) is NOT deleted yet β€” localNlpService.ts
still uses it directly; removal is PR 4 of this sequence, after that
migration lands too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): pool-termination lifecycle, idle-respawn recovery, SQL param binding

Three real findings from CodeRabbit's review of #287 (duckdbClient
migration onto WorkerBus v2), all confirmed against current code
before fixing:

1. CRITICAL β€” params silently dropped (workers/v2/duckdb.worker.ts).
   duckdbClient.send() enqueues {sql, params}, but handleQuery/
   handleExec only read req.sql and called connection.query(sql)
   directly, ignoring params entirely and leaving an unbound-query/
   SQL-injection surface flagged by static analysis. This was already
   live: services/ai/telemetryService.ts's writeToDuckDb() passes real
   bound params ([taskType, backend, modelId, latencyMs, success])
   that were being silently discarded. Fixed via DuckDB-WASM prepared
   statements (connection.prepare(sql) + stmt.query(...params) +
   stmt.close()) when params is non-empty; unchanged direct
   connection.query(sql) otherwise. Needed a narrow structural type
   (PreparableConnection) since tsgo doesn't resolve
   AsyncDuckDBConnection.prepare() through this package's nested
   `export *` chain, despite it being present in the package's own
   .d.ts β€” same class of tsgo/external-package gap CLAUDE.md already
   documents for the transformers.js path alias.

2. MAJOR β€” DuckDB pool worker losing its connection on respawn
   (services/duckdb/duckdbClient.ts). The duckdb pool's minWorkers:0
   lets its worker idle-terminate; a freshly spawned replacement
   worker's module state has no `connection` until INIT runs again on
   it, so the next QUERY/EXEC after a respawn failed with "DuckDB not
   initialized". Fixed by transparently re-sending INIT once and
   retrying the original call when send() sees that specific error
   message (bounded to one retry, INIT/SHUTDOWN excluded from the
   check to avoid any recursive loop).

3. MAJOR β€” terminatePool() had no lifecycle contract
   (packages/worker-bus/src/workerBus.ts +
   services/workerBusManager.ts). Removing a pool didn't settle tasks
   already routed to it β€” their result promises would await a RESULT
   message from a worker that no longer exists. Fixed by tracking
   task->pool assignment (new taskPools map, set/cleared around
   runTask's pool-acquire phase) and cancelling matching tasks before
   deletion. Separately, ensureDuckDbPool() assumed a non-null bus
   always has every pool, so any call after duckdbClient.terminate()
   would fail with NO_POOL forever; added WorkerBus.hasPool() and a
   re-registration path (duckDbPoolOptions()/reRegisterDuckDbPool(),
   shared with doInitWorkerBus() to avoid option drift between the two
   registration sites).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): add the missing ensureInferencePool export that broke prod builds

Root cause of both Vercel deployment failures on this branch/PR chain:
services/ai/localEmbeddingService.ts imports and calls
ensureInferencePool() from workerBusManager.ts, but that function was
never actually added there β€” only ensureDuckDbPool() was. Vitest never
caught this because localEmbeddingService.test.ts mocks the entire
workerBusManager module (vi.mock(...) replaces it wholesale), so the
real file's missing export was invisible to that suite. tsgo's
typecheck also passed clean locally for reasons still unclear (worth
a follow-up look), but rolldown's stricter static bundling in the
actual `pnpm run build:edge` (Vercel's exact build command) correctly
failed hard with [MISSING_EXPORT] β€” reproduced locally with
`NODE_OPTIONS=--max-old-space-size=3072 pnpm run build:edge`, which is
what actually surfaced this.

Added ensureInferencePool(), mirroring ensureDuckDbPool()'s shape
(decoupled from enableWorkerBusV2, re-registers the pool if it was
removed via terminatePool() while the bus stays alive). Refactored
doInitWorkerBus()'s inline 'inference' pool registration to share the
same inferencePoolOptions() the new function uses, avoiding the same
kind of drift the duckdb pool options already guard against.

New tests in workerBusManager.test.ts import the *real* module (only
@domain/worker-bus is mocked) β€” this exact suite would have caught the
missing export immediately, unlike the fully-mocked consumer-side
test. Verified fixed via a full local `pnpm run build:edge`
reproduction of Vercel's exact build command (previously failed with
[MISSING_EXPORT], now builds clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): guard pool re-registration failures in workerBusManager

ensureDuckDbPool() and ensureInferencePool() could reject after _bus was
already live because their re-registration helpers awaited a lazy import and
called registerPool() without a catch, breaking the documented "null only if
init failed" contract. Wrap both paths so a failed re-add logs instead of
propagating, and add a maxWorkers:2 assertion to the inference re-registration
test so the pool cap regression would be caught.

Addresses CodeRabbit review comments on PR #288.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(worker-bus): close Codecov patch-coverage gap on PR #288

The prior CodeRabbit fix (8a63b7c) added try/catch guards in
ensureDuckDbPool()/ensureInferencePool() so a pool re-registration failure
logs instead of propagating, but no test exercised the catch path β€” Codecov
flagged 4 missing lines (83.33% patch coverage). Adds one test per function
that forces registerPool() to throw and asserts the live bus is still
returned and the failure is logged via services/logger.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(worker-bus): add QNBS-v3 rationale comments per CodeRabbit outside-diff-range review

Addresses CodeRabbit's 2 still-valid findings on PR #288: the logger mock
(why it's isolated from the real StructuredLogger) and the maxWorkers:2
regression assertion (why 2, not just what). The other 3 flagged spots
(reRegisterInferencePool, the inference registry.register block, and the
mockLogError.mockClear() line) mirror already-uncommented sibling code in
the same files (reRegisterDuckDbPool, the duckdb registry.register block,
6 other .mockClear() calls) β€” commenting only the new instance would be
less consistent, not more, so those are left as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
qnbs added a commit that referenced this pull request Jul 30, 2026
…nce worker (#290)

* test(worker-bus): export v2 duckdb/inference handlers + parity gate tests

Neither workers/v2/duckdb.worker.ts's handlers nor
workers/v2/inference.worker.ts's handleInference were exported or
unit-tested against real logic (only webllm.worker.ts had that,
tests/unit/webllmWorkerHandler.test.ts) despite both already being
registered as live WorkerBus v2 pools with zero production callers β€”
docs/adr/0014-worker-generation-duplication.md's deferred migration
needs these to actually match v1 before any consumer cuts over.

Testing surfaced a real gap: initDuckDb()'s OPFS-unavailable catch
block silently swallowed the failure, unlike v1's out-of-band
OPFS_FALLBACK message that lets the UI warn users their analytics
won't persist. Fixed by threading ctx.emitProgress into initDuckDb()
and emitting an 'opfs-fallback' progress stage from the catch block β€”
reuses the existing progress channel WebLLM already uses for download
progress, no new message-protocol/schema change needed.

First PR of the ADR-0014 consolidation sprint (5 PRs total, see
.claude/plans/worker-generation-v1-vs-enumerated-fox.md).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): close leaked OPFS connection on ATTACH failure + QNBS-v3 format

CodeRabbit review on #286 found a real bug: initDuckDb()'s OPFS catch
block overwrote `connection` with the fallback connection without
closing the first (partially-attached) one, leaking a DuckDB
connection on every OPFS-attach failure. Fixed by tracking the OPFS
attempt in its own local variable and closing it in the catch path
before falling back. New regression test asserts close() is called.

Also collapsed several multi-line QNBS-v3 comments in
workers/v2/duckdb.worker.ts, workers/v2/inference.worker.ts, and both
new test files to the required single bracketed-line format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(duckdb): migrate duckdbClient onto WorkerBus v2, delete v1 worker

Second of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication
(ADR-0014). services/duckdb/duckdbClient.ts's public API (init/query/
exec/shutdown/terminate/setOpfsFallbackHandler) is unchanged β€” its 5
real consumers (useDuckDb.ts, duckdbAnalytics.ts, duckdbMigration.ts,
ragVectorMigration.ts, telemetryService.ts) need no changes. Only the
internals swapped from a raw `new Worker(duckdbWorker.ts)` to routing
through the shared WorkerBus v2 'duckdb' pool via a new
ensureDuckDbPool() (mirrors ensureWebLlmPool()'s decoupling from the
enableWorkerBusV2 flag β€” analytics is core, not experimental infra).

The v2 worker returns raw values and throws on failure, unlike v1's
{ok,rows,error} wrapper β€” the adapter in duckdbClient.ts's send()
translates at that one boundary instead of touching ~20 call sites
across 5 files. WorkerBus's default 2-retry is disabled per-call
(retryPolicy: {maxRetries:0}) to avoid stacking under
duckdbAnalytics.ts's existing withDuckDbRetry() app-level retry.

Two things surfaced while wiring this up, both fixed here:
- WorkerBus.shutdown() tears down all 4 pools with no way to scope to
  just 'duckdb' β€” added WorkerBus.terminatePool(poolId) so
  duckdbClient.terminate() doesn't kill in-flight inference/webllm/
  plugin work on the shared bus.
- send() is async and awaits ensureDuckDbPool() before a task handle
  exists; naively attaching the AbortSignal listener after that await
  (like a first draft did) can lose a same-tick abort, unlike v1's
  synchronous Promise-executor version. Fixed by attaching the
  listener synchronously up front and applying it once the handle is
  ready β€” regression-tested.

workers/duckdbWorker.ts deleted; features/featureCatalog.ts's
enableDuckDbAnalytics entry retargeted to workers/v2/duckdb.worker.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): make OPFS-connection cleanup best-effort on ATTACH failure

CodeRabbit's second pass on #286 flagged this as an outside-diff-range
comment (not a resolvable inline thread) on bcc41c2's own fix: the
new `await opfsConnection?.close()` call could itself reject, which
would skip emitProgress('opfs-fallback', ...) and the fallback
newDb.connect(), and replace the original ATTACH error with the close
failure β€” turning a graceful degrade into a hard init failure.

Wrapped the cleanup in its own try/catch (best-effort, logged via
console.warn, never blocks the fallback path) so a failing close()
can't prevent the fallback connection or swallow the real error. New
regression test asserts both still happen when close() rejects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(ai): migrate localEmbeddingService onto WorkerBus v2

Third of 5 PRs consolidating the v1/WorkerBus-v2 worker duplication
(ADR-0014). services/ai/localEmbeddingService.ts's public API
(embedText/embedBatch/cosineSimilarity/clearEmbeddingCache) is
unchanged β€” its consumers (localRagService.ts, crossProjectIndexService.ts,
ragPromptAssembly.ts, ragVectorMigration.ts, proForgeMemoryBank.ts,
loraEvaluationService.ts, loraDatasetBuilder.ts, inferenceGateway.ts)
need no changes. Internals swapped from a dedicated
`new Worker(inference.worker.ts)` instance to the shared WorkerBus v2
'inference' pool via ensureInferencePool() (same decoupling-from-
enableWorkerBusV2 pattern as ensureWebLlmPool/ensureDuckDbPool).

v2's handler already throws on failure (matching embedText's existing
throw-on-failure contract), so no response-shape translation is
needed here β€” just swap the transport. Deleted ~70 lines of hand-rolled
health-check/restart/backoff logic (30s ping, exponential backoff up
to 5 attempts), superseded by WorkerPool's generic health check.

Capped the shared 'inference' pool's maxWorkers to 2 (was
MAX_WORKERS_INFERENCE=4) in workerBusManager.ts: each pool replica
independently loads its own transformers.js pipeline with no
cross-replica cache sharing, so 4 concurrent workers under a burst
(e.g. embedBatch's micro-batching) could mean 4x the model memory
footprint.

workers/inference.worker.ts (v1) is NOT deleted yet β€” localNlpService.ts
still uses it directly; removal is PR 4 of this sequence, after that
migration lands too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(worker-bus): pool-termination lifecycle, idle-respawn recovery, SQL param binding

Three real findings from CodeRabbit's review of #287 (duckdbClient
migration onto WorkerBus v2), all confirmed against current code
before fixing:

1. CRITICAL β€” params silently dropped (workers/v2/duckdb.worker.ts).
   duckdbClient.send() enqueues {sql, params}, but handleQuery/
   handleExec only read req.sql and called connection.query(sql)
   directly, ignoring params entirely and leaving an unbound-query/
   SQL-injection surface flagged by static analysis. This was already
   live: services/ai/telemetryService.ts's writeToDuckDb() passes real
   bound params ([taskType, backend, modelId, latencyMs, success])
   that were being silently discarded. Fixed via DuckDB-WASM prepared
   statements (connection.prepare(sql) + stmt.query(...params) +
   stmt.close()) when params is non-empty; unchanged direct
   connection.query(sql) otherwise. Needed a narrow structural type
   (PreparableConnection) since tsgo doesn't resolve
   AsyncDuckDBConnection.prepare() through this package's nested
   `export *` chain, despite it being present in the package's own
   .d.ts β€” same class of tsgo/external-package gap CLAUDE.md already
   documents for the transformers.js path alias.

2. MAJOR β€” DuckDB pool worker losing its connection on respawn
   (services/duckdb/duckdbClient.ts). The duckdb pool's minWorkers:0
   lets its worker idle-terminate; a freshly spawned replacement
   worker's module state has no `connection` until INIT runs again on
   it, so the next QUERY/EXEC after a respawn failed with "DuckDB not
   initialized". Fixed by transparently re-sending INIT once and
   retrying the original call when send() sees that specific error
   message (bounded to one retry, INIT/SHUTDOWN excluded from the
   check to avoid any recursive loop).

3. MAJOR β€” terminatePool() had no lifecycle contract
   (packages/worker-bus/src/workerBus.ts +
   services/workerBusManager.ts). Removing a pool didn't settle tasks
   already routed to it β€” their result promises would await a RESULT
   message from a worker that no longer exists. Fixed by tracking
   task->pool assignment (new taskPools map, set/cleared around
   runTask's pool-acquire phase) and cancelling matching tasks before
   deletion. Separately, ensureDuckDbPool() assumed a non-null bus
   always has every pool, so any call after duckdbClient.terminate()
   would fail with NO_POOL forever; added WorkerBus.hasPool() and a
   re-registration path (duckDbPoolOptions()/reRegisterDuckDbPool(),
   shared with doInitWorkerBus() to avoid option drift between the two
   registration sites).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): add the missing ensureInferencePool export that broke prod builds

Root cause of both Vercel deployment failures on this branch/PR chain:
services/ai/localEmbeddingService.ts imports and calls
ensureInferencePool() from workerBusManager.ts, but that function was
never actually added there β€” only ensureDuckDbPool() was. Vitest never
caught this because localEmbeddingService.test.ts mocks the entire
workerBusManager module (vi.mock(...) replaces it wholesale), so the
real file's missing export was invisible to that suite. tsgo's
typecheck also passed clean locally for reasons still unclear (worth
a follow-up look), but rolldown's stricter static bundling in the
actual `pnpm run build:edge` (Vercel's exact build command) correctly
failed hard with [MISSING_EXPORT] β€” reproduced locally with
`NODE_OPTIONS=--max-old-space-size=3072 pnpm run build:edge`, which is
what actually surfaced this.

Added ensureInferencePool(), mirroring ensureDuckDbPool()'s shape
(decoupled from enableWorkerBusV2, re-registers the pool if it was
removed via terminatePool() while the bus stays alive). Refactored
doInitWorkerBus()'s inline 'inference' pool registration to share the
same inferencePoolOptions() the new function uses, avoiding the same
kind of drift the duckdb pool options already guard against.

New tests in workerBusManager.test.ts import the *real* module (only
@domain/worker-bus is mocked) β€” this exact suite would have caught the
missing export immediately, unlike the fully-mocked consumer-side
test. Verified fixed via a full local `pnpm run build:edge`
reproduction of Vercel's exact build command (previously failed with
[MISSING_EXPORT], now builds clean).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(ai): migrate localNlpService onto WorkerBus v2, delete v1 inference worker

Fourth and final migration PR of the worker-generation consolidation
(ADR-0014). services/ai/localNlpService.ts's public API
(analyzeSentiment/summarizeText/classifyWritingTopic) is unchanged β€”
confirmed zero real production callers beyond its own tests and a
barrel re-export in services/ai/index.ts, so this is the
lowest-blast-radius of the three migrations. Internals swapped from a
dedicated `new Worker(inference.worker.ts)` instance to the shared
WorkerBus v2 'inference' pool via ensureInferencePool() (added in the
previous PR), routing to the 'inference.text' capability.

analyzeSentiment/summarizeText already had their own app-level
graceful-degrade behavior on failure (NEUTRAL sentiment / truncated
text) β€” that's preserved unchanged via try/catch around the new
throw-on-failure requestInference() call, replacing the old
ok:false-check.

With both localEmbeddingService.ts (previous PR) and this migrated,
workers/inference.worker.ts (v1) has no remaining callers β€” deleted,
along with its dedicated test (tests/unit/inferenceWorker.test.ts).
tests/unit/localNlpService.test.ts rewritten against a
workerBusManager mock; tests/unit/services/localNlpService.test.ts
deleted as a near-total duplicate of the rewritten suite (same module,
overlapping classifyWritingTopic/analyzeSentiment coverage, the
comprehensive file already had everything it did).

Verified via typecheck and a full local `pnpm run build:edge`
reproduction of Vercel's exact build command (the lesson from the
previous PR's missing-export incident) β€” both clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(ai): omit inferenceOptions key instead of passing undefined (exactOptionalPropertyTypes)

localNlpService.requestInference() spread inferenceOptions into the enqueue
payload via object-literal shorthand, so calls without a 4th arg (analyzeSentiment)
set the key to an explicit `undefined`. With exactOptionalPropertyTypes the
optional payload field only accepts the property being absent, not present-with-undefined.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(ai): close Codecov patch-coverage gap on PR #290

Codecov flagged 3 partial branches in localNlpService.ts (83.33% patch
coverage). Two were genuinely reachable and now have tests:
- ensureInferencePool() returning null (WorkerBus unavailable)
- the worker response omitting ":score" (scoreRaw undefined, ?? '0.5' fallback)

The third β€” labelRaw's `?? 'NEUTRAL'` fallback β€” was dead code:
String.split(':') never returns an empty array, so labelRaw (index 0) can
never actually be undefined; only TypeScript's noUncheckedIndexedAccess
couldn't see that. Replaced with a targeted `as string` assertion (compile-time
only, no runtime branch) instead of writing a contrived test for an
unreachable path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(test): require an enqueue call before asserting payload shape

Addresses a CodeRabbit finding on PR #290: the exactOptionalPropertyTypes
test used `requestCalls[0] ?? {}`, so it would still pass on an empty
{} object if analyzeSentiment failed to enqueue at all β€” a regression in
the enqueue path wouldn't be caught. Assert the call actually happened first.

The other finding on this review (missing QNBS-v3 comment on the two new
fallback tests) doesn't hold: none of the 8 tests in this describe block
have individual QNBS-v3 comments, only the file-level one β€” adding it to
just the 2 new tests would be less consistent, not more.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant