Skip to content

Commit ede07cf

Browse files
committed
release: prepare native memory 0.6.0
1 parent 2495766 commit ede07cf

42 files changed

Lines changed: 4318 additions & 190 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "opencode-memory"
3-
version = "0.6.0-beta.2"
3+
version = "0.6.0"
44
edition = "2024"
55
license = "MIT"
66
rust-version = "1.97"

README.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Add the plugin to `opencode.json` or `opencode.jsonc`:
2424

2525
```json
2626
{
27-
"plugin": ["@nguyenthdat/opencode-memory@0.6.0-beta.2"]
27+
"plugin": ["@nguyenthdat/opencode-memory@0.6.0"]
2828
}
2929
```
3030

@@ -33,7 +33,7 @@ The package also includes an OpenCode TUI companion. Enable it in `tui.json`:
3333
```json
3434
{
3535
"$schema": "https://opencode.ai/tui.json",
36-
"plugin": ["@nguyenthdat/opencode-memory@0.6.0-beta.2"]
36+
"plugin": ["@nguyenthdat/opencode-memory@0.6.0"]
3737
}
3838
```
3939

@@ -86,6 +86,29 @@ The plugin automatically registers its packaged `rules/native-memory.md` as an O
8686
| `memory_status` | Health-check the plugin and inspect backend, model, and schema status |
8787
| `memory_doctor` | Run shallow or deep integrity checks |
8888
| `memory_purge` | Confirm and delete the complete project store |
89+
| `memory_model_profiles` | List stable, preview, and unsupported embedding model profiles |
90+
| `memory_model_switch` | Run a non-mutating model switch preflight |
91+
92+
### Embedding Profiles
93+
94+
The daemon owns an immutable profile catalog. The current selectable default is
95+
`qwen3-text-4b-q4`, which preserves the existing Qwen3 4B GGUF configuration.
96+
The catalog also describes popular presets such as `qwen3-text-0.6b-q8`,
97+
`qwen3-text-8b-q4`, `bge-m3`, and `nomic-embed-text-v1.5`, plus experimental
98+
Qwen3-VL 2B/8B entries. Preview and multimodal entries are visible but not
99+
selectable until their artifact, runtime, quality, and memory gates pass.
100+
101+
Use the safe phase-1 commands:
102+
103+
```text
104+
/memory model profiles
105+
/memory model switch qwen3-text-0.6b-q8 --dry-run
106+
/memory model switch qwen3-vl-embedding-8b --dry-run
107+
```
108+
109+
The current release performs preflight only. It never changes an existing
110+
project's model or mixes incompatible vectors. Durable collection-generation
111+
migration, cancellation, resume, and rollback are a later phase.
89112

90113
The 15 stable taxonomy values are `task_attempt`, `tool_call`, `session_summary`, `architecture_fact`, `codebase_fact`, `user_fact`, `fix_pattern`, `code_template`, `tool_heuristic`, `code_style`, `library_pref`, `workflow_pref`, `decision`, `team_convention`, and `project_standard`.
91114

@@ -137,6 +160,8 @@ Changing model identity or vector-affecting preprocessing requires rebuilding th
137160
| `OPENCODE_MEMORY_AUTO_CAPTURE` | Evaluate compaction candidates through the capture gate; default `true` |
138161
| `OPENCODE_MEMORY_AUTO_INDEX_DOCUMENTS` | Incrementally index non-ignored project documents; default `true` |
139162
| `OPENCODE_MEMORY_DOCUMENT_INDEX_DEBOUNCE_MS` | File-watcher re-index debounce; default `750` |
163+
| `OPENCODE_MEMORY_AUTO_OPTIMIZE` | Coalesced zvec compaction after writes/index drift; default `true` |
164+
| `OPENCODE_MEMORY_OPTIMIZE_DEBOUNCE_MS` | Maintenance debounce; default `5000` |
140165
| `OPENCODE_MEMORY_SHARED_SYNC` | Synchronize `.opencode/memory/**/*.md`; default `true` |
141166
| `OPENCODE_MEMORY_FEEDBACK_TRACKING` | Track retrieval feedback; default `true` |
142167
| `OPENCODE_MEMORY_MIN_SCORE` | Default calibrated search threshold; default `0.42` |
@@ -170,6 +195,14 @@ Shared Markdown is treated as untrusted data: paths are contained under `.openco
170195

171196
Automatic document indexing scans the project at startup and after debounced document watcher events. It respects `.gitignore`, `.ignore`, `.git/info/exclude`, global Git ignores, hidden directories, and the 32 MiB per-file limit; `.opencode/memory/` remains managed exclusively by repository-memory sync. A private derived manifest at `document-index.json` tracks source hashes and chunk ownership, so unchanged files skip extraction/embedding, changed files replace their previous chunks, deleted files are removed without tombstones, and a malformed file retains its last valid index. Set `OPENCODE_MEMORY_AUTO_INDEX_DOCUMENTS=false` to disable automatic synchronization, or call `memory_index_documents` for an explicit/forced run.
172197

198+
Automatic maintenance coalesces index/shared-memory/capture writes and runs
199+
`optimize` after a short debounce when zvec index completeness drops below
200+
98% or pending journals are observed. `index_documents`, `sync_shared`, and
201+
`optimize` are deterministic maintenance operations; if their response is
202+
lost after dispatch, the plugin retries the same operation once and never
203+
replays arbitrary mutations. Disable this behavior with
204+
`OPENCODE_MEMORY_AUTO_OPTIMIZE=false`.
205+
173206
## Architecture
174207

175208
```text

build.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,16 @@ fn main() {
33
.compile_protos(&["schema/opencode/memory/v1/memory.proto"], &["schema"])
44
.expect("compile Protobuf memory protocol schema");
55

6+
prost_build::Config::new()
7+
.compile_protos(
8+
&["schema/opencode/memory/model/v1/model.proto"],
9+
&["schema"],
10+
)
11+
.expect("compile Protobuf model protocol schema");
12+
613
let mut daemon = prost_build::Config::new();
714
daemon.extern_path(".opencode.memory.v1", "crate::memory_proto");
15+
daemon.extern_path(".opencode.memory.model.v1", "crate::model_proto");
816
daemon
917
.compile_protos(
1018
&["schema/opencode/memory/daemon/v1/daemon.proto"],
@@ -23,6 +31,7 @@ fn main() {
2331
_ => {}
2432
}
2533
println!("cargo:rerun-if-changed=schema/opencode/memory/v1/memory.proto");
34+
println!("cargo:rerun-if-changed=schema/opencode/memory/model/v1/model.proto");
2635
println!("cargo:rerun-if-changed=schema/opencode/memory/daemon/v1/daemon.proto");
2736
println!("cargo:rerun-if-changed=build.rs");
2837
}

bun.lock

Lines changed: 12 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/embedding-engine-phase1.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Embedding Engine Phase 1
2+
3+
Status: Implemented locally
4+
5+
## Decision
6+
7+
Pressure: embedding inference was serialized around complete project calls,
8+
including lexical work, zvec writes, ranking, and state fsync. Bulk shared-memory
9+
and document transactions also submitted one zvec upsert call per record. Model
10+
selection remained a set of raw environment fields without a discoverable,
11+
daemon-owned profile catalog.
12+
13+
Decision: keep llama.cpp inference globally serialized, but move the lock to the
14+
actual query/passage embedding calls. Batch zvec upserts and shared-memory
15+
commits without weakening the durable pending journal. Add an immutable catalog
16+
of stable, preview, and unsupported profiles, and expose profile listing plus a
17+
non-mutating switch preflight.
18+
19+
Language form: Rust value types and exhaustive Protobuf enum additions for the
20+
closed profile-control methods; one shared TypeScript model-control facade for
21+
the OpenCode tools.
22+
23+
Ownership: the daemon owns profile support decisions and preflight. Project
24+
actors remain the only storage writers. `MemoryEngine` owns its collection and
25+
embedder, while the daemon registry injects the shared inference lock. The
26+
plugin never constructs model repository configurations.
27+
28+
Alternatives: changing model environment variables or rebuilding `zvec/` in
29+
place was rejected because incompatible vectors could mix or make an existing
30+
project unreadable. A public mutating switch was deferred because collection
31+
generations, an active pointer, a durable switch journal, mutation freeze,
32+
resume, cancellation, and rollback are not implemented yet.
33+
34+
Costs: profile metadata is duplicated from reviewed upstream model information
35+
until an artifact-lock file is introduced. Preview profiles are visible but not
36+
selectable. Actual model cutover remains a later generation-migration phase.
37+
38+
## Invariants
39+
40+
1. At most one llama.cpp embedding call runs at a time across project actors.
41+
2. Lexical search, ranking, zvec flush, and state fsync do not hold inference
42+
capacity.
43+
3. One pending upsert batch produces one zvec upsert call and one flush.
44+
4. Shared-memory replacements are deleted only after successor writes commit.
45+
5. Profile listing and dry-run preflight do not initialize `MemoryEngine` or
46+
load a model.
47+
6. Only `qwen3-text-4b-q4` is selectable in phase 1.
48+
7. Qwen3-VL profiles remain unsupported until runtime, artifact, quality,
49+
portability, and memory gates pass.
50+
8. A non-dry-run switch is rejected before any project mutation.
51+
52+
## Built-In Profiles
53+
54+
| Profile | Support | Runtime | Phase-1 behavior |
55+
| ----------------------- | ----------- | ------------------------------ | -------------------------- |
56+
| `qwen3-text-4b-q4` | Stable | llama.cpp GGUF | Current/default profile |
57+
| `qwen3-text-0.6b-q8` | Preview | llama.cpp GGUF | Visible, preflight blocked |
58+
| `qwen3-text-8b-q4` | Preview | llama.cpp GGUF | Visible, preflight blocked |
59+
| `bge-m3` | Preview | Unvalidated | Visible, unsupported |
60+
| `nomic-embed-text-v1.5` | Preview | Unvalidated | Visible, unsupported |
61+
| `qwen3-vl-embedding-2b` | Unsupported | No packaged multimodal runtime | Visible, unsupported |
62+
| `qwen3-vl-embedding-8b` | Unsupported | No packaged multimodal runtime | Visible, unsupported |
63+
64+
The Qwen GGUF presets include pinned repository revisions and LFS SHA-256
65+
digests. They remain non-selectable until retrieval-quality gates and the
66+
generation migration are implemented.
67+
68+
The next phase adds legacy-root generation adaptation, managed generation
69+
manifests, an atomic active pointer, and generation-aware pending journals
70+
before any mutating switch command is enabled.
71+
72+
## Protobuf Boundary
73+
74+
The live memory wire contract remains in
75+
`schema/opencode/memory/v1/memory.proto`; its released `Method`, `Request`, and
76+
`Response` tags are unchanged. Model control is in
77+
`schema/opencode/memory/model/v1/model.proto` with typed request/response
78+
`oneof`s. `daemon.proto` adds new model branches without changing the existing
79+
memory branch. Memory and model requests are validated as exactly one domain at
80+
daemon admission, and the domain schema generation is incremented to `2` so an
81+
older daemon cannot silently ignore a model branch.
82+
83+
The split follows the Protocol Buffers best-practice rules: new model messages
84+
use fresh field numbers, old memory model-method numbers are reserved, enums
85+
start with an `UNSPECIFIED` zero value, model operations use `oneof` rather than
86+
an open method/payload pair, and optional fields preserve presence for nullable
87+
profile metadata. The two domain files are intentionally grouped API surfaces
88+
for this two-file packaging boundary; storage state remains represented by Rust
89+
domain types and is not coupled to the RPC messages.

npm/darwin-arm64/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nguyenthdat/opencode-memory-darwin-arm64",
3-
"version": "0.6.0-beta.2",
3+
"version": "0.6.0",
44
"description": "Shared native OpenCode memory daemon for macOS ARM64",
55
"os": [
66
"darwin"

npm/linux-arm64-gnu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nguyenthdat/opencode-memory-linux-arm64-gnu",
3-
"version": "0.6.0-beta.2",
3+
"version": "0.6.0",
44
"description": "Shared native OpenCode memory daemon for Linux ARM64 glibc",
55
"os": [
66
"linux"

npm/linux-x64-gnu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nguyenthdat/opencode-memory-linux-x64-gnu",
3-
"version": "0.6.0-beta.2",
3+
"version": "0.6.0",
44
"description": "Shared native OpenCode memory daemon for Linux x64 glibc",
55
"os": [
66
"linux"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, expect, test } from "bun:test";
2+
import type { CaptureResponse } from "./contracts.js";
3+
import {
4+
captureWithOutcomeReconciliation,
5+
type ReconciledCaptureResponse,
6+
} from "./capture-reconciliation.js";
7+
import { DaemonOutcomeUnknownError, type NativeMemoryRequester } from "./daemon-client.js";
8+
import type { MemoryMethod } from "./protocol.js";
9+
10+
describe("automatic capture reconciliation", () => {
11+
test("does not replay a capture that returned normally", async () => {
12+
const response = storedResponse();
13+
const native = captureRequester([response]);
14+
15+
const result = await captureWithOutcomeReconciliation(native, { candidate: "memory" });
16+
17+
expect(result).toEqual({ response, reconciled: false, storedOrDuplicate: true });
18+
expect(native.calls).toBe(1);
19+
});
20+
21+
test("replays once and treats duplicate as a committed first attempt", async () => {
22+
const duplicate: CaptureResponse = {
23+
decision: { outcome: "skip", reason: "duplicate" },
24+
};
25+
const native = captureRequester([
26+
new DaemonOutcomeUnknownError("response lost", "call-1"),
27+
duplicate,
28+
]);
29+
30+
const result = await captureWithOutcomeReconciliation(native, { candidate: "memory" });
31+
32+
expect(result).toEqual({ response: duplicate, reconciled: true, storedOrDuplicate: true });
33+
expect(native.calls).toBe(2);
34+
});
35+
36+
test("does not replay definite capture failures", async () => {
37+
const failure = new Error("validation failed");
38+
const native = captureRequester([failure]);
39+
40+
await expect(captureWithOutcomeReconciliation(native, {})).rejects.toThrow("validation failed");
41+
expect(native.calls).toBe(1);
42+
});
43+
});
44+
45+
function captureRequester(
46+
outcomes: Array<CaptureResponse | Error>,
47+
): NativeMemoryRequester & { calls: number } {
48+
return {
49+
calls: 0,
50+
async request<T>(method: MemoryMethod): Promise<T> {
51+
expect(method).toBe("capture");
52+
const outcome = outcomes[this.calls++];
53+
if (outcome instanceof Error) throw outcome;
54+
if (!outcome) throw new Error("missing test outcome");
55+
return outcome as T;
56+
},
57+
};
58+
}
59+
60+
function storedResponse(): CaptureResponse {
61+
return {
62+
decision: { outcome: "accept" },
63+
stored: {
64+
id: "mem_1",
65+
inserted: true,
66+
content_hash: "hash",
67+
updated_at_ms: 1,
68+
scope: "project",
69+
},
70+
};
71+
}

0 commit comments

Comments
 (0)