Skip to content

Cortex: KNOWLEDGE filename writers destroy accented letters instead of transliterating them, and disagree with each other #2131

Description

@drpaulobernardy

Version

LifeOS 7.40.4, and main @ 5e2f2e8 (2026-09-03). The three functions below are identical in both.

What is broken

Every writer that turns a note title into a KNOWLEDGE filename keeps only [a-z0-9], so any accented letter is destroyed instead of transliterated. For a principal who writes in Portuguese, Spanish, French or German, most automatic notes get unreadable filenames:

  • MemoryTypes.ts slugify (used by MemoryReviewerMemorySystem.add for every idea / knowledge note) replaces the letter with a hyphen: "Ação e reação" → a-o-e-rea-o.
  • KnowledgeHarvester.ts toKebabCase and HarvestExecutor.ts kebabCase delete the letter: "Ação e reação" → ao-e-reao.

Two knock-on effects:

  • The same title becomes different slugs in different writers (a-o-e-rea-o vs ao-e-reao), and neither matches the ASCII name a human or the DA naturally writes by hand following skills/Cortex/SKILL.md ("add" step 3). A related: slug or [[acao-e-reacao]] link does not reach a-o-e-rea-o.md: KnowledgeGraph.ts resolveSlug (:358) tries exact match, then substring containment, and both fail.
  • MemorySystem.add only appends to an existing note when the resolved path matches exactly, so a title written once with and once without the accent lands in two separate notes.

Where (file:line)

LIFEOS/TOOLS/MemoryTypes.ts:311-314 (slugify) · LIFEOS/TOOLS/KnowledgeHarvester.ts:540-548 (toKebabCase) · LIFEOS/TOOLS/HarvestExecutor.ts:765-773 (kebabCase) · skills/Cortex/SKILL.md ("add" step 3 and "ingest" slug rule, which say kebab-case but not ASCII)

Repro on a clean tree

git clone --depth 1 https://github.com/danielmiessler/LifeOS && cd LifeOS/LifeOS/install
cat > slug-repro.ts <<'EOF'
import { readFileSync } from "node:fs";
import { basename } from "node:path";
import { resolveStoragePath } from "./LIFEOS/TOOLS/MemoryTypes.ts";

// toKebabCase / kebabCase are not exported, so evaluate the shipped function bodies verbatim.
function extract(file: string, name: string): (s: string) => string {
  const m = readFileSync(file, "utf8").match(new RegExp(`function ${name}\\((\\w+): string\\): string \\{([\\s\\S]*?)\\n\\}`));
  if (!m) throw new Error(`${name} not found in ${file}`);
  return new Function(m[1], m[2]) as (s: string) => string;
}

const writers: [string, (s: string) => string][] = [
  ["MemoryTypes slugify (idea)", (s) => basename(resolveStoragePath({ type: "idea", title: s, content: "x" } as any), ".md")],
  ["KnowledgeHarvester toKebabCase", extract("LIFEOS/TOOLS/KnowledgeHarvester.ts", "toKebabCase")],
  ["HarvestExecutor kebabCase", extract("LIFEOS/TOOLS/HarvestExecutor.ts", "kebabCase")],
];
const titles = ["Ação e reação", "Café com pão", "Über naïve résumé", "Prompt Injection"];

for (const [label, fn] of writers) {
  console.log(label);
  for (const t of titles) console.log(`  ${t} → ${fn(t)}`);
}
EOF
bun slug-repro.ts

Negative control

Output on unpatched main @ 5e2f2e8:

MemoryTypes slugify (idea)
  Ação e reação → a-o-e-rea-o
  Café com pão → caf-com-p-o
  Über naïve résumé → ber-na-ve-r-sum
  Prompt Injection → prompt-injection
KnowledgeHarvester toKebabCase
  Ação e reação → ao-e-reao
  Café com pão → caf-com-po
  Über naïve résumé → ber-nave-rsum
  Prompt Injection → prompt-injection
HarvestExecutor kebabCase
  Ação e reação → ao-e-reao
  Café com pão → caf-com-po
  Über naïve résumé → ber-nave-rsum
  Prompt Injection → prompt-injection

Same script after the patch below (ASCII titles unchanged):

MemoryTypes slugify (idea)
  Ação e reação → acao-e-reacao
  Café com pão → cafe-com-pao
  Über naïve résumé → uber-naive-resume
  Prompt Injection → prompt-injection
KnowledgeHarvester toKebabCase
  Ação e reação → acao-e-reacao
  Café com pão → cafe-com-pao
  Über naïve résumé → uber-naive-resume
  Prompt Injection → prompt-injection
HarvestExecutor kebabCase
  Ação e reação → acao-e-reacao
  Café com pão → cafe-com-pao
  Über naïve résumé → uber-naive-resume
  Prompt Injection → prompt-injection

Suggested fix

Transliterate before filtering: NFKD-normalize and drop combining marks (\p{M}). Filenames stay ASCII kebab-case, which also avoids the NFC/NFD split where two visually identical names are different bytes on disk. For pure-ASCII input, NFKD is the identity, so existing slugs do not move. Applied to a local install, a 24-case ASCII comparison before/after showed 0 differences, the three files build, and bun MemoryReviewer.ts test passes.

--- a/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/MemoryTypes.ts
@@ -310,7 +310,8 @@
 const SLUG_RE = /[^a-z0-9]+/g;
 function slugify(s: string): string {
-  return s.trim().toLowerCase().replace(SLUG_RE, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "untitled";
+  // Strip diacritics first so "ação" → "acao", not "a-o".
+  return s.trim().toLowerCase().normalize("NFKD").replace(/\p{M}/gu, "").replace(SLUG_RE, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "untitled";
 }
--- a/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/KnowledgeHarvester.ts
@@ -540,6 +540,8 @@
 function toKebabCase(str: string): string {
   return str
     .toLowerCase()
+    .normalize("NFKD")
+    .replace(/\p{M}/gu, "") // strip diacritics: "ação" → "acao", not "ao"
     .replace(/[^a-z0-9\s-]/g, "")
--- a/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts
+++ b/LifeOS/install/LIFEOS/TOOLS/HarvestExecutor.ts
@@ -765,6 +765,8 @@
 function kebabCase(value: string): string {
   return value
     .toLowerCase()
+    .normalize("NFKD")
+    .replace(/\p{M}/gu, "") // strip diacritics: "ação" → "acao", not "ao"
     .replace(/[^a-z0-9\s-]/g, "")

And in skills/Cortex/SKILL.md, the manual "add" and "ingest" slug steps can say "ASCII only: strip diacritics but keep the letter", so hand-written names match the code writers.

Out of scope, noted only: toKebabCase and kebabCase return an empty string for a title with no Latin letters (e.g. "日本語" or "!!!"), unlike slugify, which falls back to untitled. That behavior is unchanged by this patch.

Before submitting

  • I searched open and closed issues for this defect.
  • The repro runs against a clean tree of the version above, not against my modified install.
  • I removed personal data from the pasted output — real names, absolute home paths, tokens, my own content.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions