From 89be5ae1e652a2be3108ea490594c315022121f0 Mon Sep 17 00:00:00 2001 From: bm-clawd Date: Thu, 26 Feb 2026 15:18:57 -0600 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20conversation=20capture=20=E2=80=94?= =?UTF-8?q?=20catch=20any=20editNote=20error,=20fall=20through=20to=20crea?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous code matched specific error types (isNoteNotFoundError) to decide whether to fall back to writeNote. The error format from callTool didn't match, so writeNote never fired — zero conversations captured. Fix: try append, if it fails for ANY reason, create the note. Also adds Conversation schema frontmatter (type: Conversation, date) to created notes. Fixes #22 --- bm-client.ts | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/bm-client.ts b/bm-client.ts index 95e5eeb..5a6180f 100644 --- a/bm-client.ts +++ b/bm-client.ts @@ -855,22 +855,33 @@ export class BmClient { "---", ].join("\n") + // Try append first — if it fails for ANY reason, fall through to create try { await this.editNote(title, "append", entry) log.debug(`appended conversation to: ${title}`) + return } catch (err) { - if (!this.isNoteNotFoundError(err)) { - log.error("conversation append failed", err) - return - } + log.debug(`append failed, will create: ${getErrorMessage(err)}`) + } - const content = [`# Conversations ${dateStr}`, "", entry].join("\n") - try { - await this.writeNote(title, content, "conversations") - log.debug(`created conversation note: ${title}`) - } catch (createErr) { - log.error("conversation index failed", createErr) - } + // Create the note with frontmatter and first entry + const content = [ + "---", + `title: Conversations ${dateStr}`, + "type: Conversation", + `date: "${dateStr}"`, + "---", + "", + `# Conversations ${dateStr}`, + "", + entry, + ].join("\n") + + try { + await this.writeNote(title, content, "conversations") + log.debug(`created conversation note: ${title}`) + } catch (err) { + log.error("conversation index failed", err) } } From a87a658d173e391822f8531ec2f67442a06fcf9b Mon Sep 17 00:00:00 2001 From: phernandez Date: Thu, 26 Feb 2026 19:06:07 -0600 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20lint=20=E2=80=94=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- benchmark/convert-locomo.ts | 73 ++++++++++++++++++++++++++----------- benchmark/run.ts | 10 +++-- bm-client.ts | 6 +-- index.ts | 6 ++- 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/benchmark/convert-locomo.ts b/benchmark/convert-locomo.ts index 692d47f..9d00d3d 100644 --- a/benchmark/convert-locomo.ts +++ b/benchmark/convert-locomo.ts @@ -78,15 +78,24 @@ function parseDateTime(dateStr: string): { date: string; time: string } | null { ) if (!match) return null - let [, hour, min, ampm, day, month, year] = match + const [, hour, min, ampm, day, month, year] = match let h = Number.parseInt(hour) if (ampm.toLowerCase() === "pm" && h !== 12) h += 12 if (ampm.toLowerCase() === "am" && h === 12) h = 0 const months: Record = { - January: "01", February: "02", March: "03", April: "04", - May: "05", June: "06", July: "07", August: "08", - September: "09", October: "10", November: "11", December: "12", + January: "01", + February: "02", + March: "03", + April: "04", + May: "05", + June: "06", + July: "07", + August: "08", + September: "09", + October: "10", + November: "11", + December: "12", } const m = months[month] @@ -138,7 +147,10 @@ type: Person - [role] Conversation participant - [relationship] Regularly chats with ${speakerB} ` - files.set(`people/${speakerA.toLowerCase().replace(/\s+/g, "-")}.md`, speakerANote) + files.set( + `people/${speakerA.toLowerCase().replace(/\s+/g, "-")}.md`, + speakerANote, + ) const speakerBNote = `--- title: ${speakerB} @@ -151,16 +163,19 @@ type: Person - [role] Conversation participant - [relationship] Regularly chats with ${speakerA} ` - files.set(`people/${speakerB.toLowerCase().replace(/\s+/g, "-")}.md`, speakerBNote) + files.set( + `people/${speakerB.toLowerCase().replace(/\s+/g, "-")}.md`, + speakerBNote, + ) // Build a MEMORY.md with key facts that accumulate - let memoryLines: string[] = [ - `# Long-Term Memory`, + const memoryLines: string[] = [ + "# Long-Term Memory", "", - `## People`, + "## People", `- ${speakerA} and ${speakerB} are close friends who chat regularly`, "", - `## Key Events`, + "## Key Events", ] // Convert each session to a dated note @@ -170,7 +185,8 @@ type: Person const dateTimeStr = c[`${sessionKey}_date_time`] const parsed = dateTimeStr ? parseDateTime(dateTimeStr) : null - const date = parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}` + const date = + parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}` const time = parsed?.time || "12:00" // Get session summary and observations if available @@ -184,7 +200,8 @@ type: Person if (Array.isArray(obs)) { for (const item of obs) { const text = Array.isArray(item) ? item[0] : item - if (typeof text === "string") lines.push(`- [${speaker.toLowerCase()}] ${text}`) + if (typeof text === "string") + lines.push(`- [${speaker.toLowerCase()}] ${text}`) } } } @@ -213,20 +230,21 @@ date: ${date} } // Add conversation - content += `## Conversation\n` + content += "## Conversation\n" for (const turn of turns) { const text = turn.text.replace(/\n/g, "\n> ") content += `**${turn.speaker}:** ${text}\n\n` } // Add relations - content += `## Relations\n` + content += "## Relations\n" content += `- mentions [[${speakerA}]]\n` content += `- mentions [[${speakerB}]]\n` // Add to memory summary if (observation) { - const firstObs = observation.split("\n")[0]?.replace(/^- \[\w+\] /, "") || "" + const firstObs = + observation.split("\n")[0]?.replace(/^- \[\w+\] /, "") || "" if (firstObs) memoryLines.push(`- [${date}] ${firstObs}`) } @@ -234,7 +252,7 @@ date: ${date} } // Write MEMORY.md - files.set("MEMORY.md", memoryLines.join("\n") + "\n") + files.set("MEMORY.md", `${memoryLines.join("\n")}\n`) // Convert QA to benchmark queries const queries: BenchmarkQuery[] = [] @@ -253,7 +271,8 @@ date: ${date} // Find the session's date const dateTimeStr = c[`session_${sessionNum}_date_time`] const parsed = dateTimeStr ? parseDateTime(dateTimeStr) : null - const date = parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}` + const date = + parsed?.date || `2023-01-${String(sessionNum).padStart(2, "0")}` groundTruth.add(`conversations/${date}-session-${sessionNum}.md`) } @@ -266,8 +285,14 @@ date: ${date} query: qa.question, category, ground_truth: [...groundTruth], - expected_content: isAdversarial ? undefined : answer.length < 100 ? answer : undefined, - note: isAdversarial ? `Adversarial: correct answer is "${answer}"` : undefined, + expected_content: isAdversarial + ? undefined + : answer.length < 100 + ? answer + : undefined, + note: isAdversarial + ? `Adversarial: correct answer is "${answer}"` + : undefined, }) } @@ -303,7 +328,9 @@ async function main() { const convDir = `corpus-locomo/conv-${idx}` const outDir = resolve(BENCHMARK_DIR, convDir) - console.log(`\nConverting conversation ${idx} (${conv.conversation.speaker_a} & ${conv.conversation.speaker_b})...`) + console.log( + `\nConverting conversation ${idx} (${conv.conversation.speaker_a} & ${conv.conversation.speaker_b})...`, + ) const { files, queries } = convertConversation(conv, idx) @@ -332,8 +359,10 @@ async function main() { } } - console.log(`\nāœ… Total: ${totalFiles} files, ${totalQueries} queries across ${indices.length} conversations`) - console.log(` Output: benchmark/corpus-locomo/`) + console.log( + `\nāœ… Total: ${totalFiles} files, ${totalQueries} queries across ${indices.length} conversations`, + ) + console.log(" Output: benchmark/corpus-locomo/") } main().catch((err) => { diff --git a/benchmark/run.ts b/benchmark/run.ts index 7844e03..ff03f91 100644 --- a/benchmark/run.ts +++ b/benchmark/run.ts @@ -87,13 +87,15 @@ const RESULTS_DIR = resolve(BENCHMARK_DIR, "results") const CORPUS_SIZE = process.argv.find((a) => a.startsWith("--corpus="))?.split("=")[1] || "small" const BM_PROJECT = - process.argv.find((a) => a.startsWith("--project="))?.split("=")[1] || "benchmark" + process.argv.find((a) => a.startsWith("--project="))?.split("=")[1] || + "benchmark" const QUERIES_PATH = process.argv.find((a) => a.startsWith("--queries="))?.split("=")[1] || resolve(BENCHMARK_DIR, "queries.json") -const QUERY_LIMIT = Number.parseInt( - process.argv.find((a) => a.startsWith("--limit="))?.split("=")[1] || "0", -) || 0 +const QUERY_LIMIT = + Number.parseInt( + process.argv.find((a) => a.startsWith("--limit="))?.split("=")[1] || "0", + ) || 0 // --------------------------------------------------------------------------- // MCP Client diff --git a/bm-client.ts b/bm-client.ts index 5a6180f..059e0d2 100644 --- a/bm-client.ts +++ b/bm-client.ts @@ -196,7 +196,7 @@ function isRecoverableConnectionError(err: unknown): boolean { ) } -function isNoteNotFoundError(err: unknown): boolean { +function _isNoteNotFoundError(err: unknown): boolean { const msg = getErrorMessage(err).toLowerCase() return ( msg.includes("entity not found") || @@ -830,10 +830,6 @@ export class BmClient { return payload as unknown as MetadataSearchResult } - private isNoteNotFoundError(err: unknown): boolean { - return isNoteNotFoundError(err) - } - async indexConversation( userMessage: string, assistantResponse: string, diff --git a/index.ts b/index.ts index a38782c..220a019 100644 --- a/index.ts +++ b/index.ts @@ -103,7 +103,9 @@ export default { 'uv tool install "basic-memory @ git+https://github.com/basicmachines-co/basic-memory.git@main" --force', { encoding: "utf-8", timeout: 120_000, stdio: "pipe" }, ) - log.info(`basic-memory installed: ${result.trim().split("\n").pop()}`) + log.info( + `basic-memory installed: ${result.trim().split("\n").pop()}`, + ) // Verify it worked try { execSync(`command -v ${bmBin}`, { stdio: "ignore" }) @@ -113,7 +115,7 @@ export default { "bm installed but not found on PATH. You may need to add uv's bin directory to your PATH (typically ~/.local/bin).", ) } - } catch (uvErr) { + } catch (_uvErr) { log.error( "Cannot auto-install basic-memory: uv not found. " + "Install uv first (brew install uv, or curl -LsSf https://astral.sh/uv/install.sh | sh), " + From ca51efe8fa027d1075aa4ff4b9e352ca62332b23 Mon Sep 17 00:00:00 2001 From: phernandez Date: Thu, 26 Feb 2026 19:28:50 -0600 Subject: [PATCH 3/3] fix: update test to match fall-through-on-any-error behavior The indexConversation method now falls through to writeNote on any editNote error, not just not-found errors. Updated test to expect writeNote to be called on validation errors. Co-Authored-By: Claude Opus 4.6 --- bm-client.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bm-client.test.ts b/bm-client.test.ts index 04ac363..a088fb6 100644 --- a/bm-client.test.ts +++ b/bm-client.test.ts @@ -523,18 +523,23 @@ describe("BmClient MCP behavior", () => { expect(result.file_path).toBe("archive/my-note.md") }) - it("indexConversation does not create fallback note on non-not-found edit errors", async () => { + it("indexConversation falls through to writeNote on any editNote error", async () => { ;(client as any).editNote = jest .fn() .mockRejectedValue(new Error("validation failed")) - ;(client as any).writeNote = jest.fn() + ;(client as any).writeNote = jest.fn().mockResolvedValue({ + title: "conversations", + permalink: "conversations", + content: "x", + file_path: "conversations/x.md", + }) await client.indexConversation( "user message long enough", "assistant reply long enough", ) - expect((client as any).writeNote).not.toHaveBeenCalled() + expect((client as any).writeNote).toHaveBeenCalledTimes(1) }) it("indexConversation creates fallback note only on note-not-found errors", async () => {