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 MemoryReviewer → MemorySystem.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
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.tsslugify(used byMemoryReviewer→MemorySystem.addfor everyidea/knowledgenote) replaces the letter with a hyphen: "Ação e reação" →a-o-e-rea-o.KnowledgeHarvester.tstoKebabCaseandHarvestExecutor.tskebabCasedelete the letter: "Ação e reação" →ao-e-reao.Two knock-on effects:
a-o-e-rea-ovsao-e-reao), and neither matches the ASCII name a human or the DA naturally writes by hand followingskills/Cortex/SKILL.md("add" step 3). Arelated:slug or[[acao-e-reacao]]link does not reacha-o-e-rea-o.md:KnowledgeGraph.tsresolveSlug(:358) tries exact match, then substring containment, and both fail.MemorySystem.addonly 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
Negative control
Output on unpatched
main@ 5e2f2e8:Same script after the patch below (ASCII titles unchanged):
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, andbun MemoryReviewer.ts testpasses.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:
toKebabCaseandkebabCasereturn an empty string for a title with no Latin letters (e.g. "日本語" or "!!!"), unlikeslugify, which falls back tountitled. That behavior is unchanged by this patch.Before submitting