Skip to content

fix(plugin): harden local plugin runtime and embeddings - #2266

Open
Hun-ger wants to merge 10 commits into
mainfrom
fix-20260820-local-plugin
Open

fix(plugin): harden local plugin runtime and embeddings#2266
Hun-ger wants to merge 10 commits into
mainfrom
fix-20260820-local-plugin

Conversation

@Hun-ger

@Hun-ger Hun-ger commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR consolidates the tested local-plugin fixes from fix-20260820-local-plugin:

  • preserves the OpenClaw stale/recycled PID lock fix from fix: pidIsAlive self-PID check for stale lock detection #2099;
  • makes packed OpenClaw and Hermes installations compatible with npm 11 when development peer dependencies are omitted;
  • replaces the character-based head truncation proposed in Fix #2121: [Bug] memos-local-plugin: embedding source text exceeds embedding-3's 3072-token #2123 with provider-level batchSize, token-based maxInputTokens, bounded four-chunk sampling and vector pooling, adaptive batch splitting for 400/413/422 responses, and per-input failure isolation across capture, import, and embedding rebuild paths;
  • defaults new installations to maxInputTokens: 1024, while preserving 0 for existing configurations created before this setting;
  • removes the separate rebuild “items per request” control and exposes only provider batch size and the per-input token limit.

This supersedes #2123.

Related Issue (Required): Fixes #2121
Related: #2099

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
    • pnpm test: 176 test files passed; 1,466 tests passed and 2 skipped.
    • Targeted config, embedding, install, runtime-lock, and pipeline suite: 258 tests passed and 1 skipped.
  • Test Script Or Test Steps
    • pnpm lint
    • pnpm build:package
    • macOS: installed and behavior-tested with OpenClaw and Hermes.
    • Windows: installed and behavior-tested with OpenClaw, Hermes, and DeepSeek Harness.
  • Pipeline Automated API Test (not applicable)

make format was also attempted, but Poetry is not installed in the local environment (poetry: No such file or directory). The plugin's own lint, full tests, and production build all pass.

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) | Not applicable; the in-repository configuration templates and embedding documentation were updated.
  • I have linked the issue to this PR (if applicable) | 我已将 issue 链接到此 PR(如果适用)
  • I have mentioned the person who will review this PR | 我已提及将审查此 PR 的人

@Memtensor-AI @syzsunshine219 please review.

Reviewer Checklist

34262315716 and others added 9 commits July 12, 2026 15:33
Fixes #2098

## Problem
When OpenClaw gateway exits abnormally, the openclaw-runtime.lock/ dir
and owner.json remain on disk. On restart, if the OS reuses the same
PID, pidIsAlive() signals itself and incorrectly reports duplicate
runtime.

## Fix
Added `if (pid === process.pid) return false;` in pidIsAlive() to
exclude self-PID from stale lock check. Zero side-effect: owner.pid can
only equal process.pid when the lock is from a previous lifecycle of the
same process.

## Files changed
- apps/memos-local-plugin/adapters/openclaw/runtime-lock.ts
…fix/local-plugin-embedding-input-limits

# Conflicts:
#	apps/memos-local-plugin/install.ps1
#	apps/memos-local-plugin/install.sh
@Memtensor-AI Memtensor-AI added area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Aug 21, 2026
@Memtensor-AI

Memtensor-AI commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2266
Task: a0819c4b48208c12
Base: main
Head: fix-20260820-local-plugin

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


1. apps/memos-local-plugin/core/capture/embedder.ts (L92-L93)

In the non-summaryOnly path, inputs is packed as [...summaryTexts, ...actionTexts], so inputs.length === steps.length * 2. When warnPartialFailures is called with inputs.length as inputCount, the log emits { failedCount, inputCount: steps.length * 2, stepCount: steps.length }. This means a reader debugging a partial failure would see e.g. failedCount=2, inputCount=4, stepCount=2 — it's unclear whether inputCount counts vectors or steps, making the log hard to interpret. Consider either:

  • Renaming inputCount to vectorCount in the warning payload, or
  • Passing steps.length as a separate vectorsPerStep field, or
  • Deriving stepCount from inputCount / 2 implicitly in the warning body.

Suggested fix: log vectorCount instead of inputCount so the relationship to stepCount is self-evident.

💡 Suggested Change

Before:

      warnPartialFailures(settled, inputs.length);
      const out: VecPair[] = new Array(steps.length);

After:

      warnPartialFailures(settled, inputs.length); // inputCount = steps.length * 2 (summary + action)
      const out: VecPair[] = new Array(steps.length);

2. apps/memos-local-plugin/core/capture/embedder.ts (L91-L94)

The warnPartialFailures helper uses optional chaining (settled[i]?.ok) to guard against undefined entries. If embedManySettled returns fewer items than inputCount, the missing slots silently count as failures. However, there is no validation or assertion that settled.length === inputCount before consuming the results. A short-length response would already be mis-counted as failures in warnPartialFailures, but then the consuming loop at line 94–101 would also produce null vectors for the missing slots without any distinct warning that the contract was violated (wrong array length vs. actual embedding error). Consider adding a length check after the embedManySettled call to surface contract violations explicitly:

if (settled.length !== inputs.length) {
  log.warn("embed.length_mismatch", { expected: inputs.length, actual: settled.length });
}
💡 Suggested Change

Before:

      const settled = await embedder.embedManySettled(inputs);
      warnPartialFailures(settled, inputs.length);
      const out: VecPair[] = new Array(steps.length);
      for (let i = 0; i < steps.length; i++) {

After:

      const settled = await embedder.embedManySettled(inputs);
      if (settled.length !== inputs.length) {
        log.warn("embed.length_mismatch", { expected: inputs.length, actual: settled.length });
      }
      warnPartialFailures(settled, inputs.length);
      const out: VecPair[] = new Array(steps.length);
      for (let i = 0; i < steps.length; i++) {

3. apps/memos-local-plugin/core/config/index.ts (L185-L196)

When raw.embedding exists on disk but is not a plain object (e.g., embedding: null, embedding: "", or a numeric scalar — all legal in YAML), isPlainObject returns false, so embedding falls back to {}, and the returned object silently replaces the on-disk value with { maxInputTokens: 0 }. This discards whatever the user wrote.

The batchSize field introduced in the same PR has the same problem: it has no corresponding legacy back-fill here, so if an existing config has embedding: null on disk it will end up with the defaults' batchSize: 32 thanks to deepMerge, while maxInputTokens is pinned to 0. That asymmetry is intentional for maxInputTokens, but the silent replacement of a non-object embedding scalar without a warning is a subtle data-loss bug.

Suggested fix: guard against the non-plain-object case explicitly, emit a warning, and skip injection (let deepMerge fall back to defaults) rather than silently constructing a partial embedding object:

function withLegacyEmbeddingInputLimit(raw: unknown): unknown {
  if (!isPlainObject(raw)) return raw;
  // If embedding is not a plain object, skip injection; deepMerge will
  // fall back to defaults (including the new 1024 default), which is
  // the correct behaviour for new/broken configs.
  if (!isPlainObject(raw.embedding)) return raw;
  if (Object.hasOwn(raw.embedding, "maxInputTokens")) return raw;
  return {
    ...raw,
    embedding: {
      ...raw.embedding,
      maxInputTokens: 0,
    },
  };
}

With this, only a pre-existing valid embedding object that is simply missing maxInputTokens gets the 0 back-fill; every other case falls through to the normal merge/default path.

💡 Suggested Change

Before:

function withLegacyEmbeddingInputLimit(raw: unknown): unknown {
  if (!isPlainObject(raw)) return raw;
  const embedding = isPlainObject(raw.embedding) ? raw.embedding : {};
  if (Object.hasOwn(embedding, "maxInputTokens")) return raw;
  return {
    ...raw,
    embedding: {
      ...embedding,
      maxInputTokens: 0,
    },
  };
}

After:

function withLegacyEmbeddingInputLimit(raw: unknown): unknown {
  if (!isPlainObject(raw)) return raw;
  // If embedding is not a plain object, skip injection; deepMerge will
  // fall back to defaults (including the new 1024 default), which is
  // the correct behaviour for new/broken configs.
  if (!isPlainObject(raw.embedding)) return raw;
  if (Object.hasOwn(raw.embedding, "maxInputTokens")) return raw;
  return {
    ...raw,
    embedding: {
      ...raw.embedding,
      maxInputTokens: 0,
    },
  };
}

4. apps/memos-local-plugin/core/config/schema.ts (L63-L64)

The description parameter is passed to maxInputTokens but omitted for batchSize. Since batchSize already has a JSDoc comment above it, this is not critical, but for consistency with the NumberInRange API extension introduced in this same diff, consider adding a description string here as well. This ensures both fields have machine-readable descriptions in the emitted JSON Schema (used for editor autocomplete per the file's header comment).

💡 Suggested Change

Before:

  /** Maximum physical texts sent in one embedding-provider HTTP request. */
  batchSize: NumberInRange(32, 1, 256),

After:

  /** Maximum physical texts sent in one embedding-provider HTTP request. */
  batchSize: NumberInRange(32, 1, 256, "Maximum texts sent per embedding-provider HTTP request."),

5. apps/memos-local-plugin/core/embedding/types.ts (L194-L197)

The embedManySettled method is declared as optional (?) on the Embedder interface with a comment that "implementations predating this method may omit it". However, the concrete implementation in embedder.ts always registers it on the api object, so callers targeting the concrete embedder can always rely on it.

The risk is that call sites which accept a generic Embedder reference must defensively guard against undefined before calling it (e.g. embedder.embedManySettled?.(…)). If any call site forgets the optional-chaining operator, TypeScript will flag it — but that's only a compile-time check; silently falling back to the throwing embedMany at runtime may be surprising.

Consider whether the method should be made required (now that it is implemented in the single concrete factory), or at least document the expected fallback strategy for callers working against the interface.


6. apps/memos-local-plugin/core/embedding/types.ts (L206-L208)

The discriminated union uses MemosError (a concrete class) as the error type in the failure branch. This couples the public interface type to a specific internal error class. If a future provider or adapter wraps errors differently, callers that pattern-match on result.error will need to be updated.

Consider widening the error type to Error (or even unknown) for better long-term flexibility, and narrowing inside implementations with instanceof MemosError as needed.


7. apps/memos-local-plugin/core/pipeline/memory-core.ts (L4647)

Potential runtime TypeError: when result is undefined (i.e., settled has fewer elements than batch), result?.error evaluates to undefined, and then .message is accessed on undefined, causing an uncaught TypeError.

The optional chain ?. guards result itself, but does not guard the subsequent .message property access on the already-undefined result?.error sub-expression.

Suggestion: extend the optional chain all the way to .message to keep the fallback safe.

💡 Suggested Change

Before:

            firstSlotError ??= result?.error.message ?? `missing vector for ${slot.id}`;

After:

            firstSlotError ??= result?.error?.message ?? `missing vector for ${slot.id}`;

8. apps/memos-local-plugin/core/pipeline/memory-core.ts (L4669-L4671)

In rebuild mode the stall-detection condition (updated === 0 && failed > 0) is not evaluated, so a persistent provider rejection (e.g. 400/422 on every slot in the current page) will silently keep the done flag as false and the caller will loop forever, always requesting the same page at the same offset without making progress.

Consider applying the same stall guard to rebuild mode, or documenting explicitly why infinite looping is acceptable here (e.g. caller-side retry budget).

💡 Suggested Change

Before:

    const done = mode === "rebuild"
      ? nextOffset >= targetSlots.length || batch.length === 0
      : statsAfter.needsRepair === 0 || batch.length === 0 || (updated === 0 && failed > 0);

After:

    const done = mode === "rebuild"
      ? nextOffset >= targetSlots.length || batch.length === 0 || (updated === 0 && failed > 0)
      : statsAfter.needsRepair === 0 || batch.length === 0 || (updated === 0 && failed > 0);

9. apps/memos-local-plugin/core/embedding/embedder.ts (L240-L241)

When a logical input is split into multiple chunks, ??= records only the first chunk error. If chunk 0 succeeds and chunk 1 fails, logical.error remains null, yet chunkVectors[1] stays null. The downstream poolChunkVectors guard then throws a generic internal MemosError("[embedding] internal: missing chunk vector") instead of propagating the real provider error. The actual provider error is silently swallowed.

Fix: track whether any chunk failed, not just the first, and return { ok: false, error } when any chunk vector is missing. One approach:

if (result.ok) {
  entry.logical.chunkVectors[entry.chunkIndex] = result.vector;
} else {
  entry.logical.errors ??= [];
  entry.logical.errors.push(result.error);
  // Replace null placeholder so poolChunkVectors can detect it
}

Or simply: after the batch loops, check logical.chunkVectors.some(v => v === null) and if any is null, pick the first recorded error (or aggregate them) rather than calling poolChunkVectors.


10. apps/memos-local-plugin/core/embedding/embedder.ts (L275-L277)

roundTrips++ is incremented at the top of embedPhysicalBatch, but when a 400/413/422 triggers a recursive binary split, the function calls itself recursively without decrementing. The original failing attempt is counted, and each recursive sub-call also increments roundTrips — which is correct. However, failures++ is also incremented at the top of the catch block before the split decision is made. When the split path is taken, the original failure is counted even though no terminal failure occurred (it may succeed via sub-batches). This inflates the failures counter artificially.

Additionally, if the recursive sub-calls themselves fail terminally, recordTerminalProviderFailure is called for every leaf sub-batch, causing multiple config.onError callbacks and multiple notifyStatus calls for a single logical user request.

Fix: move failures++ (and its associated accounting) to recordTerminalProviderFailure only, and remove it from the catch block when the split path is taken:

catch (err) {
  const wrapped = asEmbeddingError(err, provider.name);
  if (entries.length > 1 && shouldSplitProviderBatch(wrapped)) {
    // do NOT count failures++ here — sub-batches will account for themselves
    ...
    return [...left, ...right];
  }
  failures++; // only for terminal failures
  recordTerminalProviderFailure(...);
}

11. apps/memos-local-plugin/core/embedding/embedder.ts (L120-L125)

The .find() check on line 120-121 is dead code. If any result has !result.ok, the subsequent .map() will already throw on the first such element (arrays iterate from index 0). The .find() pre-check adds unnecessary iteration without providing any short-circuit benefit since .map() visits every element anyway. Remove the redundant guard:

async function embedMany(...): Promise<EmbeddingVector[]> {
  const settled = await embedManySettled(inputs, options);
  return settled.map((result) => {
    if (!result.ok) throw result.error;
    return result.vector;
  });
}

12. apps/memos-local-plugin/core/embedding/embedder.ts (L465)

When text is an empty string "", the chunking loop body never executes (for...of on "" yields nothing), so current remains "" (falsy) and chunks.length is 0. The condition chunks.length === 0 is true, so the empty string "" is pushed. This results in splitEmbeddingInput("", ...) returning [""] rather than [] or [""] being guarded upstream. The empty string chunk is then sent to the provider as an embedding request, which may cause unexpected API errors.

Consider adding an early guard:

function splitEmbeddingInput(text: string, configuredLimit: number): string[] {
  if (!text) return [text]; // let upstream/provider handle empty strings uniformly
  ...

Or validate non-empty strings upstream in toInput/embedManySettled.


13. apps/memos-local-plugin/core/embedding/embedder.ts (L497-L506)

poolChunkVectors returns a Float32Array, but EmbeddingVector is typed as number[] (from core/types.ts). Float32Array is not assignable to number[] in TypeScript — methods like .map(), .filter(), spread ([...vec]), and JSON.stringify() behave differently. For example, JSON.stringify(new Float32Array([1,2])) produces {"0":1,"1":2} not [1,2].

Either convert the result to a plain array before returning:

return normalize ? Array.from(l2Normalize(pooled)) : Array.from(pooled);

or confirm that EmbeddingVector is actually Float32Array | number[] and that all consumers handle both forms.

Note: the single-chunk fast path if (vectors.length === 1) return first; returns the original provider vector (likely number[]), while multi-chunk returns Float32Array — this inconsistency in return type is also a concern.


14. apps/memos-local-plugin/core/embedding/embedder.ts (L474)

The divisor (MAX_CHUNKS_PER_LOGICAL_INPUT - 1) equals 3 with the current constant value of 4, which is safe. However, if a future maintainer sets MAX_CHUNKS_PER_LOGICAL_INPUT = 1, this becomes a division by zero producing NaN. chunks[NaN] is undefined, and the ! non-null assertion silently pushes undefined into selected.

Add a guard or assert the constant at definition:

const MAX_CHUNKS_PER_LOGICAL_INPUT = 4;
if (MAX_CHUNKS_PER_LOGICAL_INPUT < 2) throw new Error("MAX_CHUNKS_PER_LOGICAL_INPUT must be >= 2");

Or guard inline:

const divisor = MAX_CHUNKS_PER_LOGICAL_INPUT - 1;
if (divisor === 0) return [chunks[0]!];
const index = Math.round((i * (chunks.length - 1)) / divisor);

15. apps/memos-local-plugin/core/embedding/embedder.ts (L321)

Using bitwise right-shift >> 1 for the midpoint calculation is unconventional in TypeScript. Prefer Math.floor(entries.length / 2) for readability and consistency with the rest of the codebase.


16. apps/memos-local-plugin/core/util/foreground-resources.ts (L273-L281)

The catch block in the embedMany fallback silently converts all thrown errors — including AbortError from signal cancellation — into settled { ok: false } results. This breaks cancellation semantics: when the caller aborts the signal, the function should reject with the abort error (as embedMany does), not return a batch of { ok: false } items. Callers checking signal.aborted after await embedManySettled(...) will never see a rejection, causing them to process stale/cancelled results.

Re-throw abort/lifecycle errors before folding them into settled results:

💡 Suggested Change

Before:

      } catch (err) {
        const error = err instanceof MemosError
          ? err
          : new MemosError(
              ERROR_CODES.EMBEDDING_UNAVAILABLE,
              `legacy embedMany failed: ${err instanceof Error ? err.message : String(err)}`,
            );
        return slice.map(() => ({ ok: false as const, error }));
      }

After:

      } catch (err) {
        // Re-throw abort and lifecycle errors so callers can observe
        // cancellation correctly, consistent with embedMany behaviour.
        if (
          err instanceof DOMException && err.name === "AbortError" ||
          (err instanceof MemosError && err.code === ERROR_CODES.PIPELINE_SHUTDOWN)
        ) {
          throw err;
        }
        const error = err instanceof MemosError
          ? err
          : new MemosError(
              ERROR_CODES.EMBEDDING_UNAVAILABLE,
              `legacy embedMany failed: ${err instanceof Error ? err.message : String(err)}`,
            );
        return slice.map(() => ({ ok: false as const, error }));
      }

17. apps/memos-local-plugin/install.ps1 (L295-L296)

The abbreviation DSH in this comment is unclear and appears to be a typo or OCR/editor artifact. The parallel comment in install.sh uses the correct term devDependency. This comment should be updated to match the intent expressed in install.sh for clarity and consistency.

Suggested fix: replace DSH development peer trees with devDependency peer trees.

💡 Suggested Change

Before:

            # npm 11 still resolves omitted DSH development peer trees unless
            # legacy peer resolution is requested for the packed runtime.

After:

            # npm 11 still resolves omitted devDependency peer trees unless
            # legacy peer resolution is requested for the packed runtime.

18. apps/memos-local-plugin/install.sh (L400)

--legacy-peer-deps disables npm's peer-dependency conflict detection globally for this install. The intent is correct (the @deepseek-ai/* peers are optional and absent from the standalone runtime), but this flag is wider than necessary: it silences all peer-dep conflicts, not just the optional DSH ones. If a future non-optional peer conflict is introduced, it will be silently bypassed here.

A more targeted alternative is to mark every DSH peer as optional in peerDependenciesMeta (which is already done in package.json) and ship an .npmrc in the tarball that sets legacy-peer-deps=true only for that directory, keeping the flag scoped. Alternatively, document that this flag must be reviewed whenever direct dependencies are upgraded, so silent conflicts are not missed.


19. apps/memos-local-plugin/install.sh (L400-L401)

All stdout and stderr from npm install are discarded (>/dev/null 2>&1), and --legacy-peer-deps additionally suppresses peer-dep warnings. The only error signal left is the [[ -d node_modules ]] directory check, which is a very coarse health signal — npm install can exit 0 with a partially-installed tree even without the flag. Consider at minimum capturing stderr to a temp log and emitting it via warn on non-zero exit, so operators have actionable information when the install silently degrades.

💡 Suggested Change

Before:

  ( cd "${prefix}" && PATH="${node_dir}:${PATH}" MEMOS_SKIP_SETUP=1 npm install --omit=dev --legacy-peer-deps --no-fund --no-audit --loglevel=error >/dev/null 2>&1 )
  [[ -d "${prefix}/node_modules" ]] || die "npm install failed in ${prefix}"

After:

  local npm_err
  npm_err="$(mktemp)"
  if ! ( cd "${prefix}" && PATH="${node_dir}:${PATH}" MEMOS_SKIP_SETUP=1 npm install --omit=dev --legacy-peer-deps --no-fund --no-audit --loglevel=error >/dev/null 2>"${npm_err}" ); then
    warn "npm install stderr:"
    cat "${npm_err}" >&2
    rm -f "${npm_err}"
    die "npm install failed in ${prefix}"
  fi
  rm -f "${npm_err}"
  [[ -d "${prefix}/node_modules" ]] || die "npm install failed in ${prefix}"

20. apps/memos-local-plugin/viewer/src/stores/i18n.ts (L891-L892)

The default value 1024 is hard-coded in the user-facing hint string. If the actual default ever changes (e.g. in defaults.ts), this string will silently become out of sync and mislead users. Consider driving the hint text dynamically from the config default, or at minimum add a code comment co-locating the magic number with its source so a future change is harder to miss.


21. apps/memos-local-plugin/viewer/src/views/SettingsView.tsx (L529-L575)

The two new Field blocks are both individually guarded by type === "embedding", causing the condition to be evaluated and written twice. Wrap both fields in a single conditional to keep the structure DRY and reduce drift risk if the condition ever changes.

💡 Suggested Change

Before:

        {type === "embedding" && (
          <Field label={t("settings.embedding.maxInputTokens.label")}>
            <input
              class="input"
              type="number"
              min={0}
              max={1_000_000}
              step={1}
              value={block.maxInputTokens ?? 1_024}
              onInput={(e) =>
                onPatch({
                  maxInputTokens: Math.max(
                    0,
                    Math.floor(Number((e.target as HTMLInputElement).value) || 0),
                  ),
                })}
            />
            <span class="muted" style="font-size:var(--fs-2xs)">
              {t("settings.embedding.maxInputTokens.hint")}
            </span>
          </Field>
        )}
        {type === "embedding" && (
          <Field label={t("settings.embedding.providerBatchSize.label")}>
            <input
              class="input"
              type="number"
              min={1}
              max={256}
              step={1}
              value={block.batchSize ?? 32}
              onInput={(e) =>
                onPatch({
                  batchSize: Math.max(
                    1,
                    Math.min(
                      256,
                      Math.floor(Number((e.target as HTMLInputElement).value) || 1),
                    ),
                  ),
                })}
            />
            <span class="muted" style="font-size:var(--fs-2xs)">
              {t("settings.embedding.providerBatchSize.hint")}
            </span>
          </Field>
        )}

After:

        {type === "embedding" && (
          <>
            <Field label={t("settings.embedding.maxInputTokens.label")}>
              <input
                class="input"
                type="number"
                min={0}
                max={1_000_000}
                step={1}
                value={block.maxInputTokens ?? 1_024}
                onInput={(e) =>
                  onPatch({
                    maxInputTokens: Math.max(
                      0,
                      Math.floor(Number((e.target as HTMLInputElement).value) || 0),
                    ),
                  })}
              />
              <span class="muted" style="font-size:var(--fs-2xs)">
                {t("settings.embedding.maxInputTokens.hint")}
              </span>
            </Field>
            <Field label={t("settings.embedding.providerBatchSize.label")}>
              <input
                class="input"
                type="number"
                min={1}
                max={256}
                step={1}
                value={block.batchSize ?? 32}
                onInput={(e) =>
                  onPatch({
                    batchSize: Math.max(
                      1,
                      Math.min(
                        256,
                        Math.floor(Number((e.target as HTMLInputElement).value) || 1),
                      ),
                    ),
                  })}
              />
              <span class="muted" style="font-size:var(--fs-2xs)">
                {t("settings.embedding.providerBatchSize.hint")}
              </span>
            </Field>
          </>
        )}

22. apps/memos-local-plugin/viewer/src/views/SettingsView.tsx (L537-L544)

The fallback default 1_024 is a magic number duplicated from DEFAULT_CONFIG.maxInputTokens (defined in core/config/defaults.ts). Similarly, batchSize ?? 32 duplicates DEFAULT_CONFIG.batchSize. If the backend defaults ever change, this UI display value will silently diverge. Consider importing or re-exporting these constants from the config layer so the UI stays in sync automatically.

💡 Suggested Change

Before:

              value={block.maxInputTokens ?? 1_024}
              onInput={(e) =>
                onPatch({
                  maxInputTokens: Math.max(
                    0,
                    Math.floor(Number((e.target as HTMLInputElement).value) || 0),
                  ),
                })}

After:

              // Import DEFAULT_MAX_INPUT_TOKENS and DEFAULT_EMBEDDING_BATCH_SIZE from a shared constants module
              value={block.maxInputTokens ?? DEFAULT_MAX_INPUT_TOKENS}
              onInput={(e) =>
                onPatch({
                  maxInputTokens: Math.max(
                    0,
                    Math.floor(Number((e.target as HTMLInputElement).value) || 0),
                  ),
                })}

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch fix-20260820-local-plugin git@github.com:MemTensor/MemOS.git /data/test-workspaces/3c925f108355c92d/repo
Cloning into '/data/test-workspaces/3c925f108355c92d/repo'...
nc: read failed (0/4): Broken pipe
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Branch: fix-20260820-local-plugin

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch fix-20260820-local-plugin git@github.com:MemTensor/MemOS.git /data/test-workspaces/a0819c4b48208c12/repo
Cloning into '/data/test-workspaces/a0819c4b48208c12/repo'...
nc: read failed (0/4): Broken pipe
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Branch: fix-20260820-local-plugin

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] memos-local-plugin: embedding source text exceeds embedding-3's 3072-token limit → HTTP 400 (code:1210), entire batch fails

3 participants