Skip to content

fix(embedding): add foreground-priority gate to prevent turn.start starvation - #2187

Open
pittosporum-seu wants to merge 1 commit into
MemTensor:mainfrom
pittosporum-seu:fix/embedding-priority-gate
Open

fix(embedding): add foreground-priority gate to prevent turn.start starvation#2187
pittosporum-seu wants to merge 1 commit into
MemTensor:mainfrom
pittosporum-seu:fix/embedding-priority-gate

Conversation

@pittosporum-seu

Copy link
Copy Markdown

Description

Add a lightweight foreground-priority gate to the embedding layer to prevent turn.start RPC starvation when the background capture pipeline (reflection, reward, skill crystallization) is running CPU-bound ONNX inference.

Problem: The local ONNX embedding provider (Xenova/all-MiniLM-L6-v2) runs inference sequentially on the main thread. When background tasks embed trace rows in a batch loop, foreground retrieval requests must wait for the entire batch to finish — often exceeding the host's 8-second prefetch timeout. This causes memory injection to be silently skipped on every affected turn.

Solution: A cooperative yield mechanism (priority-gate.ts) that:

  1. Foreground callers (retrieval) signal via enterForeground() before embedding
  2. The local provider checks isForegroundPending() between individual inference calls and yields the event loop via setImmediate
  3. This gives foreground requests a scheduling opportunity without changing the Embedder interface or adding worker threads

Design rationale: Minimal, non-breaking change. No interface changes to Embedder, no new dependencies, no worker threads. The yield is a single setImmediate between ONNX calls — negligible overhead when no foreground is pending (just a boolean check).

Related Issue (Required): Fixes #2186

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g. code style improvements, linting)
  • Documentation update

How Has This Been Tested?

  • Unit Test
  • Test Script Or Test Steps (please provide)
  • Pipeline Automated API Test (please provide)

Unit test: tests/unit/embedding/priority-gate.test.ts — 6 tests covering:

  • Initial state (no foreground pending)
  • Enter/release lifecycle
  • Stacking multiple foreground calls
  • Idempotent release
  • Yield behavior (immediate when no foreground, setImmediate when foreground pending)

Additionally verified:

  • npx tsc --noEmit passes with zero errors
  • Full test suite: 147/153 test files pass (6 pre-existing failures unrelated to this change — locale/path issues on Windows)

Checklist

  • I have performed a self-review of my own code
  • I have commented my code in hard-to-understand areas
  • I have added tests that prove my fix is effective or that my feature works
  • I have created related documentation issue/PR in MemOS-Docs (if applicable)
  • I have linked the issue to this PR (if applicable)
  • I have mentioned the person who will review this PR

Changes Made

File Change
core/embedding/priority-gate.ts New — foreground-priority gate (enterForeground, isForegroundPending, yieldIfForegroundPending)
core/embedding/providers/local.ts Yield between individual ONNX inference calls when foreground is pending
core/embedding/index.ts Export priority-gate public API
core/retrieval/retrieve.ts Wrap foreground query embed with enterForeground()/release
tests/unit/embedding/priority-gate.test.ts New — 6 unit tests for the priority gate

@Memtensor-AI Memtensor-AI added area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 29, 2026
@Memtensor-AI
Memtensor-AI requested review from hijzy and whipser030 July 29, 2026 17:51
@Memtensor-AI

Memtensor-AI commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2187
Task: dcf29efcbfeb5e26
Base: main
Head: fix/embedding-priority-gate

🔍 OpenCodeReview found 3 issue(s) in this PR.

⚠️ 1 warning(s) occurred during review.


1. apps/memos-local-plugin/core/embedding/providers/local.ts (L80)

The yield is skipped for i === 0 (the first text in the batch). If a foreground request arrives exactly while the first — potentially long — ONNX inference call is running, it must wait for that call to finish before the cooperative yield is ever reached. Consider moving the yield check to execute unconditionally before every inference call (including i === 0) so that a foreground request that arrives after the batch has already started can still be prioritised immediately on the next iteration boundary:

await yieldIfForegroundPending();
const result = await ext(...);

Alternatively, the abort check and the yield guard can both be hoisted to the top of the loop body, keeping them symmetric. The current guard only optimises the common case where the batch starts with no foreground pending and avoids a redundant microtask tick for the very first item — but it silently creates a worst-case equal to a full single-item inference latency for the second and later foreground arrivals that land during index 0.


2. apps/memos-local-plugin/core/retrieval/retrieve.ts (L267-L269)

enterForeground() increments foregroundCount before deps.embedder.embed(...) is called. If embed() throws synchronously (e.g., embedder not yet initialized), the .finally(release) is never attached to any Promise, so release is never invoked and foregroundCount permanently leaks — starving future background batches of any scheduling turns.

Consider wrapping the call in a try/finally to guarantee cleanup even for synchronous throws:

const release = enterForeground();
let p: Promise<...>;
try {
  p = deps.embedder.embed(compiled.text, "query", { ... });
} catch (e) {
  release();
  throw e;
}
return p.then(...).catch(...).finally(release);

Or use a utility like Promise.resolve().then(() => deps.embedder.embed(...)) to guarantee the call is always async.

💡 Suggested Change

Before:

          const release = enterForeground();
          return deps.embedder
            .embed(compiled.text, "query", {

After:

          const release = enterForeground();
          let embedPromise: Promise<unknown>;
          try {
            embedPromise = deps.embedder.embed(compiled.text, "query", {
              signal: opts.signal,
              deadlineAt: opts.deadlineAt,
            });
          } catch (e) {
            release();
            throw e;
          }
          return embedPromise
            .then((vec) => {

3. apps/memos-local-plugin/core/retrieval/retrieve.ts (L277-L289)

The body of the .catch callback is not indented relative to the new IIFE wrapper level. The callback body (lines starting with const code, const message, etc.) sits at the same column as .catch((err) => { instead of being indented one level deeper, which is inconsistent with the surrounding code style and reduces readability.

💡 Suggested Change

Before:

            .catch((err) => {
            const code = (err as { code?: string })?.code;
            const message = err instanceof Error ? err.message : String(err);
            embeddingStats.degraded = true;
            embeddingStats.errorCode = code;
            embeddingStats.errorMessage = message;
            log.warn("embed_failed", {
              reason: ctx.reason,
              code,
              err: message,
            });
            return null;
            })

After:

            .catch((err) => {
              const code = (err as { code?: string })?.code;
              const message = err instanceof Error ? err.message : String(err);
              embeddingStats.degraded = true;
              embeddingStats.errorCode = code;
              embeddingStats.errorMessage = message;
              log.warn("embed_failed", {
                reason: ctx.reason,
                code,
                err: message,
              });
              return null;
            })

🧹 Filtered 5 low-confidence OCR finding(s) before posting/fix-loop (existing_code_mismatch: 5).

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (6/6 executed). memos_local_plugin/unit: 6/6. Duration: 2s

Branch: fix/embedding-priority-gate

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 29, 2026
…arvation (MemTensor#2186)

The local ONNX embedding provider runs CPU-bound inference sequentially on
the main thread. When the background capture pipeline (reflection,
reward, skill crystallization) is embedding trace rows, a foreground
retrieval request (turn.start) must wait for the entire batch to finish,
often exceeding the host's 8s prefetch timeout.

Add a lightweight cooperative yield mechanism:
- priority-gate.ts: enterForeground()/yieldIfForegroundPending() signals
- local.ts: yield between individual ONNX inference calls when foreground pending
- retrieve.ts: mark foreground retrieval embed calls via enterForeground()

This gives the single-threaded ONNX inference a cooperative scheduling
point without changing the Embedder interface or adding worker threads.
@pittosporum-seu
pittosporum-seu force-pushed the fix/embedding-priority-gate branch from 09d750f to a757121 Compare August 5, 2026 15:48
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 5, 2026
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (6/6 executed). memos_local_plugin/unit: 6/6. Duration: 2s [advisory, non-gating] AI-generated tests on branch test/auto-gen-dcf29efcbfeb5e26-20260805235819: 41/41 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: fix/embedding-priority-gate

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: foreground turn.start RPC starved by background pipeline (embedding + LLM resource contention)

4 participants