Skip to content

Fix #2335: l3.abstraction hard-fails entire world-model generation on empty LLM title (no r - #2338

Open
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.33from
Memtensor-AI:bugfix/autodev-2335-20260903031432962
Open

Fix #2335: l3.abstraction hard-fails entire world-model generation on empty LLM title (no r#2338
Memtensor-AI wants to merge 2 commits into
MemTensor:dev-v2.0.33from
Memtensor-AI:bugfix/autodev-2335-20260903031432962

Conversation

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

Description

Fixes issue #2335: l3.abstraction no longer aborts a whole cluster's world-model generation when the LLM returns an empty title. The validate callback still requires the triple (environment / inference / constraints) but no longer throws on empty title; normaliseDraft synthesises a fallback title from domain_tags (title-cased, joined with " · "), the first non-empty environment[].label, or a static "Untitled world model". abstractDraft emits an abstract.title_fallback warn breadcrumb so ops can still spot LLM regressions.

Applied the same softening to l2.induction: title becomes a display attribute (falls back to signatureLabel, else "Untitled policy") while trigger and procedure remain load-bearing and still fail the draft when missing.

Verified with vitest: the two focused test files run 16 tests (all passing) and the full tests/unit/memory/l2 + l3 suite runs 89 tests (all passing). tsc -p tsconfig.json --noEmit is clean. Changes touch four files under apps/memos-local-plugin/{core,tests}/memory/{l2,l3}; opsp task file and openspec change artefacts are staged in .ai-tasks/ and openspec/changes/ for scheduler archival.

Related Issue (Required): Fixes #2335

Type of change

Please delete options that are not relevant.

  • 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?

Automated tests are pending.

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

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

@whipser030, @hijzy please review this PR.

Reviewer Checklist

…Tensor#2335)

l3.abstraction previously threw LLM_OUTPUT_MALFORMED when the LLM
returned an empty/whitespace `title`, aborting the entire world-model
generation for the cluster. In production (v2.0.17) this turned a
single flaky LLM response into a full pipeline failure.

Soften the validator: keep the triple (environment / inference /
constraints) as load-bearing but derive `title` from `domain_tags`,
first environment label, or a static "Untitled world model" when the
LLM leaves it empty. Emit `abstract.title_fallback` warn so ops still
see the regression.

Apply the same pattern to l2.induction — `title` is a display attribute
while `trigger` and `procedure` remain load-bearing.
@Memtensor-AI Memtensor-AI added ai:generated Generated or modified by AI | 由 AI 生成或修改 area:plugin OpenClaw & Hermes status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Sep 3, 2026
@Memtensor-AI

Memtensor-AI commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Open Code Review

Target: PR #2338
Task: 60cd97aef73c495f
Base: dev-v2.0.33
Head: bugfix/autodev-2335-20260903031432962
Head SHA: ed61fb9928d9f1ac2b8ac159aaaea3c9a286cb2e

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


1. apps/memos-local-plugin/core/memory/l2/induce.ts (L145-L151)

Finding #8 is not fully resolved. The log records rawTitleType but omits the actual rawTitle value. Operators cannot distinguish between null, undefined, "", 0, or an object from the type field alone — typeof null and typeof undefined are different, but typeof null === 'object' makes it impossible to tell null from a plain object without the value. Add rawTitleValue: rawTitle to the log payload so operators can see the exact LLM output.

💡 Suggested Change

Before:

    if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
      log.warn("induce.title_fallback", {
        signatureLabel: input.signatureLabel,
        synthesisedTitle: draft.title,
        rawTitleType: typeof rawTitle,
      });
    }

After:

    if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
      log.warn("induce.title_fallback", {
        signatureLabel: input.signatureLabel,
        synthesisedTitle: draft.title,
        rawTitleType: typeof rawTitle,
        rawTitleValue: rawTitle,
      });
    }

2. apps/memos-local-plugin/core/memory/l3/abstract.ts (L126-L134)

Finding 1 (not fixed): The raw title is re-read from rsp.value after normaliseDraft has already consumed and evaluated it internally. normaliseDraft already reads value.title via sanitizeDerivedText (line 270) and decides whether to call synthesiseTitle. This post-call re-read duplicates that logic and keeps title-emptiness reasoning split across two call sites. A cleaner fix is to have normaliseDraft return a flag (e.g., a { draft, titleWasSynthesised } tuple) or to accept a callback so the warning can be emitted from within the single place that has all the information.

💡 Suggested Change

Before:

    const draft = normaliseDraft(rsp.value);
    const rawTitle = (rsp.value as { title?: unknown }).title;
    if (typeof rawTitle !== "string" || rawTitle.trim().length === 0) {
      log.warn("abstract.title_fallback", {
        clusterKey: input.cluster.key,
        synthesisedTitle: draft.title,
        rawTitleType: typeof rawTitle,
      });
    }

After:

    const { draft, titleWasSynthesised } = normaliseDraft(rsp.value);
    if (titleWasSynthesised) {
      log.warn("abstract.title_fallback", {
        clusterKey: input.cluster.key,
        synthesisedTitle: draft.title,
      });
    }

3. apps/memos-local-plugin/core/memory/l3/abstract.ts (L129-L133)

Finding 8 (not fixed): The warning log records rawTitleType but omits the raw LLM value itself. Operators cannot distinguish null, undefined, "", or a whitespace-only string from this log entry alone — typeof is identical ("object" for null vs "string" for the empty/whitespace cases). Add rawTitle (or a safe string representation of it) to the log payload.

💡 Suggested Change

Before:

log.warn("abstract.title_fallback", {
        clusterKey: input.cluster.key,
        synthesisedTitle: draft.title,
        rawTitleType: typeof rawTitle,
      });

After:

      log.warn("induce.title_fallback", {
        signatureLabel: input.signatureLabel,
        synthesisedTitle: draft.title,
        rawTitleType: typeof rawTitle,
        rawTitle: rawTitle === null ? "null" : rawTitle === undefined ? "undefined" : String(rawTitle).slice(0, 80),
      });

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

🔧 Open Code Review requested Agent fix

Open Code Review found 8 issue(s). I have resumed the development Agent to fix them.

  • Task: 60cd97aef73c495f
  • Fix attempt: 1/2
  • Finding delta: 0 repeated / 8 new / 0 likely resolved

The Agent will push a new commit to this PR branch. OCR will recheck after the commit is pushed.

- Inline the raw-title check in l3.abstract (parity with l2.induce);
  drop the now-unused isEmptyString helper.
- Add rawTitleType to the induce/abstract title_fallback warn logs so
  operators can distinguish null vs. "" vs. non-string LLM responses.
- Route l2.induce.synthesiseTitle through sanitizeDerivedText + the
  shared truncate() helper so the fallback path matches the normal
  sanitised/ellipsised path.
- Make l3.abstract's titleCaseTag and synthesiseTitle Unicode-safe by
  iterating over code points instead of UTF-16 code units, so emoji or
  surrogate-pair tags no longer corrupt the display title.
- Drop the outdated docs/openspec/…-2335-….md pointer from the
  synthesiseTitle JSDoc — the referenced path never landed.
@Memtensor-AI

Copy link
Copy Markdown
Collaborator Author

✅ Automated Test Results: PASSED

All tests passed (16/16 executed). memos_local_plugin/unit: 16/16. Duration: 3s [advisory, non-gating] AI-generated tests on branch test/auto-gen-60cd97aef73c495f-20260903115355: 56/61 passed, 5 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: bugfix/autodev-2335-20260903031432962

@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 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai:generated Generated or modified by AI | 由 AI 生成或修改 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.

2 participants