Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Embedding lifecycle races can retain or recreate pipelines after disposal, while idle release can stall profile learning until an unrelated operation reloads the model.
3 open issues
What changed in this PR
Adds opt-in idle release of the local ONNX embedding model to reduce memory usage in long-running OpenCode hosts. The default remains disabled.
Changes:
- Adds
embeddingIdleTimeoutMsconfiguration and documentation. - Introduces pipeline disposal, idle scheduling, and final-client teardown.
- Adds tests for timeout behavior, in-flight embeddings, and shared-client ownership.
| File | Description |
|---|---|
tests/plugin-turso-refcount.test.ts |
Checks model release only after the final client lease closes. |
tests/fixtures/memory-injection-eligibility.mjs |
Adds disposal support to the embedding mock. |
tests/embedding-idle-dispose.test.ts |
Tests idle release, disabled timeout, and in-flight protection. |
tests/config.test.ts |
Tests timeout defaults and validation. |
src/services/embedding.ts |
Adds idle timers, pipeline disposal, and warmup generation tracking. |
src/services/client.ts |
Releases embeddings when the final Turso lease closes. |
src/config.ts |
Defines, validates, and documents the timeout option. |
README.md |
Documents idle release and subsequent model warmup. |
Review effort: Balanced
| this.generation++; | ||
| const pipe = this.pipe; | ||
| this.pipe = null; | ||
| this.isWarmedUp = false; |
Contributor
Author
There was a problem hiding this comment.
This is the existing cold-buffer path (buffered while embeddings are unavailable, drained on the next warm merge, cap 50); warming on demand here would reload the model at session idle, which is when profile learning runs, so it is out of scope for this PR.
hu-ja-ja
force-pushed
the
feat/embedding-idle-release
branch
from
October 30, 2026 08:47
6d03de3 to
b241b8d
Compare
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
Add an opt-in
embeddingIdleTimeoutMs: after that many milliseconds without anembed, the local ONNX pipeline is disposed and the next memory operation re-warms it
from the local Hugging Face cache. OpenCode v2 runs plugins inside a long-lived
serve --serviceprocess, so today the weights stay resident for days even when nomemory operation has run for hours — measured at +561 MB RSS with the default
model. Default is
0(disabled): residency, first-call latency and vectors areunchanged unless the user opts in.
Test plan
plugin in an OpenCode v2 service for four days with the option enabled;
49 instrumented warmups / 54 releases, +561 MB model cost, ~500 MB handed back
on release, 0 dispose errors (numbers below)
Windows-only and no issue is filed for tracking
Checklist
main(do not open PRs from commits already onmain) —rebased onto
0d96489(v2.37.0), ahead 1 / behind 0bun testpasses (or N/A) — 977 pass / 7 skip / 0 fail across 134 filesbun run typecheckpasses (or N/A)bun run checkpasses (format + lint + typecheck; or N/A)README.md(embedding paragraph + option table) andCONFIG_TEMPLATE;translated READMEs untouched
Behaviour
embed(), so it measures idle time rather thanuptime, and it is
unref'd so it can never hold the process open. Remote embeddingsnever arm it: there is no local model to release.
dispose()that arrives mid-flight runs as soon asthe running call drains, instead of pulling the pipeline from under it.
pipeline()finishes loadingafter a
dispose()ran, the new pipeline is disposed immediately instead of beinginstalled, so a release cannot be undone by a slow warmup.
LocalMemoryClient.close()releases the pipeline with the last Turso lease(
releasePluginTurso() === true).EmbeddingServicelives onglobalThis, so anunconditional dispose would drop the model for a sibling instance still serving.
safeLog(): a lazily created log directory can throw onENOSPC/EACCES, which must not fail the idle timer or
close().Measurement
Method: the plugin logs
process.memoryUsage().rssof the OpenCode service process attwo points — immediately before
pipeline()builds the model, and at the end of everyrelease — so the model cost is a within-process paired delta that unrelated allocations
cannot inflate.
Windows 11, Bun 1.4.3,
Xenova/nomic-embed-text-v1(fp32), four days of ordinary use(2026-10-27 → 2026-10-30),
embeddingIdleTimeoutMs: 600000:Peak observed RSS: 1001 MB. Re-warm takes 1.16 s median, 1.57 s p90, 3.1 s worst (first
use of the day). RSS drops by ~500 MB at release and returns to baseline by the next build.
Across the 86.8 h window the model was resident 50% of the time — the feature working,
and also why the default stays off: with a 10-minute timeout, 29 of 34 idle releases
were followed by another embed within 30 minutes, so short values mostly churn. No
dispose errors or races were logged across those four days.
Verification
bun test tests/embedding-idle-dispose.test.ts tests/config.test.ts tests/plugin-turso-refcount.test.ts— 40 pass, 0 fail.fails 1 of 3 idle-release tests; disposing on every
close()instead of the lastlease fails 1 of 2 refcount tests.
memory-portabilitytest timed out onceunder full-suite load and passes standalone on both runs; unrelated to this change.
Risks / open questions
behaviour rather than a documented contract (it did here: ≈500 MB back immediately,
more by the next load). macOS/Linux residency and re-warm time are unmeasured.
AI assistance
Developed with an AI coding agent (OpenCode, DeepSeek V4.1 Flash): implementation,
tests, the measurements above and this description. The numbers come from real runs on
the contributor's machine and the result was reviewed before submission.