Skip to content

Commit 64388b5

Browse files
fix(plugin): isolate Windows DLLs and resolve DSH memory issues (#2519)
## Description Fix problems found while investigating #2495: Windows native model dependencies can conflict with host Sharp/libvips, a recalled memory can abort a DSH V4 turn, malformed import rows are silently reported as skipped, and local model source settings are unavailable or ignored. - Isolate Windows local embeddings in a shared, lazy child process. Transformers/Sharp/ONNX stay outside the host process; requests, cancellation and progress use IPC. Remove inherited host preloads, restart failed children, release the child after the last provider closes, and reclaim idle processes. macOS/Linux retain the in-process path. No DLL replacement, production dependency changes or host-version pinning. - Give both recall and auxiliary host-LLM messages the producer-owned `plugin:memos-local-memory` source kind accepted by DSH V4. - Validate bundle rows before writing, report duplicates/invalid rows/database failures separately, and roll back a rejected trace together with any synthetic parents. Keep the existing `imported`/`skipped` response fields. - Add write-free import previews, explicit preserve/target-profile ownership, and active-profile visibility counts. Existing IDs remain duplicates and sharing scopes are preserved. - Add local embedding mirror/model/cache directories, offline mode, a bounded initial wait, and download/load logs. Explicit mirror configuration overrides `HF_ENDPOINT`; default model caching lives outside installed package files. Serialize differently configured loads and restore Transformers' shared environment afterward. - Expose these options in Viewer Settings/Import, test unsaved local model settings, and document configuration and import semantics. Remove an accidental real-model dependency from the Hermes summarizer-status unit test. No production dependency or host peer-range changes. V4 regression tests add the pinned official codec as a dev dependency, align its Cordis dev peer, and pin existing brand/protocol dev peers; both lockfiles are updated. Changes are confined to `apps/memos-local-plugin/`. Related Issue (Required): #2495 (partial follow-up). **Windows sharp/libvips isolation is now included. Native Windows validation is pending; keep the issue open until both load orders and the original long-session case are confirmed on Windows.** Reviewers: @hijzy and @whipser030 (assigned by the repository). ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [x] Documentation update ## How Has This Been Tested? Run from the repository root: ```sh make format cd apps/memos-local-plugin npm run lint npm test npm run build:package node node_modules/typescript/bin/tsc --noEmit --strict --target ES2022 --module ES2022 --moduleResolution Bundler --jsx react-jsx --jsxImportSource preact --lib ES2022,DOM --esModuleInterop --skipLibCheck viewer/src/main.tsx ``` Results: Ruff checks passed (630 Python files unchanged); TypeScript, strict Viewer type checking, and package/Viewer builds passed. Vitest: **190 test files passed; 1638 tests passed, 3 skipped**. Repository pre-commit checks passed over the changed files before committing. - [x] Unit Test — structured world-model round trips (including evidence/legacy strings), malformed structure, session/episode ownership conflicts in preview and writes across batches, stable UI/headless preset ownership, explicit-profile isolation, and backup/transactional legacy-profile migration; Windows process isolation without host Transformers import, concurrent response/progress routing, cancellation, crash/restart, startup/IPC failure, invalid vectors and shared-provider cleanup; source attribution, real V4 codec admission/encoding for recall, pre-step inbox batches and host-LLM output, plus rejection of retired sources; local mirror precedence, offline paths, stable cache, shared-environment restoration, serialized loads, progress and timeout/warmup; SQLite import validation, rollback, preview and ownership. - [x] Test Script Or Test Steps (please provide) — On macOS with official DSH **0.2.0-rc.2**, use an isolated DSH profile/data home and the logged-in `deepseek-flash` account. Load this build, import a synthetic deployment-region memory, and ask `deployment region` without including the stored answer. Recall injected the matching memory, `memos_get` returned its contents, and the model answered the stored value with a completed turn and no V4 admission error. Restore the original test installation afterward and verify its hash. A real cached local model also returned a 384-dimensional embedding in offline mode. - [x] Pipeline Automated API Test (please provide) — `tests/unit/server/http.test.ts` verifies import options and invalid-option responses. An isolated HTTP smoke test verified zero writes during preview; one valid, one duplicate and one invalid timestamp row were classified correctly; target ownership made the imported row visible. ## Checklist - [x] I have performed a self-review of my own code | 我已自行检查了自己的代码 - [x] I have commented my code in hard-to-understand areas | 我已在难以理解的地方对代码进行了注释 - [x] I have added tests that prove my fix is effective or that my feature works | 我已添加测试以证明我的修复有效或功能正常 - [x] I have created related documentation issue/PR in [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) (if applicable) | 我已在 [MemOS-Docs](https://github.com/MemTensor/MemOS-Docs) 中创建了相关的文档 issue/PR(如果适用) — N/A: local-plugin configuration and import behavior are documented in this package's README. - [x] I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用) - [x] I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人 ## Reviewer Checklist - [x] Issue linkage and scope reviewed: relates to #2495; keep the issue open pending native Windows DLL verification. - [x] Made sure local checks passed; GitHub CI results are tracked separately on this PR. - [x] Tests have been provided ### V4 regression coverage Bridge tests now use the production recall factory and assert producer-owned source kinds. The published `@deepseek-ai/dsh-session-format-v3-to-v4@0.2.0-rc.2` validates both individual messages and the query/recall inbox batch through `assertV4RowAdmission` and `encodeEvent`. Host-LLM auxiliary messages pass without `form`; both old source shapes are rejected. A mutation check restoring `kind: "plugin"` in the real factory made the admission regression fail with the original error. Tests require no desktop installation or account. ### Windows isolation validation - **Cross-platform checks completed:** real Node and official DSH Electron subprocess smoke tests each returned two concurrent 384-dimensional vectors, with a separate worker PID and no Sharp/libvips/ONNX native libraries added to the parent process. `npm pack --dry-run --ignore-scripts` confirmed all three worker runtime modules are included. - **Native Windows check pending:** build the branch and run `scripts/verify-windows-dll-isolation.mjs --host-sharp <host sharp package path> [--cache-dir <existing model cache>] [--offline]`. The README includes PowerShell instructions for executing with DSH's Electron executable when host Sharp is in ASAR. Each load order runs in a fresh process and verifies both embedding output and real PNG encoding using host Sharp. No native Windows pass is claimed from macOS. - Restart DSH completely before testing the plugin; confirm new and long conversations plus host image processing after local-model initialization. Electron hosts need `ELECTRON_RUN_AS_NODE`/`child_process.fork` support; failure does not fall back to loading native model libraries into the host. ### QA report follow-up Address the verified findings from the independent QA report: - Preserve imported world-model `structure`, including labelled evidence entries and legacy string entries. Reject malformed structures with field diagnostics instead of silently clearing them. - Reject traces whose existing/planned episode belongs to another session or whose parent records belong to another memory owner. Preview tracks accepted parents across batches and applies the same checks without writing records. - Use the configured MemOS `profileId` consistently for DSH recall, capture, tools and Viewer; `agentPreset` no longer changes memory ownership. Explicit profiles stay isolated. Older preset-owned records are not silently merged: the packaged `migrate-dsh-preset-profile.mjs` tool defaults to read-only preview and requires a new SQLite backup before an atomic, explicitly scoped move. Skill-name/constraint conflicts roll back the entire move. - Clarify that the Viewer button **Repair embeddings** invokes `POST /api/v1/embeddings/rebuild` with `mode: repair`. Validation on the final changes: **190 test files passed; 1638 tests passed, 3 skipped**; `npm run lint`, package/Viewer build, `make format` (630 Python files unchanged), pre-commit, and npm package-content checks passed. Added regressions failed against the old implementation before the fixes. No schema migration or automatic modification of existing user databases is introduced. Windows native verification remains pending as documented above.
2 parents adcb9f2 + 04bfa63 commit 64388b5

56 files changed

Lines changed: 4031 additions & 1658 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Local plugin installation contract
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'apps/memos-local-plugin/**'
7+
- '.github/workflows/memos-local-plugin-install-contract.yml'
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
clean-install:
15+
strategy:
16+
fail-fast: false
17+
matrix:
18+
os: [ubuntu-latest, macos-latest, windows-latest]
19+
node: ['22', '24']
20+
manager: [npm, pnpm]
21+
runs-on: ${{ matrix.os }}
22+
defaults:
23+
run:
24+
shell: bash
25+
working-directory: apps/memos-local-plugin
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: actions/setup-node@v4
29+
with:
30+
node-version: ${{ matrix.node }}
31+
- name: Pin installation tools
32+
run: |
33+
npm install --global npm@11.17.0 pnpm@11.7.0 --ignore-scripts --no-audit --no-fund
34+
test "$(npm --version)" = 11.17.0
35+
test "$(pnpm --version)" = 11.7.0
36+
- name: Verify reviewed lock metadata
37+
run: |
38+
npm run lock:check
39+
npm run verify:script-policy
40+
- name: Clean npm installation
41+
if: matrix.manager == 'npm'
42+
run: npm ci --no-audit --no-fund
43+
- name: Frozen pnpm installation
44+
if: matrix.manager == 'pnpm'
45+
run: pnpm install --frozen-lockfile
46+
- name: SQLite and CPU ONNX runtime smoke
47+
run: node scripts/verify-native-runtime.cjs
48+
- name: Type check and build package
49+
run: |
50+
npm run lint
51+
npm run build:package
52+
- name: Check runtime and lock contracts
53+
run: |
54+
npm exec -- vitest run tests/unit/install/sqlite-install-contract.test.ts
55+
git diff --exit-code -- package.json package-lock.json pnpm-lock.yaml

‎.github/workflows/memos-local-plugin-publish.yml‎

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -388,19 +388,17 @@ jobs:
388388

389389
- name: Install dependencies
390390
shell: bash
391-
run: bash ../../.github/scripts/retry.sh --label "npm ci" -- npm ci
392-
393-
- name: Rebuild for x64 under Rosetta (darwin-x64 only)
394-
if: matrix.platform == 'darwin-x64'
395-
shell: bash
396-
run: bash ../../.github/scripts/retry.sh --label "npm rebuild better-sqlite3 under Rosetta" -- arch -x86_64 npm rebuild better-sqlite3
391+
run: |
392+
npm install --global npm@11.17.0 --ignore-scripts --no-audit --no-fund
393+
npm run lock:check
394+
bash ../../.github/scripts/retry.sh --label "npm ci" -- npm ci
397395
398396
- name: Collect prebuild
399397
shell: bash
400398
run: |
401399
bash ../../.github/scripts/retry.sh --label "collect ${{ matrix.platform }} prebuild" -- bash -euo pipefail -c '
402400
mkdir -p prebuilds/${{ matrix.platform }}
403-
cp node_modules/better-sqlite3/build/Release/better_sqlite3.node prebuilds/${{ matrix.platform }}/
401+
cp node_modules/better-sqlite3/prebuilds/${{ matrix.platform }}.node prebuilds/${{ matrix.platform }}/better_sqlite3.node
404402
'
405403
406404
- name: Upload prebuild artifact
@@ -492,16 +490,12 @@ jobs:
492490
find . -name "*.node" -exec ls -lh {} \;
493491
'
494492
495-
- name: Install dependencies (skip native build)
496-
run: bash ../../.github/scripts/retry.sh --label "npm ci --ignore-scripts" -- npm ci --ignore-scripts
497-
498-
- name: Install Linux sqlite binding for validation
493+
- name: Install dependencies with reviewed scripts
499494
run: |
500-
bash ../../.github/scripts/retry.sh --label "install linux sqlite binding for validation" -- bash -euo pipefail -c '
501-
test -s prebuilds/linux-x64/better_sqlite3.node
502-
mkdir -p node_modules/better-sqlite3/build/Release
503-
cp prebuilds/linux-x64/better_sqlite3.node node_modules/better-sqlite3/build/Release/better_sqlite3.node
504-
'
495+
npm install --global npm@11.17.0 --ignore-scripts --no-audit --no-fund
496+
npm run lock:check
497+
bash ../../.github/scripts/retry.sh --label "npm ci" -- npm ci
498+
npm run verify:native
505499
506500
- name: Generate telemetry credentials for a real publish
507501
if: ${{ inputs.dry_run != true && (inputs.publish_phase || 'full') != 'stage_release' }}

‎apps/memos-local-plugin/.npmrc‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
engine-strict=true
2+
strict-allow-scripts=true

‎apps/memos-local-plugin/README.md‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,69 @@
11
# @memtensor/memos-local-plugin
22

3+
> **Runtime compatibility change:** this development version requires Node.js
4+
> **22 or newer in the actual host process**, including Electron's embedded Node.
5+
> It pins `better-sqlite3` to `13.0.3`, using packaged Node-API prebuilds instead
6+
> of per-Node/Electron ABI rebuilds. Node 20 users must keep their previously
7+
> installed compatible release until upgrading the host; this version rejects
8+
> Node 20 before opening storage. This is a breaking runtime requirement and
9+
> must be called out in the release version/notes; no database schema migration
10+
> is introduced by this dependency change.
11+
12+
### Reproducible dependency maintenance
13+
14+
Use npm **11.17.0** and pnpm **11.7.0** with a supported Node runtime. These are
15+
our installation-policy validation versions; installing a different package
16+
manager version must not silently regenerate the release locks.
17+
18+
From `apps/memos-local-plugin`, after a reviewed dependency edit:
19+
20+
```sh
21+
npm install --package-lock-only --ignore-scripts
22+
npm run lock:normalize
23+
pnpm import
24+
npm run lock:check
25+
```
26+
27+
`--ignore-scripts` is only for lock generation, not the release installation.
28+
`lock:normalize` repairs npm's SQLite install-metadata inference only for the
29+
reviewed 13.0.3 tarball URL and integrity. It refuses unknown artifacts and is
30+
never invoked during end-user installation. Commit both lockfiles together.
31+
`pnpm import` can change peer layouts; inspect every version change rather than
32+
assuming a one-to-one conversion. The release validation command checks the
33+
SQLite lock metadata, and the installation contract tests compare both locks.
34+
35+
Validate `npm ci` and `pnpm install --frozen-lockfile` in separate clean checkouts
36+
with independent `node_modules`. Both must load SQLite, create a table, write,
37+
query and close under the actual Node/Electron runtime. Repeat lock generation
38+
and normalization to check that it leaves the committed locks unchanged.
39+
Runtime tarballs installed by DSH use the profile owner's dependency graph;
40+
the source checkout locks alone do not establish the packed-install result.
41+
42+
### Dependency install-script policy
43+
44+
npm policy lives in this package's `allowScripts`; `.npmrc` enables engine and
45+
script strictness for source/CI installs. pnpm 11 uses the separate
46+
`pnpm-workspace.yaml#allowBuilds` policy. Reviewed esbuild and Sharp versions
47+
may run scripts; SQLite 13.0.3, protobufjs, optional fsevents, and the ONNX CUDA
48+
downloader are denied. CPU ONNX and SQLite prebuilds must pass runtime smoke
49+
checks even when these scripts are denied. New versions require review.
50+
51+
These policies belong to the installing root, not arbitrary transitive
52+
packages. For DSH, prepare the equivalent policy in the selected profile before
53+
the first add. Do not overwrite another plugin's settings or an explicit script
54+
denial. The installer rejects unknown pending builds instead of approving them.
55+
Direct host-managed installs must configure the host's policy as well.
56+
Do not bypass this with `--dangerously-allow-all-scripts`, blanket approval or
57+
`--ignore-scripts`. Older npm versions have different policy behavior and are
58+
not the release installation validation path.
59+
60+
A failed prebuilt probe is an installation failure, not permission to invoke
61+
node-gyp automatically. Preserve the old deployment and diagnose the actual
62+
host/runtime and binary resolution. Standard installs do not require MSVC,
63+
Python or Electron headers. Windows Desktop must be revalidated with the final
64+
package; isolated dependency-smoke results do not replace integration tests.
65+
66+
367
> Reflect2Evolve memory plugin for AI agents.
468
> One algorithm core, with adapters for OpenClaw, Hermes Agent, and DeepSeek
569
> Harness.
@@ -261,6 +325,113 @@ The runtime/config location is resolved in the following priority order:
261325
4. **`DSH_HOME`** (DSH only) — defaults the DSH memory root to `$DSH_HOME/memos-plugin/`
262326
5. **Default path** — `~/.hermes/memos-plugin/`, `~/.openclaw/memos-plugin/`, or `~/.dsh/memos-plugin/` based on the agent
263327

328+
### Local embedding model sources
329+
330+
For `embedding.provider: local`, the Viewer Settings page exposes the same
331+
options as `config.yaml`:
332+
333+
```yaml
334+
embedding:
335+
provider: local
336+
model: Xenova/all-MiniLM-L6-v2
337+
local:
338+
remoteHost: "" # Optional HTTP(S) mirror base URL
339+
modelPath: "" # Optional local model root containing <model>/ files
340+
cacheDir: "" # Empty: ~/.cache/memos/models, outside installed packages
341+
offline: false # true: load local/cache files only, without downloads
342+
loadTimeoutMs: 120000
343+
```
344+
345+
`local.remoteHost` takes precedence over `HF_ENDPOINT`; leaving both empty uses
346+
Hugging Face. Use absolute directory paths (a leading `~/` is also supported).
347+
For offline use, place the model's config, tokenizer and quantized ONNX files
348+
under `<modelPath>/<model>/`, or point `cacheDir` to an existing Transformers.js
349+
cache. Changing the model/source settings causes a fresh pipeline load.
350+
351+
The Settings **Test** button uses the form's unsaved local settings and shows a
352+
loading notice. Logs report `loading`, per-file `download.progress`, `ready`,
353+
and `load_failed`. The load timeout bounds the caller's wait; Transformers
354+
loading can continue in the background and warm the next request.
355+
356+
### Windows native-library isolation
357+
358+
On Windows, local embedding inference runs in a separate Node process so the
359+
plugin's Transformers/Sharp/libvips libraries cannot occupy DLL names in the
360+
host process. The model process is shared, starts lazily, and stops when its
361+
last provider closes or after five minutes without requests. Cancelling one
362+
request does not cancel other callers. A crashed child is restarted on the
363+
next request; failures are reported as `embedding_unavailable`, without
364+
falling back to loading these libraries inside the host. macOS/Linux keep
365+
the existing in-process embedding path.
366+
367+
Electron hosts must support `ELECTRON_RUN_AS_NODE`/`child_process.fork`.
368+
The child reuses the host executable, clears inherited `NODE_OPTIONS` and
369+
`NODE_PATH` preloads, and sends only model settings/text/vectors and model
370+
logs over IPC. No DLL replacement or host Sharp version pin is required.
371+
372+
To verify the native collision fix on Windows, build this branch and run
373+
`scripts/verify-windows-dll-isolation.mjs` against the **host's** Sharp package.
374+
For a DSH installation whose JavaScript is inside `app.asar`, use its Electron
375+
executable so ASAR paths remain readable (replace the example install paths):
376+
377+
```powershell
378+
npm ci
379+
npm run build:package
380+
$DshExe = "C:\path\to\DeepSeek Harness.exe"
381+
$HostSharp = "C:\path\to\resources\app.asar\dsh\node_modules\sharp"
382+
$ModelCache = "$env:USERPROFILE\.cache\memos\models"
383+
$PreviousRunAsNode = $env:ELECTRON_RUN_AS_NODE
384+
$env:ELECTRON_RUN_AS_NODE = "1"
385+
try {
386+
& $DshExe .\scripts\verify-windows-dll-isolation.mjs --host-sharp $HostSharp --cache-dir $ModelCache --offline
387+
} finally {
388+
$env:ELECTRON_RUN_AS_NODE = $PreviousRunAsNode
389+
}
390+
```
391+
392+
Use an existing Transformers model cache with `--offline`; omit that flag to
393+
allow downloading (including through `HF_ENDPOINT`). Each of the two load
394+
orders runs in a fresh process: plugin model before host Sharp, and host
395+
Sharp before plugin model. Both must return a 384-dimensional vector and
396+
successfully encode a PNG with host Sharp; model inference must add no
397+
Sharp/libvips/ONNX libraries to the parent process. The script reports the
398+
separate host/worker PIDs and host library versions.
399+
400+
Then restart DSH completely, enable this plugin build and the local model,
401+
and verify both a new conversation and a long conversation that previously
402+
failed, including any image processing path. Windows DLL behavior requires
403+
this native check; passing the cross-platform IPC tests alone does not prove
404+
that the original Windows failure is resolved.
405+
406+
### Bundle import preview and ownership
407+
408+
Choosing a JSON bundle in the Viewer previews it without writing records.
409+
Choose either preserving original ownership (default) or importing into the
410+
active agent's current/named profile. Sharing scopes are preserved. Preview
411+
shows how many candidate rows would be visible to the active profile.
412+
Existing IDs are skipped, including on a target-profile import; this does not
413+
change ownership of records already stored.
414+
415+
`POST /api/v1/import` accepts the same JSON or multipart body as before, with
416+
optional query parameters `dryRun=true`, `ownership=preserve|target` and
417+
`targetProfileId` (only with `ownership=target`). The response retains
418+
`imported` and `skipped`, adding `duplicates`, `invalid`, `failed`, `visible`,
419+
`hidden`, `dryRun` and up to 50 row diagnostics (`kind`, zero-based `index`,
420+
`field`, `code`, `message`). Timestamps and integer IDs such as `turnId` must
421+
be safe integers; invalid values are rejected rather than rounded.
422+
423+
Preview counts are estimates: database write failures can only be reported
424+
on the real import. A rejected trace rolls back its synthetic session and
425+
episode. Use **Repair embeddings** after import to enable semantic retrieval
426+
for imported rows; JSON bundles do not contain embedding vectors.
427+
API callers should use `POST /api/v1/embeddings/rebuild` with
428+
`{"mode":"repair","limit":100}` and repeat batches until `done` is true;
429+
`Repair embeddings` is the Viewer button label, not an `/embeddings/repair` route.
430+
431+
DSH memory ownership follows the configured `profileId`, independently of
432+
agent presets. See [legacy preset ownership repair](adapters/deepseek-harness/README.md#legacy-preset-ownership)
433+
for explicitly migrating older `standard`/other preset partitions after backup.
434+
264435
### Docker Deployment
265436

266437
When running the daemon in a Docker container, you must explicitly specify the config location if it differs from the default path. There are three ways to do this:

0 commit comments

Comments
 (0)