From 4086aab83ec93caaa8751f427d231e7768706c1d Mon Sep 17 00:00:00 2001 From: roboomp Date: Mon, 6 Jul 2026 00:56:44 +0000 Subject: [PATCH] fix(mnemopi): dropped object-shaped fact coercion - Unwrapped extractor fact items from known text fields instead of coercing arbitrary objects with String(). - Dropped unrecognized object entries so derived fact indexes cannot persist literal [object Object] rows. - Added a parseFacts regression covering mixed strings, wrapped objects, and junk timeline objects. Fixes #4649 --- packages/mnemopi/CHANGELOG.md | 4 ++++ packages/mnemopi/src/core/extraction.ts | 17 +++++++++++++++-- packages/mnemopi/test/extraction.test.ts | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/mnemopi/CHANGELOG.md b/packages/mnemopi/CHANGELOG.md index fe4b3f303..132558a8c 100644 --- a/packages/mnemopi/CHANGELOG.md +++ b/packages/mnemopi/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed extractor JSON parsing so object-shaped fact, instruction, preference, and timeline items are unwrapped from known text fields or dropped instead of persisting literal `[object Object]` rows. ([#4649](https://github.com/can1357/oh-my-pi/issues/4649)) + ## [16.3.7] - 2026-07-05 ### Added diff --git a/packages/mnemopi/src/core/extraction.ts b/packages/mnemopi/src/core/extraction.ts index 967c3d06a..9fb47072b 100644 --- a/packages/mnemopi/src/core/extraction.ts +++ b/packages/mnemopi/src/core/extraction.ts @@ -85,6 +85,7 @@ function stripFence(raw: string): string { const FLAT_FACT_LIMIT = 5; const STRUCTURED_CATEGORY_LIMIT = 5; const STRING_CATEGORY_KEYS = ["facts", "instructions", "preferences", "timelines"] as const; +const FACT_TEXT_FIELD_KEYS = ["fact", "text", "content", "value", "statement"] as const; /** Parsed knowledge-graph edge emitted by the extractor LLM. */ export interface ExtractedKgTriple { @@ -118,8 +119,20 @@ function normalizeFactArray(items: unknown): string[] { } const out: string[] = []; for (const item of items) { - if (item !== null && item !== undefined && String(item).trim() !== "") { - const normalized = normalizeFact(String(item)); + let text: string | null = null; + if (typeof item === "string") { + text = item.trim(); + } else if (isRecord(item)) { + for (const key of FACT_TEXT_FIELD_KEYS) { + const candidate = item[key]; + if (typeof candidate === "string" && candidate.trim() !== "") { + text = candidate.trim(); + break; + } + } + } + if (text !== null && text !== "") { + const normalized = normalizeFact(text); if (normalized !== "") { out.push(normalized); if (out.length >= STRUCTURED_CATEGORY_LIMIT) break; diff --git a/packages/mnemopi/test/extraction.test.ts b/packages/mnemopi/test/extraction.test.ts index 02c4aa702..ab58166ae 100644 --- a/packages/mnemopi/test/extraction.test.ts +++ b/packages/mnemopi/test/extraction.test.ts @@ -52,6 +52,22 @@ describe("structured extraction", () => { expect(parseFacts("NO_FACTS")).toEqual([]); }); + it("drops unrecognized object facts instead of stringifying them", () => { + const modelJson = JSON.stringify({ + facts: [{ fact: "The user prefers tabs over spaces" }, { nested: {} }, "The user likes concise replies."], + instructions: [{ text: "Always include verification details" }], + preferences: [{ value: "Prefers dark mode" }], + timelines: [{ subject: "release", predicate: "on", object: "2026-08-01" }], + }); + + expect(parseFacts(modelJson)).toEqual([ + "The user prefers tabs over spaces", + "The user likes concise replies", + "Always include verification details", + "Prefers dark mode", + ]); + }); + it("treats a valid empty structured extraction as no facts", () => { expect(parseFacts('{"facts": [], "instructions": [], "preferences": [], "timelines": [], "kg": []}')).toEqual([]); expect(