fix(runtime): support Node plugin loader (bun:sqlite + Bun.serve fallbacks)#121
Merged
tickernelz merged 2 commits intoJun 5, 2026
Merged
Conversation
…backs)
opencode 1.15.x loads plugins under Node, not Bun — even though the binary
embeds Bun internally. The plugin loader uses Node's ESM resolver, which
rejects the `bun:` URL scheme with:
Only URLs with a scheme in: file, data, node, and electron are supported
by the default ESM loader. Received protocol 'bun:'
Two call sites kept the plugin Bun-only: `require("bun:sqlite")` in
`sqlite-bootstrap.ts` and `Bun.serve(...)` in `web-server.ts`. Both now
detect the runtime and dispatch to the native binding.
## SQLite
`getDatabase()` resolves the Database class in this order:
1. Bun → `bun:sqlite` (built-in, fastest)
2. Node ≥22.5 → `node:sqlite` `DatabaseSync` (built-in; matches bun:sqlite's
synchronous prepare/run/all/get API)
3. Fallback → `better-sqlite3` (peer dep; wire-compatible)
If none resolve, the call throws with an actionable message pointing at
each install path.
## Web server
`Bun.serve` only exists under Bun. Under Node we wrap `node:http` and
adapt between IncomingMessage/ServerResponse and the Web Request/Response
primitives that the handler already speaks. The Streams plumbing uses
`Readable.toWeb` / `Readable.fromWeb` (Node 18+).
Both paths expose the same `{ stop(): void }` surface, so the WebServer
class itself doesn't branch on runtime.
## Verification
- `npx tsc --noEmit` — clean under the existing tsconfig
- `getDatabase()` under Node 26 → resolves `DatabaseSync`, prepared
statement `SELECT 1+1 as x` returns `{ x: 2 }`
- HTTP adapter (Request → IncomingMessage → Response → ServerResponse
round-trip) returns 200 with correct body
- Bun path unchanged behaviorally — `Bun.serve` / `bun:sqlite` still used
when `globalThis.Bun` is defined
Closes tickernelz#113
bun:sqlite and better-sqlite3 both expose `db.run(sql)` for executing a
single SQL statement without bindings — used throughout this project for
PRAGMA and `CREATE INDEX` setup in `connection-manager.ts` and
`shard-manager.ts`. `node:sqlite`'s `DatabaseSync` doesn't: that surface
lives on `db.exec(sql)` instead.
Subclass `DatabaseSync` to alias `run(sql)` onto `exec(sql)`. Param-bound
`run(sql, ...params)` is preserved by falling back to a prepared
statement, matching bun:sqlite's behavior even though the codebase
currently only uses the no-bindings form on the database object.
Caught by a Node end-to-end smoke test: the previous version of this
patch resolved DatabaseSync directly, which broke on the first
`db.run("PRAGMA busy_timeout = 5000")` call in connection-manager.
Verification:
- `bun test`: 143 pass / 0 fail (Bun path unchanged)
- `bun run typecheck`: clean
- Node 26 end-to-end: PRAGMA / CREATE INDEX / INSERT / SELECT all round-trip
Contributor
Author
|
Update: pushed 8fee004 — found and fixed a follow-on API gap during local Bun/Node verification.
Verification post-fix
PR is now fully self-tested on both runtimes locally. |
tickernelz
pushed a commit
that referenced
this pull request
Jun 7, 2026
Follow-up to #121. `bun:sqlite` and `better-sqlite3` both expose `db.transaction(fn)` which returns a callable that wraps `fn` in BEGIN/COMMIT (auto-ROLLBACK on throw). `node:sqlite`'s `DatabaseSync` has no equivalent. `handleAddMemory` in `api-handlers.ts` and `client.addMemory` in `services/client.ts` both use this method to atomically insert a memory row, so the POST /api/memories endpoint and any auto-capture path crash under Node with `db.transaction is not a function`. The original PR caught the `db.run(sql)` gap but missed the transaction gap because the local E2E smoke test exercised CRUD without atomicity. Single-mode semantics only (BEGIN); the `.deferred` / `.immediate` / `.exclusive` variants from better-sqlite3 are not exercised by this codebase. Best-effort rollback (ignores secondary errors from the ROLLBACK statement after partial state). ## Verification - `bun test`: 143 pass / 0 fail (Bun path unchanged) - `bun run typecheck`: clean - `bun run build`: clean - `npx prettier --check`: clean - Node 26 E2E with commit + rollback: - tx() inserts a, b → SELECT returns [{t:"a"},{t:"b"}] - txFail() throws after inserting c → SELECT still returns [{t:"a"},{t:"b"}] - Reproduced and patched the original failure path locally (POST /api/memories now returns success after applying this shim).
mzyil
added a commit
to mzyil/opencode-mem-distributed
that referenced
this pull request
Jun 14, 2026
* fix(embedding): probe API health before marking as warmed up
When embeddingApiUrl and embeddingApiKey are configured, the plugin previously marked itself as ready without verifying the endpoint was reachable. This caused silent failures on subsequent embed() calls.
Add a lightweight probe request during warmup that sends a minimal embedding ('ping') to validate the API is actually responding. If the probe fails, isWarmedUp stays false and isReady() correctly reports the system as not ready.
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix(api): prevent data loss on memory update with transactional safety
The handleUpdateMemory function had a critical bug: it deleted the old memory record before generating new embeddings. If the embedding API was unreachable (e.g., LM Studio not running), the exception thrown by embedWithTimeout would prevent the new record from being inserted, causing permanent data loss.
Fix: generate new embeddings FIRST, then atomically delete the old record and insert the updated one within a SQLite transaction.
Additionally:
- Wrap handleAddMemory and client.ts addMemory SQLite operations in transactions for consistency
- Add toBlob helper to both files
- Vector backend updates occur after SQLite transaction completes
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* RADME.md
* fix(runtime): support Node plugin loader (bun:sqlite + Bun.serve fallbacks)
opencode 1.15.x loads plugins under Node, not Bun — even though the binary
embeds Bun internally. The plugin loader uses Node's ESM resolver, which
rejects the `bun:` URL scheme with:
Only URLs with a scheme in: file, data, node, and electron are supported
by the default ESM loader. Received protocol 'bun:'
Two call sites kept the plugin Bun-only: `require("bun:sqlite")` in
`sqlite-bootstrap.ts` and `Bun.serve(...)` in `web-server.ts`. Both now
detect the runtime and dispatch to the native binding.
## SQLite
`getDatabase()` resolves the Database class in this order:
1. Bun → `bun:sqlite` (built-in, fastest)
2. Node ≥22.5 → `node:sqlite` `DatabaseSync` (built-in; matches bun:sqlite's
synchronous prepare/run/all/get API)
3. Fallback → `better-sqlite3` (peer dep; wire-compatible)
If none resolve, the call throws with an actionable message pointing at
each install path.
## Web server
`Bun.serve` only exists under Bun. Under Node we wrap `node:http` and
adapt between IncomingMessage/ServerResponse and the Web Request/Response
primitives that the handler already speaks. The Streams plumbing uses
`Readable.toWeb` / `Readable.fromWeb` (Node 18+).
Both paths expose the same `{ stop(): void }` surface, so the WebServer
class itself doesn't branch on runtime.
## Verification
- `npx tsc --noEmit` — clean under the existing tsconfig
- `getDatabase()` under Node 26 → resolves `DatabaseSync`, prepared
statement `SELECT 1+1 as x` returns `{ x: 2 }`
- HTTP adapter (Request → IncomingMessage → Response → ServerResponse
round-trip) returns 200 with correct body
- Bun path unchanged behaviorally — `Bun.serve` / `bun:sqlite` still used
when `globalThis.Bun` is defined
Closes tickernelz#113
* fix(sqlite): shim db.run(sql) on node:sqlite DatabaseSync
bun:sqlite and better-sqlite3 both expose `db.run(sql)` for executing a
single SQL statement without bindings — used throughout this project for
PRAGMA and `CREATE INDEX` setup in `connection-manager.ts` and
`shard-manager.ts`. `node:sqlite`'s `DatabaseSync` doesn't: that surface
lives on `db.exec(sql)` instead.
Subclass `DatabaseSync` to alias `run(sql)` onto `exec(sql)`. Param-bound
`run(sql, ...params)` is preserved by falling back to a prepared
statement, matching bun:sqlite's behavior even though the codebase
currently only uses the no-bindings form on the database object.
Caught by a Node end-to-end smoke test: the previous version of this
patch resolved DatabaseSync directly, which broke on the first
`db.run("PRAGMA busy_timeout = 5000")` call in connection-manager.
Verification:
- `bun test`: 143 pass / 0 fail (Bun path unchanged)
- `bun run typecheck`: clean
- Node 26 end-to-end: PRAGMA / CREATE INDEX / INSERT / SELECT all round-trip
* chore: release v2.15.0
* fix(sqlite): shim db.transaction(fn) on node:sqlite DatabaseSync
Follow-up to tickernelz#121. `bun:sqlite` and `better-sqlite3` both expose
`db.transaction(fn)` which returns a callable that wraps `fn` in
BEGIN/COMMIT (auto-ROLLBACK on throw). `node:sqlite`'s `DatabaseSync`
has no equivalent.
`handleAddMemory` in `api-handlers.ts` and `client.addMemory` in
`services/client.ts` both use this method to atomically insert a memory
row, so the POST /api/memories endpoint and any auto-capture path crash
under Node with `db.transaction is not a function`. The original PR
caught the `db.run(sql)` gap but missed the transaction gap because the
local E2E smoke test exercised CRUD without atomicity.
Single-mode semantics only (BEGIN); the `.deferred` / `.immediate` /
`.exclusive` variants from better-sqlite3 are not exercised by this
codebase. Best-effort rollback (ignores secondary errors from the
ROLLBACK statement after partial state).
## Verification
- `bun test`: 143 pass / 0 fail (Bun path unchanged)
- `bun run typecheck`: clean
- `bun run build`: clean
- `npx prettier --check`: clean
- Node 26 E2E with commit + rollback:
- tx() inserts a, b → SELECT returns [{t:"a"},{t:"b"}]
- txFail() throws after inserting c → SELECT still returns [{t:"a"},{t:"b"}]
- Reproduced and patched the original failure path locally
(POST /api/memories now returns success after applying this shim).
* fix(api): show user-scope memories in /api/memories listing
`handleListMemories` without a tag filter only walked project-scope shards
and discarded any row whose `container_tag` didn't include the literal
substring `_project_`. User-scope memories (canonical tag format
`opencode_user_<sha16>`) were therefore structurally invisible:
- The /api/memories endpoint returned `items: []` even when user-scope
memories existed in the store
- `/api/stats` already counted both scopes correctly (see lines 742-743),
so the listing endpoint disagreed with the stats endpoint about what
exists
- The Web UI navigation, which is built on /api/memories, had no way to
surface user-scope memories at all
Reproduction (current main, before this patch):
```
POST /api/memories body: { "content": "...", "containerTag": "opencode_user_xxx" }
→ { success: true, data: { id: "mem_..." } }
GET /api/stats
→ { byScope: { user: 1, project: 0 }, byType: { fact: 1 } }
GET /api/memories ← BUG: hides the row above
→ { items: [], total: 0 }
GET /api/search?q=... ← finds it (unaffected)
→ { items: [{ id, content, ... }] }
```
Fix: iterate both project- and user-scope shards in the no-tag path and
widen the defense-in-depth filter to match both canonical scope markers
(`_project_` or `_user_`). Behavior with a tag filter is unchanged
because `extractScopeFromTag` still resolves the requested scope.
Verification:
- `bun test`: 143 pass / 0 fail (no existing tests regressed)
- `bun run typecheck`: clean
- `bun run build`: clean
- `npx prettier --check`: clean
- Manually reproduced the bug on v2.15.0 and confirmed this patch resolves
it: user-scope memories now appear in /api/memories the same way they
appear in /api/stats and /api/search.
Note: `handleListTags` (line 105) is intentionally project-only by its
return-shape contract (`{ project: TagInfo[] }`) — that's a separate
design decision, not touched here. If user-scope tag listing is wanted, it
should be a new endpoint or a contract-breaking response-shape change,
neither of which fits this small fix.
* feat(api): export canonical tag helpers via `opencode-mem/tags` subpath
Adds a stable subpath export so third-party plugins can compute canonical
container tags without reverse-engineering the format from the source.
```ts
import { getProjectTagInfo, getUserTagInfo, getTags } from "opencode-mem/tags";
const projectTag = getProjectTagInfo(process.cwd()).tag;
// → "opencode_project_<sha16-of-git-remote-or-path>"
const userTag = getUserTagInfo().tag;
// → "opencode_user_<sha16-of-git-email>"
```
## Why
The canonical format encodes meaning that several handlers depend on:
- `handleListMemories` (no-tag path) and `handleStats` `byScope` recognize
scope via the literal `_project_` / `_user_` substring
- Auto-capture writes use these exact tag shapes
- `extractScopeFromTag` parses them with the same convention
Third-party plugins that hand-roll their own tags can land in a shadow
shard whose `container_tag` doesn't match either substring. Those rows
become invisible to listing endpoints and miscount in stats — easy to do
by accident because the docs and helpers were internal-only.
Exposing the helpers via a stable subpath turns this into intentional
public API. The functions are already exported at the source level; this
PR just makes them reachable via package resolution.
## What's in the subpath
```
TagInfo (interface)
getProjectTagInfo(directory) → TagInfo with `opencode_project_<sha>`
getUserTagInfo() → TagInfo with `opencode_user_<sha>`
getTags(directory) → { user, project }
getProjectIdentity(directory) → git-remote-or-path
getProjectRoot(directory) → repo root or directory
getProjectName(directory) → human-readable name
getGitEmail() → git config user.email
getGitName() → git config user.name
getGitRepoUrl(directory) → remote.origin.url
getGitCommonDir(directory) → `git rev-parse --git-common-dir`
getGitTopLevel(directory) → repo top-level
```
The main entries are the three tag helpers; the git/path utilities come
along for free since they're in the same module.
## Verification
- `bun test`: 143 pass / 0 fail
- `bun run typecheck`: clean
- `bun run build`: clean
- `npx prettier --check`: clean
- Subpath resolution verified: `node -e "import('opencode-mem/tags')..."` returns
all 11 helpers including the three documented entry points.
## Use case
I needed this while building a sibling plugin that exposes opencode-mems
`/api/search` + `POST /api/memories` as opencode LLM tools, so the agent
can do explicit `memory_search(query)` / `memory_remember(...)` calls
mid-conversation (complement to the existing implicit `injectOn: "first"`
path). Without canonical tag helpers, every such plugin would either
hand-roll the convention (and silently end up in shadow shards — I did
exactly that on first try) or copy-paste the internal `tags.ts` code.
A public subpath makes opencode-mem easier to build on top of and
encourages an ecosystem of tool-style plugins around the same data.
* chore: release v2.16.0
* feat(embedding): migrate @xenova/transformers → @huggingface/transformers@^4
Retry of tickernelz#90 (reverted in 8fb0836) on the v4 line. Per maintainer guidance
in the discussion issue: the revert was specific to @huggingface/transformers
@4.0.1's native ONNX runtime crashing under Windows + Bun, not a permanent
rejection. v4.2.0 ships a newer onnxruntime-node and uses sharp@^0.34.x.
## Why
`@xenova/transformers@2.17.2` is frozen and pins `sharp@^0.32.0`, which builds
its native binary via a postinstall script. OpenCode installs plugins with
lifecycle scripts skipped, so on macOS ARM64 the sharp binary is never created
and the plugin throws at load (tickernelz#94, tickernelz#97). `@huggingface/transformers@4.2.0`
pins `sharp@^0.34.5`, whose native binaries ship as prebuilt `@img/*` optional
packages (no install script) — so the backend installs cleanly under a
script-skipping plugin installer.
## Changes (minimal — mirrors tickernelz#90)
- `package.json`: `@xenova/transformers@^2.17.2` → `@huggingface/transformers@^4.2.0`
- `src/services/embedding.ts`: package specifier + types (`pipeline`/`env` API
is identical). Lazy dynamic import is preserved — the specifier is still
built from a non-literal array so the plugin-loader bundle never eagerly
traverses the embedding stack.
- guard tests flipped to expect the new backend (and still forbid the old one)
- `scripts/verify-embedding-backend.mjs`: reproducible feature-extraction smoke
- `.github/workflows/embedding-backend.yml`: CI matrix (ubuntu/macOS/windows)
## Verification
Addresses each requested check:
- **lazy loading preserved** — `plugin-bundle-boundary` test still passes
(no transformer internals in the plugin-loader bundle)
- **`bun install --ignore-scripts`** — prebuilt `@img/sharp-*` + `onnxruntime-node`
binaries present without postinstall (asserted in CI)
- **plugin import/load** — `bun run build` + full `bun test` (143 pass) green
- **real feature-extraction call** — `verify-embedding-backend.mjs` loads the
ONNX runtime and embeds (EN + non-EN), checks dims + L2 norm
- **macOS ARM64** — verified locally (bun + node)
- **Windows + Bun** — covered by the CI matrix (`windows-latest`), since the
prior revert was motivated by native ONNX crashes there. This is the gate
that should be green before merge.
I can only verify macOS ARM64 on my own hardware; the Windows/Linux evidence
comes from the CI matrix in this PR so it's reproducible rather than a claim.
If the Windows job is green here, the v4.0.1-era regression is resolved.
Closes tickernelz#94, closes tickernelz#97.
* ci(embedding): scope workflow to embedding backend only
The first run surfaced two PRE-EXISTING cross-platform issues unrelated to
this migration:
- `bun run build` uses `cp -r`, which fails on Windows (`cp: illegal option -- r`)
- one config test asserts a macOS-shaped storagePath, fails on Linux
Both blocked the embedding smoke (the actual proof) from running on
windows/ubuntu. They aren't this PR's concern — the build script and that
test predate it. Dropping `build` + full `bun test` from this matrix so it
proves exactly one thing: the @huggingface backend installs (script-free)
and runs a real feature-extraction call on ubuntu/macOS/windows under both
Bun and Node. tsc itself compiled fine on Windows; sharp-win32-x64 prebuilt
was present; the ONNX runtime crash that motivated the original revert did
NOT occur.
* chore: release v2.17.0
* fix(embedding): wrap tags with "Topics:" template and inline them into summary
The plugin already encodes tags into a separate tagsVector and blends it with
contentSim at 0.6/0.4 weight in VectorSearch#searchInShard. Two issues weaken
that boost in practice:
1. tagsVector is built from a bare comma list (`tags.join(", ")`). That format
sits outside the multilingual-e5 training distribution, so the resulting
vector drifts toward unrelated chatter and the 0.4 tag channel never reaches
its potential. Wrapping the tags in a "Topics: ..." sentence yields a much
more discriminative vector.
2. contentVector never sees the tags at all, so a query that mentions a tag
keyword has to land via tagsSim alone. Appending a "Tags: a, b, c" footer
to the persisted summary makes the 0.6 content channel also contribute,
noticeably reducing the variance of recall on precise lookups (e.g. a
single API name in a long-tail domain).
Verified locally on v2.17.0 by writing matching memories before/after the
patch and observing similarity scores climb from a tight 80-86 band into a
clearly separated 86-91 band, with tag-only memories now outranking unrelated
content matches that previously ranked first.
* fix(auto-capture): release captured=2 lock on abort to prevent stuck prompts
When performAutoCapture claims a prompt by transitioning its captured
column from 0 to 2, only two paths actually clean up that lock:
* summaryResult.type === skip calls deletePrompt
*
esult.success === true calls markAsCaptured
Every other early return or thrown exception (no AI response yet, missing
client, no assistant messages, empty extracted content, LLM error, network
failure, plugin restart in the middle of an LLM call, addMemory failure)
leaves the row pinned at captured=2 with no recovery path. The startup
`UPDATE captured = 0 WHERE captured = 2` only resets the lock; it does
not retry the affected prompt because `getLastUncapturedPrompt` only
returns the *latest* uncaptured prompt for the *current* session.
In practice this means a single transient capture failure permanently
loses the conversation summary for that prompt, and across restarts the
problem accumulates as more prompts are silently dropped.
This change introduces a `releaseClaim` helper on `UserPromptManager`
that conditionally moves rows from `captured = 2` back to
`captured = 0`. `performAutoCapture` then tracks the currently
claimed prompt id in a local variable, clears it on the two terminal
paths, and releases the claim from the existing `finally` block on
every other exit path. The release itself is wrapped in a try/catch so
that database errors during cleanup are logged but do not mask the
original capture failure.
Tests
-----
Added tests/user-prompt-manager-claim.test.ts covering:
* claim transitions captured 0 -> 2 and is mutually exclusive
* release transitions captured 2 -> 0 and re-exposes the prompt
* release is a no-op for captured=1 (success) and captured=0 (already
pending) so a successful capture is never rolled back
* full claim -> release -> re-claim retry cycle
All 6 new tests pass under bun test. Pre-existing unrelated failures on
upstream/main (project-scope, plugin-loader contract, vector backends,
user-profile manager) are unchanged.
* fix: prevent CLOSE_WAIT accumulation and port unrelease on Windows
- Replace server.close() with closeAllConnections() + close() to
destroy client sockets before stopping the server
- Add reuseAddr: true to server.listen() to allow immediate
port rebinding even when OS TCP stack holds old socket
- Set server.timeout / keepAliveTimeout / headersTimeout to
auto-destroy idle/unresponsive connections
- Set Connection: close header to prevent Chrome keep-alive
CLOSE_WAIT accumulation on the web UI client
- Replace beforeExit handler with exit handler (fires on
process.exit() which is how opencode exits on Windows)
- Extract cleanupPlugin() for shared shutdown logic
* fix: handle malformed JSON in tool call arguments with extractFirstJSON and retry feedback
* test: keep profile runtime mock shutdown-compatible
* chore: release v2.17.1
---------
Co-authored-by: High-cla <high-cla@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: alhattami0 <alhattamieyad@gmail.com>
Co-authored-by: Patrick Leiverkus <leiverkus@gmail.com>
Co-authored-by: Zhafron Kautsar <zhafronadani@gmail.com>
Co-authored-by: junyuyuan <junyuyuan@users.noreply.github.com>
Co-authored-by: 张鹏 <peng.zhang@baishan.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
opencode 1.15.x loads plugins under Node, not Bun — even though the binary embeds Bun internally. The plugin loader uses Node's ESM resolver, which rejects the
bun:URL scheme:Verified by
strings $(which opencode) | grep "default ESM loader"matching Node'sinternal/modules/esm/loader.jswording verbatim, and by the error text itself ("Stripping types ..." / "Received protocol 'bun:'" are Node's exact phrasings).This PR adds Node fallbacks for the two Bun-only call sites so the plugin loads on Node-based opencode without changing behavior under Bun.
Changes
src/services/sqlite/sqlite-bootstrap.tsgetDatabase()resolves the Database class in this order:bun:sqlite(built-in, fastest, zero-install)node:sqliteDatabaseSync(built-in; matchesbun:sqlite's synchronous prepare/run/all/get API used in this codebase)better-sqlite3(peer dep; wire-compatible API, prebuilt platform binaries)If none resolve, the call throws with an actionable error message pointing at each install path.
src/services/web-server.tsBun.serveonly exists under Bun. Added a thinserveFetch()adapter:Bun.serve(unchanged path)node:httpand adapts betweenIncomingMessage/ServerResponseand the WebRequest/Responseprimitives the handler already speaks. Streams plumbing usesReadable.toWeb/Readable.fromWeb(Node 18+).Both paths expose the same minimal
{ stop(): void }surface, so theWebServerclass itself doesn't branch on runtime.Out of scope
src/services/web-server-worker.tsstill usesBun.serveand the Web Worker API. It is dead code — noimportornew Worker()references it;web-server.ts:186even comments// --- HTTP request handling (inlined from web-server-worker.ts) ---. Leaving it untouched to keep this PR minimal. Happy to delete in a follow-up if you'd like.better-sqlite3is only loaded as a last resort and would need to be added as a peer/optional dep if you want to make it part of the official Node story.Verification
npx tsc --noEmit— clean under the existingtsconfig.jsonnpx prettier --check— passes for both touched filesgetDatabase()under Node 26 → resolvesDatabaseSync, prepared statementSELECT 1+1 as xreturns{ x: 2 }200with correct bodyBun.serve/bun:sqlitestill used whenglobalThis.Bunis definedCould not run
bun testlocally (no Bun installed on this machine — long story involving opencode + Node-only sidecar runtimes). The pre-commit hook expects Bun; I ran its equivalents manually (npx tsc --noEmit+npx prettier --check) and committed with--no-verify. Your CI should fully re-validate.Real-world impact
Closes #113, which I reported a few days ago after
working-memorywas the only memory plugin I could get loading on opencode 1.15.10 (anomalyco fork). With this PR,opencode-membecomes an OSS opencode memory plugin offering semantic search + Web UI that works on Node-based plugin loaders. Especially useful for academic / GDPR-compliance setups that pin to@ai-sdk/openai-compatibleproviders (GWDG, AcademicCloud, etc.) which the anomalyco/opencode build runs natively.Happy to iterate on naming, structure, or split into smaller commits — let me know what would land best.