From 16b3f6605303e62e99cdee9f35b9ceeca04260a7 Mon Sep 17 00:00:00 2001 From: Da7-Tech <286182457+Da7-Tech@users.noreply.github.com> Date: Fri, 10 Jul 2026 09:53:42 +0300 Subject: [PATCH] fix: harden persistence, concurrency, and memory export --- .github/workflows/ci.yml | 4 +- CHANGELOG.md | 40 ++ README.ar.md | 60 +-- README.md | 102 ++--- SECURITY.md | 35 +- SKILL.md | 21 +- assets/why-not-just-use.jpeg | Bin 137424 -> 0 bytes bench/discrim.py | 11 +- bench/multilang.py | 28 +- bench/soak.py | 2 + docs/DESIGN.md | 13 +- mind.py | 719 +++++++++++++++++++++++------------ tests/test_mind.py | 387 ++++++++++++++++++- 13 files changed, 1043 insertions(+), 379 deletions(-) delete mode 100644 assets/why-not-just-use.jpeg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c186316..90d2c99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: env: PYTHONUTF8: "1" steps: - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: ${{ matrix.python }} - run: python3 -m unittest discover -s tests -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 535637e..7427bc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # Changelog +## 6.2.8 — 2026-07-10 + +Exhaustive line-by-line audit of code, tests, benchmarks, documentation, +release metadata, and the rendered presentation. Every item below was +reproduced against 6.2.7 before the fix: + +- **Concurrent edges no longer lose data.** Save merging is now per + directional edge pair instead of whole adjacency maps; concurrent links + remain symmetric, stale unrelated writers cannot erase a fresh edge boost, + reinforcement merges as a delta, and daily edge decay is decided inside the + graph lock so concurrent dreams decay only once. +- **Atomic writes reject truncation.** Unique O_EXCL temporary files replace + predictable per-pid names, short `os.write` calls are completed instead of + silently replacing the graph with partial JSON, failed writes clean up their + temporary file, and append records reject partial syscalls. +- **Agent export treats memories as data.** Multiline memories are collapsed, + guard markers are escaped, and the hot section explicitly forbids executing + directives found inside remembered text. The standing contract now forbids + secrets, credentials, private personal data, and untrusted prompt text. +- **Provenance is complete for the queried id.** `link` belongs to both + endpoints, `why ` streams the full journal beyond the 10 MB status tail, + and journal reads refuse symlinks. +- **Hostile live/on-disk state is repaired consistently.** Structurally invalid + live graphs are quarantined before save; malformed created/access timestamps, + history entries, loaded control characters, edge metadata, and oversized + actor/session environment values are repaired or bounded. +- **Determinism and command behavior tightened.** Recall ties have explicit + id tie-breakers; hot-memory weight ties prefer confirmed and recently used + facts; duplicate confirm ids count once; `link` validates both endpoints + before either can persist; `link`/`confirm` refresh agent files; help and + usage errors carry the real script path and reject extra args. +- **Presentation made defensible.** The stale competitor screenshot/checklist + is replaced by a neutral scope comparison based on current official docs; + soak output is reported as the measured 7/8 hot slots and gated at >=7; + short-token, working-memory-size, fuzz, privacy, and dream-phase wording now + match the implementation. Benchmark temp directories are cleaned. +- GitHub Actions pins updated to checkout v7.0.0 and setup-python v6.3.0. + 213 tests. Mutation kill rate: 39% on the seeded 120-mutant sample. + + ## 6.2.7 — 2026-07-06 - **Windows: agent-file reads retry transient sharing violations** — the diff --git a/README.ar.md b/README.ar.md index dfe0890..4d86b94 100644 --- a/README.ar.md +++ b/README.ar.md @@ -16,8 +16,8 @@ كودكس، كرسور، زد)، و`CLAUDE.md` (كلاود كود)، و`GEMINI.md` (جيمناي) — وتُتبنّى تلقائيًّا في `.cursorrules` و`.windsurfrules` و`.clinerules` و`.roo/rules/mind.md` للمشاريع التي تستخدم تلك الأدوات أصلًا. ```bash -curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.7/mind.py -python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='c55594585f8d1dbb944e3ec56b57d6e36dc7c639ceeb0f5621f19ce45c66451b',h;print('mind.py: OK')" +curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.8/mind.py +python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='b3e944ce9103bd5e353e09a12f434721cb274bc919326361039104889c6c5e03',h;print('mind.py: OK')" python3 mind.py init python3 mind.py remember "قاعدة بيانات المشروع بوستغرس ١٦" python3 mind.py recall "ما قاعدة البيانات" @@ -28,13 +28,14 @@ python3 mind.py dream # بين الجلسات: نسيان وتوطيد و ## أرقام مقيسة لا كلام -كل رقم أدناه من `python3 bench/bench.py` — أعد تشغيله بنفسك +الجدول الأول من `python3 bench/bench.py`، وكل قياس لاحق يسمّي أمره القابل +لإعادة الإنتاج — أعد تشغيلها بنفسك (٢٠ استعلامًا ثنائي اللغة بإجابات معلومة ضد رسوم مليئة بالمشتتات؛ الكمون يعتمد على بيئتك — أعد القياس على عتادك): | حجم الرسم | دقة أول نتيجة | دقة أول ٥ | الكمون الوسيط | الشريحة ٩٥ | |---|---|---|---|---| -| ١٠٠ عقدة | **١٫٠٠** | ١٫٠٠ | **~٠٫٥–٠٫٧ م.ث** | ~٢–٤ م.ث | -| ١٠٠٠ عقدة | **١٫٠٠** | ١٫٠٠ | ~٢–٣ م.ث | ~١١–١٦ م.ث | +| ١٠٠ عقدة | **١٫٠٠** | ١٫٠٠ | **~٠٫٨ م.ث** | ~٢٫٧–٣٫٠ م.ث | +| ١٠٠٠ عقدة | **١٫٠٠** | ١٫٠٠ | ~٣٫٣–٣٫٩ م.ث | ~١٣٫٧–١٥٫٥ م.ث | (فجوة ٠٫٩٥ التاريخية — سؤال بالفئة «ما إطار css» عن ذكرى لا تذكر إلا «tailwind» — أغلقتها بذرة المفاهيم في ٥٫٥٫٠؛ الترادف خارج البذرة والنص ما يزال حدًّا موثقًا أدناه.) @@ -43,37 +44,35 @@ python3 mind.py dream # بين الجلسات: نسيان وتوطيد و **اختبار معمّر ١٨٠ يومًا** (`bench/soak.py` — الكود الحقيقي بساعة محقونة وعبء عمل واقعي: حقائق يومية/أسبوعية/شهرية + ٣٥٧ ملاحظة قمامة + حلم كل ليلة): بقاء الحقائق الجوهرية **١٥/١٥** في كل الإيقاعات، بقاء القمامة الأقدم من -نافذة السماح **٠/٢٥٦**، الرسم منضبط الحجم، والاسترجاع على رسم معمّر ٠٫٣٧ م.ث. +نافذة السماح **٠/٢٥٦**، و**٧/٨** خانات الذاكرة الساخنة حقائق جوهرية، +والرسم منضبط الحجم، والاسترجاع على رسم معمّر نحو ٠٫٤–٠٫٥ م.ث. هذا الاختبار اصطاد عيبَي معايرة حقيقيين قبل النشر وأُصلحا باختبارات انحدار. -الاستعلام الوحيد الفاشل في المقياس قصورٌ موثَّق في قسم الحدود، لا مخفي. **متعددة اللغات بالقياس** (`bench/multilang.py`، يعمل في CI): الأداة مهندَسة للعربية والإنجليزية، لكن نواتها محايدة لغويًّا، و٥٫٦٫٠ أضافت فهرسة ثنائيات الحروف للنصوص عديمة المسافات. دقة أول نتيجة على ٨ لغات لم تُضبط لها قط (مع مشتتات): الفرنسية والألمانية والإسبانية والروسية والتركية والصينية واليابانية والكورية — **٣/٣ لكل لغة (٢٤/٢٤)**. (الصينية واليابانية كانتا ٣/٦ قبل ثنائيات الحروف؛ ٣ استعلامات للغة مقياس دخانيّ لا كحزمة العربية/الإنجليزية العشرين، والتصريف الثقيل بلا تجذير سيفوّت حالات لا يغطيها.) **التمييز بالقياس** (`bench/discrim.py`، يعمل في CI): تدقيقان مستقلان أصابا حين قالا إن استرجاع الإبرة من ضجيج نظيف لا يقيس التفريق بين حقائق تتشارك المفردات — فهذا المقياس مشتِّتاته كلها متنافسة معجميًّا، وفيه حالات فشل التدقيقَين حرفيًّا («ما اسمي» ضد «اسم الملف يجب أن يطابق اسم الصنف» بالعربية والإنجليزية). النتيجة الحالية: **١٢/١٢** ببوابة ≥٠٫٨٥ في CI كي لا يتراجع التمييز صامتًا مجددًا. -الاختبارات: **١٩٠ اختبارًا** بمكتبة بايثون القياسية وحدها + مشوّش (٤٢٠ حالة كاملة/١٦٠ في CI) + مطفّر. +الاختبارات: **٢١٣ اختبارًا** بمكتبة بايثون القياسية وحدها + مشوّش (٤٢٠ حالة كاملة/١٦٠ في CI) + مطفّر بنسبة قتل **٣٩٪** على العينة المبذورة ذات ١٢٠ طفرة. -## لماذا لا تستخدم ___؟ +## موضعها بين الأدوات — وظائف مختلفة لا لوحة فائزين -| | استرجاع انتشاري | توطيد نومي | صلاحية زمنية | سجل مصدر | أي وكيل | صفر إعداد | توطيد بلا كلفة | -|---|---|---|---|---|---|---|---| -| mem0 | ✗ | ✗ | ✗ | ~ | ✗ | ✗ مفاتيح LLM | ✗ | -| Letta | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ منصة كاملة | ✗ يحرق توكنات | -| Zep / Graphiti | ~ | ✗ | ✓ ثنائي-الزمن (قوّته الأصيلة) | ✓ | ✗ | ✗ Neo4j+مفاتيح | ✗ | -| HippoRAG 2 | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ GPU/API | ✗ | -| OpenClaw dreams | ✗ | ✓ | ✗ | ✗ | ✗ داخل أوبن كلو فقط | ✓ | ✗ يحرق توكنات | -| claude-mem | ✗ | ✗ | ✗ | ~ | ~ | ✗ | ✗ | -| **mind** | **✓** | **✓ حتمي** | **✓ valid-time + ‎--at** | **✓ يومية إلحاقية + why** | **✓** | **✓ ملف واحد** | **✓** | +| المشروع | نطاقه الأساسي | خيار `mind` الأضيق | +|---|---|---| +| [Mem0](https://docs.mem0.ai/open-source/overview) | طبقة ذاكرة قابلة للتهيئة كمكتبة أو خادم ذاتي | ملف قياسي واحد بلا نموذج أو تضمينات أو مخزن متجهات | +| [Letta](https://docs.letta.com/letta-agent/memory/) | بيئة وكيل بذاكرة قابلة للتحرير وMemFS ووكلاء نوم | ذاكرة مشروع محايدة عن بيئة الوكيل عبر ملفات القواعد | +| [Graphiti](https://help.getzep.com/graphiti/getting-started/overview) | إطار رسم معرفة زمني باسترجاع هجين | رسم محلي صغير بصلاحية زمنية وJSON واضح | +| [HippoRAG 2](https://github.com/OSU-NLP-Group/HippoRAG) | إطار RAG/ذاكرة بحثي لاسترجاع معرفة واسع | ذاكرة تشغيلية لمئات أو آلاف حقائق المشروع | -إحقاقًا للحق: نموذج Graphiti ثنائيّ-الزمن هو المرجع في هذا الباب — طبقة الصلاحية في mind هي الأخذة الحتمية صفرية-التبعيات من الفكرة نفسها، لا ادعاء تفوق عليه. +نموذج Graphiti الزمني مرجع مهم في هذا الباب؛ طبقة الصلاحية في `mind` قراءة +حتمية صفرية التبعيات للفكرة، لا ادعاء تفوق عليه. -**مُشوَّشة** (`bench/fuzz.py`، مبذور حتمي، يعمل في CI): ٤٢٠ حالة عدائية — ملفات رسم معطوبة (NaN/لانهاية/أنواع خاطئة/محارف تحكم/JSON مبتور) ومدخلات أوامر عدائية؛ العقد: لا انهيار، لا فقد بيانات، والرسم يُحمَّل نظيفًا دائمًا. أول تشغيل للمشوّش اصطاد عيبًا حقيقيًّا فاتته ست جولات تدقيق سابقة. +**مُشوَّشة** (`bench/fuzz.py`، مبذور حتمي، يعمل في CI): ٤٢٠ حالة عدائية — ملفات رسم معطوبة (NaN/لانهاية/أنواع خاطئة/محارف تحكم/JSON مبتور) ومدخلات أوامر عدائية؛ العقد: لا traceback، والمدخل التالف يُصلح أو يُعزل، والرسم الناتج يُحمَّل نظيفًا ويقبل كتابة جديدة. أول تشغيل للمشوّش اصطاد عيبًا حقيقيًّا فاتته ست جولات تدقيق سابقة. -**مُختبَرة بالطفرات** (`bench/mutate.py`): تُحقن عيوب من الدرجة الأولى (قلب مقارنات، كسر حساب، دفع ثوابت) ويجب أن تصطادها الحزمة؛ أول تشغيل كشف ١٧ سلوكًا لم تكن الاختبارات تعضّ عليه — كلٌّ مثبَّت الآن باختبار مخصص. +**مُختبَرة بالطفرات** (`bench/mutate.py`): تُحقن عيوب من الدرجة الأولى (قلب مقارنات، كسر حساب، دفع ثوابت) ويجب أن تصطادها الحزمة؛ أول تشغيل كشف ١٧ سلوكًا لم تكن الاختبارات تعضّ عليه — كلٌّ مثبَّت الآن باختبار مخصص. النسبة أداة تشخيص لا درجة جودة؛ والناجون المطبوعون يشملون طفرات مكافئة، وفروع منصات أخرى، وحدود عرض، وقيم معايرة تحرسها بوابات المقاييس. ## كيف تعمل — ثلاث طبقات كالدماغ -- **الطبقة العاملة** `‎.mind/ACTIVE.md` — نحو ٢٠٠ توكن يراها الوكيل دائمًا (أحرّ الذكريات + فهرس القشرة)، تُحقن في ملفات الوكلاء. +- **الطبقة العاملة** `‎.mind/ACTIVE.md` — حمولة حقائق ساخنة بنحو ٢٠٠ توكن، فوق عقد الذاكرة الدائم، وتُحقن في ملفات الوكلاء. - **الحُصين** `‎.mind/graph.json` — رسم موزون؛ الاسترجاع بانتشار التنشيط (≤٣ قفزات) مدموجًا رتبيًّا (RRF+IDF) مع المطابقة المباشرة، مع إكمال الأنماط (إشارة ناقصة تكفي) وفصل الأنماط (لا تكرار في النتائج). - **القشرة** `‎.mind/cortex/‎` — معرفة موطَّدة دائمة يغذيها الحالم. - **الحالم** — نوم خفيف (إشارات الجلسة) ← عميق (اضمحلال إيبنغهاوس + تشذيب مشبكي) ← حركي سريع (عنقدة وترقية + كشف تناقضات دون حذف تلقائي أبدًا). @@ -92,7 +91,7 @@ python3 mind.py correct "قاعدة ماي إس كيو إل" "قاعدة الب - **مصدر عند الكتابة:** كل عقدة تسجل `origin` (من كتبها — الوكلاء يصدّرون `MIND_BY`/`MIND_SESSION` — وعبر أي أمر)، وكل تحوير (تذكر/ربط/تأكيد/تصحيح/تقليم) يُلحق بـ**`.mind/journal.jsonl`** — سجل إلحاقي **لا يُدوَّر ولا يُمحى أبدًا** (بعكس signals.jsonl وهو قياس جلسة يُمسح). حتى بعد تقليم الحقيقة يبقى نسبها في اليومية. - **صحة منفصلة عن الانتباه:** `weight` يقيس البروز، و`valid_from`/`valid_to` يقيسان الصحة. الاضمحلال يلمس البروز فقط — **النسيان لا يجعل شيئًا كاذبًا أبدًا**؛ الإبطال لا يقع إلا بـ`correct` صريح. -- **اسأل الرسم:** `why ` (المنشأ والصلاحية وكل أحداث الحياة)، `entity "قاعدة"` (كل الحقائق عن مصطلح — الجارية والمنسوخة بفتراتها)، `recall "سؤال" --at 2026-01-15` (ما الذي كان صحيحًا حينها). +- **اسأل الرسم:** `why ` (المنشأ والصلاحية وعدد الأحداث الكامل مع آخر ثمانية؛ الملف الإلحاقي هو السجل الكامل)، `entity "قاعدة"` (كل الحقائق عن مصطلح — الجارية والمنسوخة بفتراتها)، `recall "سؤال" --at 2026-01-15` (ما الذي كان صحيحًا حينها). **حدّ صادق:** حلّ الكيانات **معجميّ** (التطبيع + التجذير + بذرة المفاهيم توحّد الهجاءات والتصريفات وثنائية العربية/الإنجليزية لنفس المصطلح) — الضمائر والأوصاف الحرة («الموظفة الجديدة» = «سارة») لا تُحل؛ ذلك يحتاج نموذجًا ويكسر وعد صفر التبعيات. @@ -112,10 +111,10 @@ python3 mind.py correct "قاعدة ماي إس كيو إل" "قاعدة الب ## خصائص الأمان -- كتابة ذرّية **معمّرة** ترفض الروابط الرمزية في كل مكان (fsync قبل الاستبدال — تصمد لانقطاع الطاقة، وملف القفل نفسه محصّن) +- كتابة ذرّية **معمّرة** بملف مؤقت فريد وفحص للكتابة الكاملة وfsync للملف والمجلد على POSIX، وترفض الروابط الرمزية - لا إتلاف صامت أبدًا: الرسم التالف يُعزل لا يُمحى، والمنسيّ بالاضمحلال يُؤرشف في `‎.mind/archive.md` لا يُدمر (وإن تعذرت الأرشفة لا يُحذف شيء أصلًا)، ومحتواك في ملفات الوكلاء محفوظ خارج علامات الحماية - `dream --dry-run` يعرض الخطة كاملة دون لمس القرص -- قفل ملفات عند الحفظ — آمن مع عمليات وكلاء متوازية +- قفل ملفات عند الحفظ — العقد تُدمج على مستوى الحقول، والحواف على مستوى كل اتجاه، واضمحلال الحواف اليومي يُحسم داخل القفل - الذاكرة ملفات نصية صرفة: راقبها بـ`git diff` وزامنها واقرأها ## حدود صادقة @@ -123,16 +122,17 @@ python3 mind.py correct "قاعدة ماي إس كيو إل" "قاعدة الب - الاسترجاع معجمي-بنيوي + **بذرة مفاهيم** منتقاة (٨٣ ربطة أداة←فئة لا لبس فيها: tailwind←css، hetzner←cloud...)؛ الترادف خارج البذرة والنص ما يزال يفوت، والكلمات متعددة المعاني مستبعدة عمدًا من البذرة (فئة كاذبة على جملة عادية أسوأ من ترادف فائت). - التجذير العربي خفيف (بادئات/لواحق + بذرة جموع تكسير)، ليس محللًا صرفيًّا كاملًا؛ وبقية اللغات بلا تجذير أو قوائم توقف أصلًا — IDF ومقاطع الحروف يحملانها (مقيس أعلاه) لكن الاستعلامات كثيفة التصريف ستفوت أكثر من العربية/الإنجليزية. - النصوص عديمة المسافات (صيني/ياباني/كوري/تايلندي) تُفهرس ثنائياتِ حروف لا تقطيعَ كلمات حقيقيًّا — مقايضة محركات البحث القياسية: استرجاع ممتاز مع تداخل ثنائيات كاذب أحيانًا. -- الرموز الأقصر من ٣ أحرف (`db`, `ai`, `os`) لا تُفهرَس — اكتبها كاملة مرة واحدة («database», «openai») وفهرس التلازم يجسر الباقي. +- الذكريات المؤلفة من رموز قصيرة (`db ai os`) تُفهرس بمسار حرفي احتياطي، لكنها تحمل دلالة أقل من الكلمات الموسعة، والرمز اللاتيني المفرد يُهمَل. - ذكرى استُرجعت أقل من مرتين ولم تُلمس بعد نافذة السماح (٤٥ يومًا) تنزل إلى `.mind/archive.md` بالتصميم (تُستعاد بـ`remember`). - الطوابع زمن محلي ساذج يقارَن معجميًّا (ISO): دقيق على جهاز واحد؛ مزامنة `.mind/` بين جهازين بمنطقتين زمنيتين قد تحرف ترتيب الصلاحية بفارق المنطقة. - معرفات العقد `md5[:12]` من النص — عنونة محتوى فقط، لا تُبنى عليها أي خاصية أمنية. +- الذاكرة والحقائق الساخنة المصدَّرة نص صريح؛ لا تحفظ أسرارًا أو مفاتيح أو بيانات شخصية خاصة أو نصوص prompt غير موثوقة. تُحيد البنية والعلامات عند العرض، لكنها تبقى مرئية للوكيل. - أكواد الخروج عقد ثابت: ٠ نجاح (حتى «لا نتائج»)، ١ فشل تشغيلي، ٢ خطأ استخدام. - مصممة لذاكرة وكيل شخصية/مشروعية (مئات إلى آلاف العقد)، لا لاسترجاع مؤسسي على ملايين الوثائق. ## التعزيز الصريح -`recall` قراءة خالصة لا تلمس القرص (الفحوص المتكررة لا تحرف الأوزان). حين تجيبك ذكرى فعلًا شغّل `confirm ` — تترسخ (+أسبوعا ثبات) وتقوى حوافها، وكل حلم يُضعف كل الحواف قليلًا (استتباب مشبكي) فتضمحل الروابط التي لا تكسب تأكيدًا وتُقلَّم. +`recall` قراءة خالصة لا تلمس القرص (الفحوص المتكررة لا تحرف الأوزان). حين تجيبك ذكرى فعلًا شغّل `confirm ` — تترسخ (+أسبوعين من الثبات) وتقوى حوافها، وكل حلم يُضعف كل الحواف قليلًا (استتباب مشبكي) فتضمحل الروابط التي لا تكسب تأكيدًا وتُقلَّم. ## التشغيل التلقائي (٦٫٢٫٠) — ذاكرة تعمل دون أن يُطلب منها @@ -140,11 +140,11 @@ python3 mind.py correct "قاعدة ماي إس كيو إل" "قاعدة الب ١. **العقد يركب الملف الذي يُحمَّل دائمًا.** `init` يزرع «أوامر دائمة» في `AGENTS.md`/`CLAUDE.md`/`GEMINI.md` — الملفات التي تحمّلها وكلاء البرمجة هذه تلقائيًّا كل جلسة (أي وكيل يقرؤها يحصل على العقد): احفظ الحقائق المستقرة أثناء العمل (مع قائمة منعٍ عدوانية ضد التعفن)، واحفظ ١-٣ حقائق قبل إنهاء أي مهمة جوهرية، واستدعِ قبل ادعاء الجهل — **دون استئذان المستخدم أبدًا**. والأوامر المصدَّرة تحمل المسار الحقيقي لـ`mind.py` فتعمل من أي مكان. -٢. **التوطيد ذاتي التشغيل.** بعد أوامر الكتابة يشتغل الحلم الكامل (اضمحلال/ترقية/فحص تناقضات) تلقائيًّا عند استحقاقه — يوميًّا أو كل ١٠ إشارات كتابة. لا كرون ولا خادم: يعمل في الحاويات وCI. للتعطيل: `MIND_AUTO_DREAM=0`. +٢. **التوطيد ذاتي التشغيل.** بعد أوامر الكتابة يشتغل الحلم الكامل (اضمحلال/تشذيب مشبكي/ترقية/فحص تناقضات) تلقائيًّا عند أول كتابة بلا حلم في اليوم أو كل ١٠ إشارات كتابة. لا كرون ولا خادم: يعمل في الحاويات وCI. للتعطيل: `MIND_AUTO_DREAM=0`. ٣. **الترسيخ يُكتسب بالاستعمال.** `recall` يطبع المعرفات والعقد يجعل الوكيل يؤكد ما أجابه فعلًا — فتترسخ الذكريات النافعة وتذوي المهملة. -مُختبَر ميدانيًّا بست محاكاة وكيل-في-مشروع (الوكيل يرى AGENTS.md المصدَّر فقط ولا يُخبَر عن الذاكرة): حفظ التفضيلات والدروس دون طلب، وصحّح حقيقة مهاجرة بـ`correct` بدل التكرار، واستدعى وعزّز عبر إعادة جلسة، وصفر نفايات في جلسة ثرثرة، وتوطيد تلقائي منتصف الجلسة عند العتبة. +خلال تطوير ٦٫٢٫٠ أُجريت يدويًّا ست محاكاة وكيل-في-مشروع (الوكيل يرى AGENTS.md المصدَّر فقط ولا يُخبَر عن الذاكرة): حفظ التفضيلات والدروس دون طلب، وصحّح حقيقة مهاجرة بـ`correct` بدل التكرار، واستدعى وعزّز عبر إعادة جلسة، وصفر نفايات في جلسة ثرثرة، وتوطيد تلقائي منتصف الجلسة عند العتبة. ## الاستخدام مع كيمي كود وهيرميس وكلاود كود وكودكس وجيمناي @@ -153,7 +153,7 @@ python3 mind.py correct "قاعدة ماي إس كيو إل" "قاعدة الب ## التطوير ```bash -python3 -m unittest discover -s tests # ١٩٠ اختبارًا +python3 -m unittest discover -s tests # ٢١٣ اختبارًا python3 bench/bench.py # أرقام العربية/الإنجليزية python3 bench/multilang.py # ٨ لغات غير مضبوطة python3 bench/discrim.py # التمييز بمشتتات متنافسة diff --git a/README.md b/README.md index d906723..d49e5b5 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ adopted automatically by `.cursorrules`, `.windsurfrules`, `.clinerules` and `.roo/rules/mind.md` in projects that already use those tools. ```bash -curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.7/mind.py -python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='c55594585f8d1dbb944e3ec56b57d6e36dc7c639ceeb0f5621f19ce45c66451b',h;print('mind.py: OK')" +curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.8/mind.py +python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='b3e944ce9103bd5e353e09a12f434721cb274bc919326361039104889c6c5e03',h;print('mind.py: OK')" python3 mind.py init python3 mind.py remember "the project database is postgres 16" python3 mind.py recall "which database do we use" @@ -39,15 +39,16 @@ No server, no vector store, no embedding model, no configuration file. ## Measured, not vibes -Every number below comes from `python3 bench/bench.py` — rerun it yourself +The first table comes from `python3 bench/bench.py`; each later measurement +names its own reproducible command — rerun them yourself (Python 3.14 on Apple M-series — latencies are environment-dependent, rerun for your hardware; 20 bilingual queries with known answers against distractor-filled graphs): | graph size | recall@1 | recall@5 | median latency | p95 | |---|---|---|---|---| -| 100 nodes | **1.00** | 1.00 | **~0.5–0.7 ms** | ~2–4 ms | -| 1,000 nodes | **1.00** | 1.00 | ~2–3 ms | ~11–16 ms | +| 100 nodes | **1.00** | 1.00 | **~0.8 ms** | ~2.7–3.0 ms | +| 1,000 nodes | **1.00** | 1.00 | ~3.3–3.9 ms | ~13.7–15.5 ms | (The long-standing 0.95 miss — a category question, "what css framework", against a memory that only said "tailwind" — is closed by the curated @@ -61,8 +62,8 @@ identical consolidation plan. clock with a realistic workload: daily/weekly/monthly facts + 357 junk notes + a dream every night): core-fact survival **15/15** across all cadence tiers, junk older than the grace window surviving: **0/256**, working -memory **8/8** hot slots held by core facts, graph size bounded -(~106 nodes), recall on the aged graph ~0.4 ms. The soak caught two +memory **7/8** hot slots held by core facts, graph size bounded +(~106 nodes), recall on the aged graph ~0.4–0.5 ms. The soak caught two real calibration bugs before release (facts pruned one day before their first monthly recall; decayed weight vetoing exact matches) — both fixed with regression tests. @@ -94,24 +95,27 @@ regress again. **Fuzzed** (`bench/fuzz.py`, seeded + deterministic): 420 adversarial cases in the full run (CI runs the 160-case quick set on every push) — hostile graph files (NaN/Infinity, wrong-typed fields, control characters, truncated JSON, dangling edges) and hostile CLI input. -Contract: never a traceback, never data loss, the graph always loads clean -afterwards. Its first run caught a real defect that six earlier audit +Contract: never a traceback; corrupt input is quarantined or repaired; the +resulting graph loads clean and accepts a new write. Its first run caught a +real defect that six earlier audit rounds had missed (see CHANGELOG 5.5.0). **Mutation-tested** (`bench/mutate.py`): first-order defects are injected (flipped comparisons, broken arithmetic, nudged constants) and the suite must catch them. Its first run exposed 17 behaviors the tests didn't actually pin down — each is now locked by a dedicated regression test -(raw kill rate on the seeded 120-mutant sample: 33% at first run, 45% +(raw kill rate on the seeded 120-mutant sample: 33% at first run, +**39%** on this release — the sample is re-drawn whenever the file changes, so the number moves a few points between releases; it is remeasured and republished each time because hiding it would be the exact sin this tool exists to catch). -Surviving mutants are triaged in the tool's output: unreachable `get()` -defaults, display-only constants, and ranking-calibration values guarded -by the CI benchmark gate (recall@1 ≥ 0.9) rather than unit assertions. +The rate is diagnostic, not a quality score. Survivors include equivalent +local calculations superseded by the locked merge, platform-only branches, +boundary/display constants, and ranking calibration guarded by the CI +benchmark gates rather than exact unit assertions; the tool prints every one. -Test suite: **190 tests**, stdlib `unittest`, `python3 -m unittest discover -s tests` — +Test suite: **213 tests**, stdlib `unittest`, `python3 -m unittest discover -s tests` — including regression tests for concurrency (parallel writers must not lose each other's memories), destructive-op gating, corrupt-graph recovery, and a mutation-kill class where every test pins a behavior the suite @@ -121,7 +125,7 @@ previously didn't bite on. ``` Layer 1 WORKING MEMORY .mind/ACTIVE.md → injected into agent rule files - the ~200-300 tokens the agent always sees: hottest memories + cortex index + a ~200-token hot-fact payload plus the standing memory contract Layer 2 HIPPOCAMPUS .mind/graph.json → weighted concept graph recall = spreading activation (≤3 hops) fused with direct keyword @@ -151,8 +155,9 @@ python3 mind.py correct "database mysql" "the database is postgres 16" # returning the old fact — but the graph still knows you migrated. # Closed facts stay in the graph through the 45-day grace window, then # archive; beyond that the lineage lives in the successor's history -# entries and the journal (forever), so `--at` answers within the -# retention window and `why` answers always. +# entries and the journal, so `--at` answers within the graph-retention +# window and `why` scans the full journal for that id even after pruning, +# reporting the full count and latest events. ``` ## Provenance & time — "where did this fact come from, and is it still true?" @@ -189,30 +194,22 @@ same term) — pronouns and free descriptions ("the new hire" = "Sara") are not resolved; that needs a model and would break the zero-dependency promise. -## Why not just use ___? +## Positioning — different tools, different jobs -![why not just use](assets/why-not-just-use.jpeg) +This is not a winner board. The neighboring projects are broader systems with +different tradeoffs: -| | spreading-activation recall | sleep consolidation | temporal validity | provenance log | works with any agent | zero setup / zero keys | consolidation costs $0 (no LLM) | -|---|---|---|---|---|---|---|---| -| mem0 | ✗ | ✗ | ✗ | ~ metadata | ✗ (SDK) | ✗ needs LLM + embedder keys | ✗ | -| Letta (MemGPT) | ✗ | ✓ sleep-time compute | ✗ | ✗ | ✗ (own server) | ✗ full platform | ✗ burns tokens | -| Zep / Graphiti | ~ graph traversal | ✗ | ✓ bi-temporal (its core strength) | ✓ | ✗ | ✗ Neo4j + LLM keys | ✗ | -| HippoRAG 2 | ✓ PageRank | ✗ | ✗ | ✗ | ✗ (batch RAG lib) | ✗ GPU/API | ✗ | -| OpenClaw dreams | ✗ | ✓ | ✗ | ✗ | ✗ (OpenClaw only) | ✓ inside OpenClaw | ✗ burns tokens | -| claude-mem | ✗ | ✗ compression | ✗ | ~ session refs | ~ several agents | ✗ Bun + worker + Chroma | ✗ | -| **mind** | **✓** | **✓ deterministic** | **✓ valid-time + `--at`** | **✓ append-only journal + `why`** | **✓ AGENTS/CLAUDE/GEMINI** | **✓ one file** | **✓** | - -Credit where due: Graphiti's bi-temporal model is the reference point for -temporal knowledge graphs — mind's valid-time layer (6.0.0) is the -zero-dependency, deterministic take on the same idea, not a claim to -have out-modeled it. - -Honest note: [Brain Memory](https://github.com/omelas-tech/brain) is the -closest project in spirit (files + decay + sleep phases) — credit where due. -`mind` differs in being a single copy-able file, bilingual EN/AR at the -tokenizer level, fully deterministic in consolidation, and shipping with a -reproducible benchmark instead of claims. +| project | primary scope | `mind`'s narrower choice | +|---|---|---| +| [Mem0](https://docs.mem0.ai/open-source/overview) | configurable memory layer, library or self-hosted server | one copied stdlib file; no LLM, embedder, vector store, or service | +| [Letta](https://docs.letta.com/letta-agent/memory/) | agent runtime with editable memory, MemFS, and sleep-time agents | runtime-agnostic project memory exported through common rule files | +| [Graphiti](https://help.getzep.com/graphiti/getting-started/overview) | temporal knowledge-graph framework with hybrid retrieval | small local lexical graph with valid-time and a plain JSON format | +| [HippoRAG 2](https://github.com/OSU-NLP-Group/HippoRAG) | research RAG/memory framework for large knowledge retrieval | operational memory for 10²–10³ project facts | + +Graphiti's temporal model is the reference point for this class of problem; +`mind`'s valid-time layer is a deterministic zero-dependency interpretation, +not a claim to have out-modeled it. [Brain Memory](https://github.com/omelas-tech/brain) +is the closest project in spirit (files + decay + sleep phases). ## Commands @@ -237,8 +234,9 @@ so connections that never earn a confirmation decay and prune away. ## Safety properties -- **Atomic, durable, symlink-refusing writes** everywhere — O_NOFOLLOW + - fsync-before-rename (survives power loss), the lock file itself is opened +- **Atomic, durable, symlink-refusing writes** everywhere — unique O_EXCL + temporary files, full-write checks, fsync-before-rename + directory fsync + on POSIX (survives power loss), the lock file itself is opened symlink-safe, and every internal write also rejects a symlinked *parent* directory so nothing can escape the `.mind/` boundary - **Never silently destroys data**: corrupt graphs are quarantined, not erased; @@ -246,7 +244,8 @@ so connections that never earn a confirmation decay and prune away. archive cannot be written, nothing is pruned at all; user content in `AGENTS.md`/`CLAUDE.md` is preserved outside guard markers - **`dream --dry-run`** previews the full plan without touching disk -- **File-locked saves** — safe under concurrent agent processes +- **File-locked saves** — nodes merge per field, edges per directional pair, + reinforcement as deltas, and daily edge decay is decided inside the lock - Memory files are plain JSON + Markdown: `git diff` them, sync them, read them ## Honest limitations @@ -269,14 +268,17 @@ so connections that never earn a confirmation decay and prune away. overlap between unrelated phrases. - Optimized for personal/project agent memory (10²–10³ nodes), not enterprise RAG over millions of documents — use a real graph DB for that. -- Tokens shorter than 3 characters (`db`, `ai`, `os`) are not indexed — - write them out once ("database", "openai") and the co-occurrence index - bridges the rest. +- Short-token-only memories (`db ai os`) use a literal fallback and are + recallable, but abbreviations carry less semantic evidence than expanded + terms and single-character Latin tokens are ignored. - Timestamps are naive local time compared lexicographically (ISO). On a single machine this is exact; syncing one `.mind/` across machines in different time zones can skew validity ordering by the zone offset. - Node ids are `md5[:12]` of the text — content addressing only; no security property is (or should be) derived from them. +- Memory and exported hot facts are plain text. Never store credentials, + secrets, private personal data, or untrusted prompt text; hot memories are + structurally escaped and labeled as data, but they remain agent-visible. - A fact recalled fewer than twice and untouched for longer than the 45-day grace window decays out of the graph (into the archive). Facts you need less often than ~every six weeks should live in cortex notes, not the @@ -298,15 +300,15 @@ and OpenClaw uses for its workspace memory: for permission**. The exported commands carry the real path to your `mind.py`, so they work from anywhere. 2. **Consolidation self-runs.** After write commands, a full dream cycle - (decay, dedup, promotion, conflict scan) fires automatically when due — - at most once per `MIND_AUTO_DREAM` window (default: daily, or every 10 - pending write signals). No cron, no server: it works in containers and + (decay, synaptic pruning, promotion, conflict scan) fires automatically + when due — on the first write without a dream that day, or after every 10 + pending write signals. No cron, no server: it works in containers and CI. Disable with `MIND_AUTO_DREAM=0`; force one with `mind dream`. 3. **Reinforcement is earned by use.** `recall` prints ids; the contract has the agent `confirm` hits that actually answered — so exactly the memories that keep proving useful harden, and unused ones fade. -Field-tested end-to-end: six simulated agent-in-project scenarios (an +During 6.2.0 development, six manual agent-in-project simulations (an agent given ONLY the exported `AGENTS.md`, never told about memory) saved stated preferences and technical lessons unprompted, corrected a migrated fact via `correct` instead of duplicating it, recalled and @@ -339,7 +341,7 @@ optional, since auto-dream already covers it. ## Development ```bash -python3 -m unittest discover -s tests # 190 tests +python3 -m unittest discover -s tests # 213 tests python3 bench/bench.py # reproduce the EN/AR numbers python3 bench/multilang.py # 8 untuned languages python3 bench/soak.py # 180 simulated days diff --git a/SECURITY.md b/SECURITY.md index 68f9977..7c7c988 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -13,27 +13,34 @@ hardening suggestions. You can expect an initial response within a few days. ## Security properties (and their tests) -- Atomic, durable writes: every replace-path write is O_NOFOLLOW + - fsync-before-rename; the append-only logs (provenance journal, - archive, dream journal, signals) use single O_NOFOLLOW+O_APPEND - writes, fsynced on the permanence-bearing ones, - including the lock file (`tests/test_mind.py` — the audit-regression classes - `TestAuditFindings2`, `TestThirdAudit`, `TestFourthAudit`). +- Atomic, durable writes: every replace-path write uses an unpredictable + O_EXCL temporary file, checks that every byte was written, fsyncs before + rename, and fsyncs the destination directory on POSIX. Append-only logs + (provenance journal, archive, dream journal, signals) use one checked + O_NOFOLLOW+O_APPEND write, fsynced on the permanence-bearing ones. + The lock file is opened O_NOFOLLOW (`tests/test_mind.py` — the regression + classes `TestAuditFindings2`, `TestThirdAudit`, `TestFourthAudit`). - Symlinked agent files, lock files, archive files and export parents are refused or skipped — never written through. The lock file's parent chain is checked before creation, so even a symlinked `.mind/` root cannot cause a single file to be created outside the project (regression test: `test_symlinked_mind_root_refused_entirely`). - Memory text is stripped of terminal control characters on every write - path, including `correct`. + and load path, including `correct`. Hot memories are collapsed to one + data-labeled line and HTML guard markers are escaped before agent export. - Hostile on-disk state is repaired, not trusted: a seeded fuzzer (`bench/fuzz.py`, 420 adversarial cases full / 160 quick in CI) holds - the contract - "no traceback, no data loss, graph always loads clean" against corrupt - graph files and hostile CLI input. -- The provenance journal (`journal.jsonl`) is append-only and written - behind the same symlink/parent-boundary checks as every other file. + the contract: no traceback; corrupt input is quarantined or repaired; the + resulting graph loads clean and accepts a new write. +- The provenance journal (`journal.jsonl`) is append-only; both writes and + reads refuse symlinks. `why ` streams the full file, while unfiltered + status reads stay tail-bounded. +- Concurrent graph saves merge nodes per field and edges per directional + pair; reinforcement is a delta, and once-per-day edge decay is decided + while holding the graph lock. +- The memory is plain text and exported hot facts are agent-visible. Do not + store secrets, credentials, private personal data, or untrusted prompt text. - Node ids are `md5[:12]` content addresses — no security property is derived from them. -- No network access, no subprocess execution, no eval — the file can be - fully audited in one sitting (~2,600 lines). +- No network access, subprocess execution, or eval — the file can be fully + audited in one sitting (~2,900 lines). diff --git a/SKILL.md b/SKILL.md index a83675b..914141b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: mind description: Project memory graph with recall, provenance, and dreams. -version: 6.2.7 +version: 6.2.8 author: Da7 (Da7-Tech) license: MIT platforms: [linux, macos, windows] @@ -36,8 +36,9 @@ built-in `memory` tool — and it is not a RAG system for large corpora. - `python3` (3.9+) and `curl` on PATH — nothing else: no API keys, no server, no packages. The tool is one stdlib-only file, MIT-licensed, - from https://github.com/Da7-Tech/mind (190 tests + benchmarks incl. 10 languages + discrimination + fuzzer + 180-day - soak test run in its CI on Linux/macOS/Windows). + from https://github.com/Da7-Tech/mind (213 tests + benchmarks incl. + 10 languages + discrimination + fuzzer + 180-day soak test run in its CI + on Linux/macOS/Windows). ## How to Run @@ -46,8 +47,8 @@ tag and integrity-checked: ```bash cd -curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.7/mind.py -python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='c55594585f8d1dbb944e3ec56b57d6e36dc7c639ceeb0f5621f19ce45c66451b',h;print('mind.py: OK')" +curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.8/mind.py +python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='b3e944ce9103bd5e353e09a12f434721cb274bc919326361039104889c6c5e03',h;print('mind.py: OK')" python3 mind.py init ``` @@ -86,13 +87,17 @@ their rule files synced too (adopted only when present). 4. Provenance is automatic (append-only `.mind/journal.jsonl`, never cleared). Set `MIND_BY` and `MIND_SESSION` env vars when running commands so `why` can attribute facts to you/this session. -5. Consolidation is SELF-RUNNING (6.2.0): after write commands, a full +5. Never put credentials, tokens, private personal data, or untrusted prompt + text in project memory. Hot facts are exported into agent instruction files. + Durable user facts unrelated to this project belong in Hermes' built-in + `memory` tool, not here. +6. Consolidation is SELF-RUNNING (6.2.0): after write commands, a full dream cycle fires automatically when >= 10 signals pend or no dream has happened yet today (including a fresh project's very first write) — you normally never schedule anything. `dream` forces a cycle; it is deterministic and reversible (archive, never delete); use `--dry-run` only when the user explicitly asks to review the plan. Every action is explained in `.mind/dreams/.md`. -6. Optional belt-and-suspenders for projects that go DAYS without any +7. Optional belt-and-suspenders for projects that go DAYS without any write (auto-dream piggybacks on writes): a zero-token nightly cron via the `cronjob` tool in no-agent mode. Use the `write_file` tool to create `~/.hermes/scripts/mind_dream.sh` with this body: @@ -124,7 +129,7 @@ their rule files synced too (adopted only when present). ## Verification ```bash -cd "$(mktemp -d)" && curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.7/mind.py && python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='c55594585f8d1dbb944e3ec56b57d6e36dc7c639ceeb0f5621f19ce45c66451b',h;print('OK')" && python3 mind.py init >/dev/null && python3 mind.py remember "the sky signal is 7413" >/dev/null && python3 mind.py recall "sky signal" +cd "$(mktemp -d)" && curl -fsSLO https://raw.githubusercontent.com/Da7-Tech/mind/v6.2.8/mind.py && python3 -c "import hashlib;h=hashlib.sha256(open('mind.py','rb').read()).hexdigest();assert h=='b3e944ce9103bd5e353e09a12f434721cb274bc919326361039104889c6c5e03',h;print('OK')" && python3 mind.py init >/dev/null && python3 mind.py remember "the sky signal is 7413" >/dev/null && python3 mind.py recall "sky signal" ``` Expected: one result containing `7413` with a printed memory id. diff --git a/assets/why-not-just-use.jpeg b/assets/why-not-just-use.jpeg deleted file mode 100644 index a8a50e08fafeaa80204ef195fce74a4073aafdd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 137424 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

NUR_cu;qdY0a4ORjnC=p=sP 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 2/3 diff --git a/bench/soak.py b/bench/soak.py index 96e493c..bd7cd86 100644 --- a/bench/soak.py +++ b/bench/soak.py @@ -159,6 +159,8 @@ def main(): hot_core = sum(1 for ln in hot_lines if any(m in ln for m in core_markers)) print("working memory: %d/%d hot slots are core facts" % (hot_core, len(hot_lines))) + if hot_core < 7: + all_ok = False lat = [] for tier, facts in CORE.items(): diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 7df3ce2..2e0cfa8 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -29,7 +29,8 @@ dreams between sessions, and portable to any agent through one standard file. (who/via/session), and every mutation appends to `journal.jsonl`, which is never cleared — "where did this fact come from" stays answerable after pruning, unlike the session telemetry in - `signals.jsonl`. `why`/`entity`/`recall --at` expose it. + `signals.jsonl`. Targeted `why` scans the full journal as a stream; + unfiltered `status` reads only the last 10 MB to stay bounded. 6. **One `init` + automatic export** for every agent (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`) behind guard markers. 7. **Writes during the session, dreams between sessions** — the agent @@ -71,15 +72,15 @@ restrengthens its edges, while every dream weakens all edges slightly | deep sleep | slow-wave consolidation + synaptic homeostasis | Ebbinghaus decay; prune weak nodes and weak edges | | REM | recombination | deterministic clustering of related memories → promote recurring clusters to cortex; scan for contradictions and *flag* them | -Everything is deterministic (offline hash embeddings, fixed thresholds): -the same memory state always yields the same plan, `--dry-run` previews it, -and the journal explains every action. No LLM is consulted — consolidation -costs zero tokens and can run in cron forever. +Everything is deterministic (offline hash embeddings, fixed thresholds, +sorted node traversal, explicit tie-breakers): the same memory state always +yields the same plan and recall order. `--dry-run` previews it, and the journal +explains every action. No LLM is consulted — consolidation costs zero tokens. ## Failure modes we accept (and why) - **No true semantic embeddings by default.** Cross-domain synonymy with no - corpus evidence is missed (the benchmark's one failing query). The + concept-seed or corpus evidence can still be missed. The alternative — bundling a model or requiring API keys — breaks the zero-setup promise that makes the tool spread. Pluggable backends can come later without breaking the file format. diff --git a/mind.py b/mind.py index 955b16d..23078e6 100644 --- a/mind.py +++ b/mind.py @@ -25,12 +25,12 @@ Design: docs/DESIGN.md | License: MIT | https://github.com/Da7-Tech/mind """ -import sys, os, json, re, time, math, hashlib +import sys, os, json, re, time, math, hashlib, tempfile, shlex from datetime import datetime, timedelta from pathlib import Path from collections import Counter, defaultdict -__version__ = "6.2.7" +__version__ = "6.2.8" # ──────────────────────────────────────────────────────────────── # Tunables (see docs/DESIGN.md for the reasoning behind each value) @@ -122,6 +122,24 @@ def _read_text_retry(path): time.sleep(0.05) +def _write_all(fd, data): + """Write every byte or raise. os.write() may legally return short.""" + view = memoryview(data) + while view: + written = os.write(fd, view) + if written <= 0: + raise OSError("short write: os.write made no progress") + view = view[written:] + + +def _write_once(fd, data): + """One append syscall; reject a short record instead of hiding it.""" + written = os.write(fd, data) + if written != len(data): + raise OSError("partial append: wrote %d of %d bytes" % ( + written, len(data))) + + def _atomic_write(path, data, boundary=None): """Atomic, symlink-safe, durable write: O_NOFOLLOW + fsync + os.replace. @@ -137,34 +155,52 @@ def _atomic_write(path, data, boundary=None): raise ValueError(f"refusing to write through a symlink: {path}") if boundary is not None: _reject_symlinked_parents(path, boundary) - tmp = "%s.%d.tmp" % (path, os.getpid()) - nofollow = getattr(os, "O_NOFOLLOW", 0) - fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | nofollow | os.O_TRUNC, 0o644) + # mkstemp is O_EXCL and unpredictable: no same-process collision and no + # pre-created hard-link/symlink can be truncated through a predictable + # `..tmp` name. + fd, tmp = tempfile.mkstemp(prefix=path.name + ".", suffix=".tmp", + dir=str(path.parent)) + replaced = False try: - os.write(fd, data.encode("utf-8") if isinstance(data, str) else data) - os.fsync(fd) - finally: - os.close(fd) - # Windows: os.replace raises PermissionError while another process - # momentarily holds the destination open (Python's open() does not - # grant FILE_SHARE_DELETE). Readers are short-lived — retry briefly - # (CI finding: 1/12 parallel writers lost this race on - # windows-latest; POSIX never enters the loop). - for attempt in range(200): try: - os.replace(tmp, str(path)) - break - except PermissionError: - if os.name != "nt" or attempt == 199: - raise - time.sleep(0.05) + try: + os.fchmod(fd, 0o644) + except (AttributeError, OSError): + pass + payload = data.encode("utf-8") if isinstance(data, str) else data + _write_all(fd, payload) + os.fsync(fd) + finally: + os.close(fd) + fd = None + # Windows: os.replace raises PermissionError while another process + # momentarily holds the destination open (Python's open() does not + # grant FILE_SHARE_DELETE). Readers are short-lived — retry briefly + # (CI finding: 1/12 parallel writers lost this race on + # windows-latest; POSIX never enters the loop). + for attempt in range(200): + try: + os.replace(tmp, str(path)) + replaced = True + break + except PermissionError: + if os.name != "nt" or attempt == 199: + raise + time.sleep(0.05) + finally: + if fd is not None: + os.close(fd) + if not replaced: + try: + os.unlink(tmp) + except OSError: + pass # full durability: fsync the DIRECTORY too, or the rename itself can # be lost on power failure (auditor finding — the docs promised more # than fsync-the-file delivers). Windows has no dir-fsync; skip. if os.name != "nt": try: - dfd = os.open(os.path.dirname(os.path.abspath(str(path))) or ".", - os.O_RDONLY) + dfd = os.open(str(path.parent.resolve()), os.O_RDONLY) try: os.fsync(dfd) finally: @@ -646,6 +682,10 @@ def __init__(self, path): # without this, an edge decayed to empty and removed from # self.edges is silently REVIVED from disk by the read-merge-write # (edge deletions, unlike node deletions, weren't tracked) + self._edge_updates = set() # directional edges created/replaced here + self._edge_bumps = Counter() # directional reinforcement deltas + self._edge_decay_day = None # daily homeostasis request, applied in lock + self._last_edge_pruned = 0 self._load() # -- persistence ------------------------------------------------- @@ -681,6 +721,26 @@ def _finite(value, default, lo=None, hi=None): f = min(hi, f) return f + @staticmethod + def _iso_timestamp(value, default): + """Return a naive-local ISO timestamp or `default` when malformed.""" + if not isinstance(value, str): + return default + try: + parsed = datetime.fromisoformat(value) + except ValueError: + return default + if parsed.tzinfo is not None: + parsed = parsed.astimezone().replace(tzinfo=None) + return parsed.isoformat() + + @classmethod + def _metadata_text(cls, value, default, cap): + if not isinstance(value, str): + return default + cleaned = " ".join(cls._clean_text(value).split())[:cap] + return cleaned or default + def _repair_nodes(self, raw): """Repair a nodes dict fresh off the disk — shared by _load AND the _save read-merge-write. The merge used to import raw disk nodes @@ -688,23 +748,26 @@ def _repair_nodes(self, raw): or another (buggier) process re-poisoned a healthy session's graph on its next save (fuzzer finding).""" out = {} + now_iso = _now().isoformat() for nid, n in raw.items(): if not isinstance(n, dict) or not isinstance(n.get("text", ""), str): continue + n["text"] = self._clean_text(n.get("text", "")) + if not n["text"]: + continue n.setdefault("text", "") n.setdefault("weight", 1.0) n.setdefault("peak_weight", n.get("weight", 1.0)) n.setdefault("confidence", 1.0) n.setdefault("access_count", 0) n.setdefault("keys", []) - n.setdefault("last_accessed", _now().isoformat()) - n.setdefault("created", _now().isoformat()) + n.setdefault("last_accessed", now_iso) + n.setdefault("created", now_iso) # timestamps must be ISO strings: a hand-edit that leaves a # number here would crash decay with TypeError, not ValueError # (auditor finding) — repair to "now" like the numeric fields for f in ("last_accessed", "created"): - if not isinstance(n.get(f), str): - n[f] = _now().isoformat() + n[f] = self._iso_timestamp(n.get(f), now_iso) # numeric fields: repair non-numeric AND non-finite values, and # clamp to the range every write path maintains (auditor + # fuzzer findings) @@ -714,26 +777,36 @@ def _repair_nodes(self, raw): n["access_count"] = int(self._finite(n["access_count"], 0, 0)) # history must be a list — correct() appends to it (fuzzer # finding: a scalar history crashed reconsolidation) - if not isinstance(n.get("history", []), list): - n["history"] = [] + raw_history = n.get("history", []) + if not isinstance(raw_history, list): + raw_history = [] + history = [] + for h in raw_history: + if not isinstance(h, dict): + continue + old_text = self._clean_text(h.get("text", "")) + if not old_text: + continue + history.append({ + "text": old_text[:MAX_TEXT_CHARS], + "replaced": self._iso_timestamp( + h.get("replaced"), "unknown"), + }) + n["history"] = history # provenance + validity (6.0.0): older graphs get honest # defaults — origin "unknown", validity open since creation - if not isinstance(n.get("origin"), dict): - n["origin"] = {"by": "unknown", "session": None, - "via": "unknown"} - if not isinstance(n.get("valid_from"), str): - n["valid_from"] = n["created"] - if not isinstance(n.get("valid_to"), str): - n["valid_to"] = None - # lexicographic comparison assumes ISO format — repair any - # hand-edited non-ISO value instead of comparing garbage - for f, d in (("valid_from", n["created"]), ("valid_to", None)): - v = n.get(f) - if isinstance(v, str): - try: - datetime.fromisoformat(v) - except ValueError: - n[f] = d + origin = n.get("origin") + if not isinstance(origin, dict): + origin = {} + n["origin"] = { + "by": self._metadata_text(origin.get("by"), "unknown", 80), + "session": self._metadata_text( + origin.get("session"), None, 120), + "via": self._metadata_text(origin.get("via"), "unknown", 40), + } + n["valid_from"] = self._iso_timestamp( + n.get("valid_from"), n["created"]) + n["valid_to"] = self._iso_timestamp(n.get("valid_to"), None) if not (n.get("superseded_by") is None or isinstance(n.get("superseded_by"), str)): n.pop("superseded_by", None) @@ -743,7 +816,8 @@ def _repair_nodes(self, raw): raw_keys = n.get("keys", []) if not isinstance(raw_keys, list): raw_keys = [] - n["keys"] = [re.sub(r'[،؛؟!."\']', '', k).strip()[:100] + n["keys"] = [re.sub(r'[،؛؟!."\']', '', self._clean_text(k)) + .strip()[:100] for k in raw_keys if isinstance(k, str)] n["keys"] = [k for k in n["keys"] if k][:24] # the write-path text cap must hold on the LOAD path too — a @@ -768,8 +842,14 @@ def _repair_edges(self, raw, nodes): if nbr not in nodes or not isinstance(e, dict): continue e["weight"] = self._finite(e.get("weight", 1.0), 1.0, 0.0, 1.0) - if not isinstance(e.get("relation", "related"), str): - e["relation"] = "related" + e["relation"] = self._metadata_text( + e.get("relation"), "related", 60) + if "created" in e: + created = self._iso_timestamp(e.get("created"), None) + if created is None: + e.pop("created", None) + else: + e["created"] = created clean[nbr] = e if clean: out[nid] = clean @@ -855,9 +935,15 @@ def _save(self): "process hung?") lock_backend = ("msvcrt", msvcrt) try: + disk = {} if self.path.exists(): try: disk = json.loads(_read_text_retry(self.path)) + if not isinstance(disk, dict): + raise ValueError("graph.json is not a JSON object") + if not isinstance(disk.get("nodes", {}), dict) or \ + not isinstance(disk.get("edges", {}), dict): + raise ValueError("nodes/edges have the wrong structure") except (json.JSONDecodeError, ValueError) as e: # never silently overwrite a corrupt graph during a # live save: quarantine it exactly like _load does @@ -865,85 +951,103 @@ def _save(self): # on the load path) self._quarantine(e) disk = {} - dn = disk.get("nodes", {}) if isinstance(disk, dict) else {} - de = disk.get("edges", {}) if isinstance(disk, dict) else {} - if isinstance(dn, dict) and isinstance(de, dict): - # repair the disk copy BEFORE merging: without this - # the merge imported raw disk content past all of - # _load's validation, so one corrupt file poisoned - # a healthy session's next save (fuzzer finding) - merged_n = {k: v - for k, v in self._repair_nodes(dn).items() - if k not in self._deleted} - # field-freshness: only nodes this session touched - # overwrite the disk copy; for everything else the - # DISK is fresher (auditor finding) - for k in self._dirty: - if k in self.nodes: - merged_n[k] = self.nodes[k] - for k, v in self.nodes.items(): - merged_n.setdefault(k, v) - # reinforcement deltas: re-apply OUR confirms on - # top of the freshest copy, so concurrent confirms - # from other processes are added to, never raced - now_iso = _now().isoformat() - for k, d in self._bumped.items(): - n = merged_n.get(k) - if n is None or k in self._dirty: - continue # dirty copy already includes it - n["access_count"] = int(self._finite( - n.get("access_count", 0), 0, 0)) + d - n["weight"] = min(1.0, self._finite( - n.get("weight", 1.0), 1.0, 0.0, 1.0) - + BOOST_PER_ACCESS * d) - n["peak_weight"] = max( - self._finite(n.get("peak_weight", 1.0), - 1.0, 0.0, 1.0), n["weight"]) - n["last_accessed"] = now_iso - # decay updates: salience only, on the fresh copy — - # min() because decay never raises a weight, and a - # concurrent confirm's boost must win the tie - for k, w in self._decayed.items(): - n = merged_n.get(k) - if n is None or k in self._dirty \ - or k in self._bumped: - continue - n["weight"] = min(self._finite( - n.get("weight", 1.0), 1.0, 0.0, 1.0), w) - # Build the merged edges from the DISK copy, stripping - # both deleted nodes and edges this process pruned, - # THEN apply our live edges. Order matters: pruned - # pairs must be removed from the disk copy *before* - # update, so a live edge we legitimately (re)created - # this session still wins — filtering after update - # would wrongly clobber it (auditor finding). - merged_e = {} - for k, v in self._repair_edges(de, merged_n).items(): - if k in self._deleted: - continue - merged_e[k] = {nbr: e for nbr, e in v.items() - if nbr not in self._deleted - and (k, nbr) not in self._pruned_edges} - merged_e.update(self.edges) - # drop node-keys the disk left empty after stripping - merged_e = {k: v for k, v in merged_e.items() if v} - self.nodes, self.edges = merged_n, merged_e - # meta merge (max-wins: ISO dates compare) — inside - # the same disk-copy scope as nodes/edges - disk_meta = (disk.get("meta", {}) - if isinstance(disk, dict) and - isinstance(disk.get("meta", {}), dict) - else {}) - merge_today = str(_now().date()) - for mk, mv in disk_meta.items(): - if mk not in _META_KEYS or not ( - isinstance(mv, str) and mv.isprintable()): - continue - mv = mv[:64] - if mk == "last_edge_decay" and mv > merge_today: - mv = merge_today # future marker = skew garbage - if mv > self.meta.get(mk, ""): - self.meta[mk] = mv + dn = disk.get("nodes", {}) + de = disk.get("edges", {}) + # repair the disk copy BEFORE merging: without this the merge + # imported raw disk content past _load's validation. + merged_n = {k: v for k, v in self._repair_nodes(dn).items() + if k not in self._deleted} + # Field freshness is per node: untouched disk copies are newer. + for k in self._dirty: + if k in self.nodes: + merged_n[k] = self.nodes[k] + for k, v in self.nodes.items(): + merged_n.setdefault(k, v) + # Reinforcement deltas apply on top of the freshest disk copy. + now_iso = _now().isoformat() + for k, d in self._bumped.items(): + n = merged_n.get(k) + if n is None or k in self._dirty: + continue + n["access_count"] = int(self._finite( + n.get("access_count", 0), 0, 0)) + d + n["weight"] = min(1.0, self._finite( + n.get("weight", 1.0), 1.0, 0.0, 1.0) + + BOOST_PER_ACCESS * d) + n["peak_weight"] = max( + self._finite(n.get("peak_weight", 1.0), + 1.0, 0.0, 1.0), n["weight"]) + n["last_accessed"] = now_iso + # Decay touches salience only on the fresh disk copy. + for k, w in self._decayed.items(): + n = merged_n.get(k) + if n is None or k in self._dirty or k in self._bumped: + continue + n["weight"] = min(self._finite( + n.get("weight", 1.0), 1.0, 0.0, 1.0), w) + + # Edge freshness is per DIRECTIONAL PAIR. Whole-adjacency + # update() lost concurrent links and could leave an asymmetric + # graph (A->B gone while B->A survived). + merged_e = {} + for k, v in self._repair_edges(de, merged_n).items(): + if k in self._deleted: + continue + kept = {nbr: e for nbr, e in v.items() + if nbr not in self._deleted + and (k, nbr) not in self._pruned_edges} + if kept: + merged_e[k] = kept + for a, b in self._edge_updates: + edge = self.edges.get(a, {}).get(b) + if edge is not None and a in merged_n and b in merged_n: + merged_e.setdefault(a, {})[b] = dict(edge) + for (a, b), count in self._edge_bumps.items(): + edge = merged_e.get(a, {}).get(b) + if edge is None: + continue + edge["weight"] = min( + 1.0, self._finite(edge.get("weight", 1.0), + 1.0, 0.0, 1.0) + + EDGE_BOOST * count) + + # Meta merge happens before daily edge decay so the decision + # is made under the lock against the freshest marker. Two + # concurrent dreams can therefore decay a day only once. + disk_meta = disk.get("meta", {}) + if not isinstance(disk_meta, dict): + disk_meta = {} + merge_today = str(_now().date()) + for mk, mv in disk_meta.items(): + if mk not in _META_KEYS or not ( + isinstance(mv, str) and mv.isprintable()): + continue + mv = mv[:64] + if mk == "last_edge_decay" and mv > merge_today: + mv = merge_today + if mv > self.meta.get(mk, ""): + self.meta[mk] = mv + + self._last_edge_pruned = 0 + decay_day = self._edge_decay_day + if decay_day is not None: + decay_day = min(decay_day, merge_today) + if decay_day and self.meta.get( + "last_edge_decay", "") < decay_day: + for a in list(merged_e): + for b in list(merged_e[a]): + edge = merged_e[a][b] + edge["weight"] = round(self._finite( + edge.get("weight", 1.0), 1.0, 0.0, 1.0) + * EDGE_DECAY_PER_DREAM, 4) + if edge["weight"] < EDGE_PRUNE_THRESHOLD: + del merged_e[a][b] + self._last_edge_pruned += 1 + if not merged_e[a]: + del merged_e[a] + self.meta["last_edge_decay"] = decay_day + + self.nodes, self.edges = merged_n, merged_e _atomic_write(self.path, json.dumps( {"nodes": self.nodes, "edges": self.edges, "meta": self.meta}, @@ -958,6 +1062,9 @@ def _save(self): self._dirty.clear() self._bumped.clear() self._decayed.clear() + self._edge_updates.clear() + self._edge_bumps.clear() + self._edge_decay_day = None finally: if lock_backend is not None: name, module = lock_backend @@ -967,6 +1074,20 @@ def _save(self): lockf.seek(0) module.locking(lockf.fileno(), module.LK_UNLCK, 1) + def decay_edges(self, dry_run=False): + """Apply synaptic homeostasis once per day, decided under the lock.""" + today = str(_now().date()) + if self.meta.get("last_edge_decay", "") >= today: + return 0 + if dry_run: + return sum( + 1 for nbrs in self.edges.values() for e in nbrs.values() + if self._finite(e.get("weight", 1.0), 1.0, 0.0, 1.0) + * EDGE_DECAY_PER_DREAM < EDGE_PRUNE_THRESHOLD) + self._edge_decay_day = today + self._save() + return self._last_edge_pruned + @staticmethod def _id(text): # content addressing only — no security property is derived from @@ -1079,17 +1200,22 @@ def _clean_text(text): return re.sub(u"[\x00-\x08\x0b-\x1f\x7f\u202a-\u202e\u2066-\u2069]", "", text or "").strip() + @classmethod + def _validated_text(cls, text, label="memory"): + if not isinstance(text, str): + raise ValueError("%s text must be a string" % label) + cleaned = cls._clean_text(text) + if not cleaned: + raise ValueError("%s text must not be empty" % label) + if len(cleaned) > MAX_TEXT_CHARS: + raise ValueError("%s text exceeds %d chars" % ( + label, MAX_TEXT_CHARS)) + return cleaned + # -- write path --------------------------------------------------- def remember(self, text, confidence=1.0): - if not text or not text.strip(): - raise ValueError("cannot remember empty text") - text = self._clean_text(text) - if not text: - raise ValueError("cannot remember control-characters-only text") - if len(text) > MAX_TEXT_CHARS: - raise ValueError("memory text exceeds %d chars — store the " - "document in a file and remember its path" - % MAX_TEXT_CHARS) + text = self._validated_text(text) + confidence = self._finite(confidence, 1.0, 0.0, 1.0) nid = self._id(text) now = _now().isoformat() is_new = nid not in self.nodes @@ -1099,8 +1225,11 @@ def remember(self, text, confidence=1.0): n["peak_weight"] = max(n.get("peak_weight", 1.0), n["weight"]) n["access_count"] = n.get("access_count", 0) + 1 n["last_accessed"] = now - n["confidence"] = max(n.get("confidence", 1.0), confidence) + old_confidence = n.get("confidence", 1.0) + n["confidence"] = max(old_confidence, confidence) self._bumped[nid] = self._bumped.get(nid, 0) + 1 + if n["confidence"] != old_confidence: + self._dirty.add(nid) # a re-remembered superseded fact is an explicit re-assertion: # the user says it IS true again — reopen a NEW validity # segment starting now (the closed segment stays queryable in @@ -1149,7 +1278,10 @@ def link(self, text_a, text_b, relation="related"): # hash the CLEANED text, exactly as remember() does, so the edge is # stored under the same id the node gets — otherwise the edge points # at a phantom id and is dropped on next load (auditor finding) - text_a, text_b = self._clean_text(text_a), self._clean_text(text_b) + # Validate BOTH endpoints before remember() can persist either one: + # a rejected second endpoint must not leave a partial first memory. + text_a = self._validated_text(text_a, "first link endpoint") + text_b = self._validated_text(text_b, "second link endpoint") id_a, id_b = self._id(text_a), self._id(text_b) # a self-loop would feed a node its own activation on every hop of # spreading recall, silently inflating its rank (auditor finding) @@ -1164,6 +1296,7 @@ def link(self, text_a, text_b, relation="related"): "weight": 1.0, "created": now} self.edges.setdefault(id_b, {})[id_a] = {"relation": relation, "weight": 1.0, "created": now} + self._edge_updates.update(((id_a, id_b), (id_b, id_a))) self._save() self._journal("link", id=id_a, other=id_b, relation=relation) self._log_signal("link", "%s --%s--> %s" % (text_a, relation, text_b)) @@ -1213,11 +1346,7 @@ def correct(self, old_hint, new_text): # write path too, and an uncleaned new_text would store text under # an id remember() would never produce (auditor finding). An empty # replacement would silently blank a memory — refuse it. - new_text = self._clean_text(new_text) - if not new_text: - raise ValueError("corrected text must not be empty") - if len(new_text) > MAX_TEXT_CHARS: - raise ValueError("corrected text exceeds %d chars" % MAX_TEXT_CHARS) + new_text = self._validated_text(new_text, "corrected") if not self.nodes: return None results, _, _ = self.recall(old_hint, top_k=1) @@ -1275,16 +1404,21 @@ def correct(self, old_hint, new_text): continue if e.get("relation") in ("supersedes", "superseded-by"): continue - self.edges.setdefault(new_nid, {}).setdefault(nbr, dict(e)) + if nbr not in self.edges.setdefault(new_nid, {}): + self.edges[new_nid][nbr] = dict(e) + self._edge_updates.add((new_nid, nbr)) rev = self.edges.get(nbr, {}).get(nid) if rev is not None and rev.get("relation") not in ( "supersedes", "superseded-by"): - self.edges.setdefault(nbr, {}).setdefault(new_nid, dict(rev)) + if new_nid not in self.edges.setdefault(nbr, {}): + self.edges[nbr][new_nid] = dict(rev) + self._edge_updates.add((nbr, new_nid)) # the explicit, timestamped state transition self.edges.setdefault(new_nid, {})[nid] = { "relation": "supersedes", "weight": 0.5, "created": now} self.edges.setdefault(nid, {})[new_nid] = { "relation": "superseded-by", "weight": 0.5, "created": now} + self._edge_updates.update(((new_nid, nid), (nid, new_nid))) # close the old fact — invalidation is explicit and reversible, # never conflated with attention decay node["valid_to"] = now @@ -1394,9 +1528,9 @@ def recall(self, query, top_k=RECALL_TOP_K, max_hops=RECALL_RADIUS, # RRF fusion: absent nodes get rank len(list)+1, not infinity, so the # spreading channel keeps real influence in the fused score. dr = {n: i for i, (n, _) in enumerate( - sorted(direct.items(), key=lambda x: -x[1]))} + sorted(direct.items(), key=lambda x: (-x[1], x[0])))} sr = {n: i for i, (n, _) in enumerate( - sorted(spread.items(), key=lambda x: -x[1]))} + sorted(spread.items(), key=lambda x: (-x[1], x[0])))} dr_default, sr_default = len(dr) + 1, len(sr) + 1 fused = {} for nid in set(direct) | set(spread): @@ -1408,7 +1542,7 @@ def recall(self, query, top_k=RECALL_TOP_K, max_hops=RECALL_RADIUS, # self.nodes if the graph was mutated externally mid-flight — # drop them instead of raising. fused = {nid: s for nid, s in fused.items() if nid in alive} - ranked = sorted(fused.items(), key=lambda x: -x[1]) + ranked = sorted(fused.items(), key=lambda x: (-x[1], x[0])) # identity questions ("what is my name") are decided by lexical # identity evidence; the char-gram rerank favors token repetition # ("file name ... class name") and must sit this one out @@ -1418,7 +1552,7 @@ def recall(self, query, top_k=RECALL_TOP_K, max_hops=RECALL_RADIUS, for nid, base in ranked[:top_k * 3]: sim = self.embedder.similarity(query, self.nodes[nid]["text"]) reranked.append((nid, base * (1.0 + sim))) - reranked.sort(key=lambda x: -x[1]) + reranked.sort(key=lambda x: (-x[1], x[0])) ranked = reranked # pattern separation: drop near-duplicate results from the head so @@ -1447,6 +1581,7 @@ def bump(self, node_ids): agent-facing path here). Tracks peak_weight for Ebbinghaus, and restrengthens the confirmed node's edges — connections you actually use stay strong, unused ones decay away dream by dream.""" + node_ids = list(dict.fromkeys(node_ids)) now = _now() changed = False for nid in node_ids: @@ -1458,9 +1593,11 @@ def bump(self, node_ids): n["last_accessed"] = now.isoformat() for nbr, e in self.edges.get(nid, {}).items(): e["weight"] = min(1.0, e.get("weight", 1.0) + EDGE_BOOST) + self._edge_bumps[(nid, nbr)] += 1 rev = self.edges.get(nbr, {}).get(nid) if rev is not None: rev["weight"] = e["weight"] + self._edge_bumps[(nbr, nid)] += 1 self._bumped[nid] = self._bumped.get(nid, 0) + 1 changed = True if changed: @@ -1564,8 +1701,8 @@ def _archive(self, texts, now): os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0), 0o644) try: - os.write(fd, (header + "\n".join(lines) + "\n") - .encode("utf-8")) + _write_once(fd, (header + "\n".join(lines) + "\n") + .encode("utf-8")) os.fsync(fd) # archived text is a durability promise finally: os.close(fd) @@ -1586,7 +1723,7 @@ def _log_signal(self, kind, content): os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0), 0o644) try: - os.write(fd, (json.dumps( + _write_once(fd, (json.dumps( {"kind": kind, "content": content, "ts": _now().isoformat()}, ensure_ascii=False) + "\n").encode("utf-8")) @@ -1596,12 +1733,14 @@ def _log_signal(self, kind, content): pass # telemetry only — never block the write it rode on # -- provenance ----------------------------------------------------- - @staticmethod - def _actor(): + @classmethod + def _actor(cls): """Who is writing. Agents/harnesses set MIND_BY and MIND_SESSION in the environment; the zero-setup default is 'agent'.""" - return (os.environ.get("MIND_BY", "agent"), - os.environ.get("MIND_SESSION")) + return ( + cls._metadata_text(os.environ.get("MIND_BY"), "agent", 80), + cls._metadata_text(os.environ.get("MIND_SESSION"), None, 120), + ) def _journal(self, op, **fields): """Append-only provenance log (journal.jsonl). Unlike @@ -1637,8 +1776,8 @@ def _journal(self, op, **fields): fd = os.open(str(jf), os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0), 0o644) try: - os.write(fd, (json.dumps(entry, ensure_ascii=False) - + "\n").encode("utf-8")) + _write_once(fd, (json.dumps(entry, ensure_ascii=False) + + "\n").encode("utf-8")) os.fsync(fd) # provenance is permanent — survive power loss finally: os.close(fd) @@ -1646,33 +1785,46 @@ def _journal(self, op, **fields): print("warning: journal.jsonl not writable (%s); provenance " "entry lost." % e, file=sys.stderr) + @staticmethod + def _event_mentions(event, node_id): + if node_id in (event.get("id"), event.get("old_id"), + event.get("new_id"), event.get("other")): + return True + ids = event.get("ids") + return isinstance(ids, list) and node_id in ids + def journal_entries(self, node_id=None, tail_bytes=10_000_000): """Read the provenance log, optionally filtered to one node. - The log is append-only and never cleared, so reads are capped to - the last `tail_bytes` (10MB ≈ years of normal use) — `why` and - `status` must not O(file) forever (auditor finding).""" + Targeted provenance scans the full file incrementally so `why ` + remains answerable after years without loading unrelated events. + Unfiltered status reads stay tail-capped to avoid loading an + ever-growing journal into memory.""" jf = self.path.parent / JOURNAL_FILE - if not jf.exists(): + if not jf.exists() or jf.is_symlink(): return [] - size = jf.stat().st_size - with jf.open("rb") as f: - if size > tail_bytes: + try: + fd = os.open(str(jf), os.O_RDONLY | + getattr(os, "O_NOFOLLOW", 0)) + except OSError: + return [] + with os.fdopen(fd, "rb") as f: + size = os.fstat(f.fileno()).st_size + if node_id is None and size > tail_bytes: f.seek(size - tail_bytes) - f.readline() # skip the partial line + f.readline() print("note: journal is %.1f MB; reading the last 10 MB." % (size / 1e6), file=sys.stderr) - data = f.read().decode("utf-8", "replace") - out = [] - for line in data.splitlines(): - try: - e = json.loads(line) - except json.JSONDecodeError: - continue - if node_id is None or node_id in (e.get("id"), e.get("old_id"), - e.get("new_id")) \ - or node_id in (e.get("ids") or []): - out.append(e) - return out + out = [] + for raw in f: + try: + event = json.loads(raw.decode("utf-8", "replace")) + except (json.JSONDecodeError, UnicodeDecodeError): + continue + if not isinstance(event, dict): + continue + if node_id is None or self._event_mentions(event, node_id): + out.append(event) + return out # -- temporal validity ---------------------------------------------- @staticmethod @@ -1711,10 +1863,17 @@ def _valid_at(node, at=None): class Cortex: def __init__(self, path): self.path = path - self.path.mkdir(parents=True, exist_ok=True) + if not self.path.is_symlink(): + try: + self.path.mkdir(parents=True, exist_ok=True) + except OSError: + pass def files(self): - return sorted(self.path.glob("*.md")) + if self.path.is_symlink() or not self.path.is_dir(): + return [] + return sorted(p for p in self.path.glob("*.md") + if p.is_file() and not p.is_symlink()) def promote(self, topic, content): base = re.sub(r'[^\w؀-ۿ]+', '_', topic).strip('_')[:40] @@ -1723,6 +1882,8 @@ def promote(self, topic, content): # two distinct topics can sanitize to the same filename — never # silently overwrite "durable" knowledge about a different topic # (auditor finding): disambiguate with a short content hash. + if fpath.is_symlink(): + raise ValueError("cortex target is a symlink") if fpath.exists(): first = fpath.read_text("utf-8").splitlines()[:1] if first and first[0] != "# %s" % topic: @@ -1748,7 +1909,8 @@ class Dreamer: def __init__(self, mind_dir, hippo, cortex): self.dir = mind_dir / DREAMS_DIR try: - self.dir.mkdir(parents=True, exist_ok=True) + if not self.dir.is_symlink(): + self.dir.mkdir(parents=True, exist_ok=True) except OSError: # a dangling-symlink dreams/ raises EEXIST here (exists() is # False so exist_ok can't swallow it). Constructing the Dreamer @@ -1792,35 +1954,13 @@ def dream(self, dry_run=False): # the once-a-day marker is persisted INSIDE graph.json — the old # journal-file-existence heuristic re-decayed whenever the day's # memo was deleted or lost (auditor finding, 6.2.2) - today = str(_now().date()) - first_cycle_today = self.hippo.meta.get("last_edge_decay", "") < today - if not dry_run and first_cycle_today: - for nbrs in self.hippo.edges.values(): - for e in nbrs.values(): - e["weight"] = round(e.get("weight", 1.0) * EDGE_DECAY_PER_DREAM, 4) - self.hippo.meta["last_edge_decay"] = today - # ...and synaptic pruning removes the ones that decayed away - pruned_edges = 0 - for nid in list(self.hippo.edges.keys()): - for neighbor in list(self.hippo.edges[nid].keys()): - if self.hippo.edges[nid][neighbor].get("weight", 1.0) < EDGE_PRUNE_THRESHOLD: - if not dry_run: - del self.hippo.edges[nid][neighbor] - self.hippo._pruned_edges.add((nid, neighbor)) - pruned_edges += 1 - if not dry_run and nid in self.hippo.edges and not self.hippo.edges[nid]: - del self.hippo.edges[nid] + # The once-per-day decision and weight updates happen inside the graph + # lock. A stale concurrent dream therefore cannot double-decay the day + # or overwrite a link/confirmation that landed while it was waiting. + pruned_edges = self.hippo.decay_edges(dry_run=dry_run) if pruned_edges: log.append("- synaptic pruning: %s %d weak edges." % ("would remove" if dry_run else "removed", pruned_edges)) - # persist the edge-weight changes: decay() only saves when a node is - # pruned, so on a steady-state night (no node/conflict change) the - # in-memory edge decay would be discarded and each fresh `dream` - # process would reload weight 1.0 and re-decay to no effect — edges - # would never actually weaken or prune across real runs (auditor - # finding: the claim was true in-process, false on disk). - if not dry_run: - self.hippo._save() # 3. REM: cluster related memories and promote recurring themes. # Clustering uses offline hash embeddings — deterministic, no network. @@ -1861,7 +2001,7 @@ def dream(self, dry_run=False): os.O_WRONLY | os.O_CREAT | os.O_APPEND | getattr(os, "O_NOFOLLOW", 0), 0o644) try: - os.write(fd, payload.encode("utf-8")) + _write_once(fd, payload.encode("utf-8")) os.fsync(fd) finally: os.close(fd) @@ -1870,13 +2010,17 @@ def dream(self, dry_run=False): "skipping dream journal for this run.", file=sys.stderr) return None, memo_text if self.signals_file.exists() and not self.signals_file.is_symlink(): - self.signals_file.unlink() + try: + self.signals_file.unlink() + except FileNotFoundError: + pass return str(memo.relative_to(self.hippo.path.parent)), memo_text def _rem_promote(self, log, dry_run): emb = self.hippo.embedder clusters = [] - for nid, n in self.hippo.nodes.items(): + for nid in sorted(self.hippo.nodes): + n = self.hippo.nodes[nid] if not self.hippo._valid_at(n): # closed facts don't cluster continue placed = False @@ -1918,8 +2062,9 @@ def _rem_conflicts(self, log, dry_run): emb = self.hippo.embedder # a superseded fact conflicting with its successor is not a # contradiction — it's history. Scan only currently-valid facts. - nodes = [(nid, n) for nid, n in self.hippo.nodes.items() - if self.hippo._valid_at(n)] + nodes = [(nid, self.hippo.nodes[nid]) + for nid in sorted(self.hippo.nodes) + if self.hippo._valid_at(self.hippo.nodes[nid])] N = max(1, len(nodes)) df = defaultdict(int) for _, n in nodes: @@ -1946,6 +2091,8 @@ def _rem_conflicts(self, log, dry_run): self.hippo.edges.setdefault(idb, {})[ida] = { "relation": "possible-conflict", "weight": 0.5, "created": now_iso} + self.hippo._edge_updates.update( + ((ida, idb), (idb, ida))) log.append("- possible conflict (sim %.2f):" % sim) log.append(" a: %s" % a["text"][:80]) log.append(" b: %s" % b["text"][:80]) @@ -1967,7 +2114,8 @@ def _read_signals(self): "ignoring it this cycle.", file=sys.stderr) return [] out = [] - for line in self.signals_file.read_text("utf-8").splitlines(): + for line in self.signals_file.read_text( + "utf-8", errors="replace").splitlines(): try: out.append(json.loads(line)) except json.JSONDecodeError: @@ -2000,9 +2148,10 @@ def _invocation(project_root=None): cmd = str(script) else: cmd = str(script) - if " " in cmd: - cmd = '"%s"' % cmd - return "python3 %s" % cmd + if os.name == "nt": + import subprocess + return subprocess.list2cmdline(["python3", cmd]) + return shlex.join(["python3", cmd]) # ──────────────────────────────────────────────────────────────── @@ -2029,12 +2178,30 @@ def generate(self, project_root): # working memory shows only facts that are currently TRUE: # superseded facts keep their lineage in the graph but never # occupy the agent's always-on context - nodes_sorted = sorted((n for n in self.hippo.nodes.values() - if self.hippo._valid_at(n)), - key=lambda n: -n["weight"]) + # Weight remains primary, but a saturated weight is common: newly + # remembered trivia and repeatedly useful facts can both be 1.0. + # Break those ties by earned confirmations, then recency, then id. + # Without this, making the order hash-seed-independent accidentally + # let arbitrary fresh noise displace confirmed core facts in the + # 180-day soak (5/8 hot slots instead of a usage-driven selection). + nodes_sorted = sorted( + ((nid, n) for nid, n in self.hippo.nodes.items() + if self.hippo._valid_at(n)), + key=lambda item: ( + item[1]["weight"], + item[1].get("access_count", 0), + item[1].get("last_accessed", ""), + item[0], + ), + reverse=True) hot, used = [], 0 - for n in nodes_sorted: - line = "- %s" % n["text"] + for _, n in nodes_sorted: + # Memories are data inside an instruction file. Collapse structural + # newlines and neutralize HTML-comment markers so a remembered + # string cannot forge headings or terminate our export guard. + display = " ".join(n["text"].split()) + display = display.replace("", second) + + def test_link_rejects_both_inputs_before_any_write(self): + h = self.hippo() + with self.assertRaises(ValueError): + h.link("valid first fact", " ") + self.assertEqual(Hippocampus( + self.mind_dir / "graph.json").nodes, {}) + + def test_malformed_created_and_history_are_repaired(self): + gpath = self.mind_dir / "graph.json" + gpath.write_text(json.dumps({"nodes": {"aaa": { + "text": "recoverable temporal fact", + "created": "garbage", + "last_accessed": "garbage", + "history": ["bad", {"text": "old fact", "replaced": 7}], + }}, "edges": {}}), "utf-8") + h = self.hippo() + datetime.fromisoformat(h.nodes["aaa"]["created"]) + datetime.fromisoformat(h.nodes["aaa"]["valid_from"]) + self.assertEqual(h.nodes["aaa"]["history"], + [{"text": "old fact", "replaced": "unknown"}]) + results, _, _ = h.recall("recoverable temporal fact") + self.assertTrue(results) + + def test_why_tolerates_repaired_history(self): + gpath = self.mind_dir / "graph.json" + gpath.write_text(json.dumps({"nodes": {"aaa": { + "text": "history display fact", "history": ["bad"], + }}, "edges": {}}), "utf-8") + import io + from contextlib import redirect_stdout, redirect_stderr + cwd = os.getcwd() + os.chdir(self.tmp) + try: + out, err = io.StringIO(), io.StringIO() + with redirect_stdout(out), redirect_stderr(err): + code = M.main(["why", "aaa"]) + finally: + os.chdir(cwd) + self.assertEqual(code, 0, err.getvalue()) + self.assertIn("history display fact", out.getvalue()) + + def test_link_event_is_provenance_for_both_endpoints(self): + h = self.hippo() + a = "alpha linked fact" + b = "beta linked fact" + h.link(a, b, "peer") + self.assertIn("link", [e["op"] for e in h.journal_entries(h._id(a))]) + self.assertIn("link", [e["op"] for e in h.journal_entries(h._id(b))]) + + def test_targeted_provenance_scans_beyond_journal_tail(self): + target = "oldtarget123" + jf = self.mind_dir / "journal.jsonl" + with jf.open("w", encoding="utf-8") as f: + f.write(json.dumps({"ts": "2000", "op": "remember", + "id": target, "text": "old target"}) + "\n") + filler = json.dumps({"ts": "2026", "op": "remember", + "id": "filler", "text": "x" * 900}) + "\n" + for _ in range(12000): + f.write(filler) + self.assertGreater(jf.stat().st_size, 10_000_000) + events = self.hippo().journal_entries(target) + self.assertEqual(events[0]["text"], "old target") + + def test_unfiltered_journal_reads_recent_tail_of_large_file(self): + jf = self.mind_dir / "journal.jsonl" + old = {"ts": "2000", "op": "remember", + "id": "old", "text": "outside tail"} + recent = {"ts": "2026", "op": "remember", + "id": "recent", "text": "inside tail"} + filler = json.dumps({"ts": "2026", "op": "remember", + "id": "filler", "text": "x" * 900}) + "\n" + with jf.open("w", encoding="utf-8") as f: + f.write(json.dumps(old) + "\n") + for _ in range(12000): + f.write(filler) + f.write(json.dumps(recent) + "\n") + self.assertGreater(jf.stat().st_size, 10_000_000) + events = self.hippo().journal_entries() + self.assertNotIn("old", [e.get("id") for e in events]) + self.assertEqual(events[-1]["id"], "recent") + + @unittest.skipIf(os.name == "nt", "Windows symlinks require privileges") + def test_journal_read_refuses_symlink(self): + outside = self.tmp / "outside.jsonl" + outside.write_text(json.dumps( + {"op": "remember", "id": "secret", "text": "outside"}) + "\n", + "utf-8") + (self.mind_dir / "journal.jsonl").symlink_to(outside) + self.assertEqual(self.hippo().journal_entries("secret"), []) + + @unittest.skipIf(os.name == "nt", "Windows symlinks require privileges") + def test_auto_dream_ignores_symlinked_signal_file(self): + outside = self.tmp / "outside-signals.jsonl" + outside.write_text( + "".join(json.dumps({"op": "remember"}) + "\n" + for _ in range(M.AUTO_DREAM_SIGNALS)), + "utf-8") + (self.mind_dir / "signals.jsonl").symlink_to(outside) + mind = Mind(self.tmp) + mind._ensure() + + def must_not_run(*_args, **_kwargs): + raise AssertionError("auto-dream followed a symlinked signal file") + + mind.dreamer.dream = must_not_run + self.assertFalse(mind._auto_dream()) + + def test_link_refreshes_active_without_auto_dream(self): + import io + from contextlib import redirect_stdout, redirect_stderr + old = os.environ.get("MIND_AUTO_DREAM") + os.environ["MIND_AUTO_DREAM"] = "0" + cwd = os.getcwd() + os.chdir(self.tmp) + try: + with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): + M.main(["init"]) + M.main(["link", "alpha fresh link memory", + "beta fresh link memory", "peer"]) + finally: + os.chdir(cwd) + if old is None: + os.environ.pop("MIND_AUTO_DREAM", None) + else: + os.environ["MIND_AUTO_DREAM"] = old + active = (self.mind_dir / "ACTIVE.md").read_text("utf-8") + self.assertIn("alpha fresh link memory", active) + self.assertIn("beta fresh link memory", active) + + def test_confirm_refreshes_hot_memory_order(self): + import io + from contextlib import redirect_stdout, redirect_stderr + old = os.environ.get("MIND_AUTO_DREAM") + os.environ["MIND_AUTO_DREAM"] = "0" + cwd = os.getcwd() + os.chdir(self.tmp) + try: + with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): + M.main(["init"]) + M.main(["remember", "alpha lower weight fact"]) + M.main(["remember", "beta higher weight fact"]) + h = self.hippo() + a = h._id("alpha lower weight fact") + b = h._id("beta higher weight fact") + h.nodes[a]["weight"] = 0.70 + h.nodes[b]["weight"] = 0.80 + h._dirty.update((a, b)) + h._save() + with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): + Mind(self.tmp).confirm([a]) + finally: + os.chdir(cwd) + if old is None: + os.environ.pop("MIND_AUTO_DREAM", None) + else: + os.environ["MIND_AUTO_DREAM"] = old + hot = (self.mind_dir / "ACTIVE.md").read_text("utf-8") + hot = hot.split("## Hot memories")[1].split("##")[0] + self.assertLess(hot.index("alpha lower"), hot.index("beta higher")) + + def test_hot_weight_ties_prefer_confirmed_memories(self): + h = self.hippo() + useful = [] + for i in range(8): + nid = h.remember("confirmed core memory %d" % i) + h.bump([nid]) + useful.append(h.nodes[nid]["text"]) + for i in range(4): + h.remember("fresh unconfirmed trivia %d" % i) + Active(self.mind_dir, h, Cortex( + self.mind_dir / "cortex")).generate(self.tmp) + hot = (self.mind_dir / "ACTIVE.md").read_text("utf-8") + hot = hot.split("## Hot memories")[1].split("##")[0] + self.assertTrue(all(text in hot for text in useful)) + self.assertNotIn("fresh unconfirmed trivia", hot) + + def test_duplicate_confirm_id_reinforces_once(self): + h = self.hippo() + nid = h.remember("deduplicated confirm target") + h.bump([nid, nid]) + self.assertEqual(h.nodes[nid]["access_count"], 1) + + def test_recall_ties_are_hash_seed_independent(self): + import subprocess + gpath = self.mind_dir / "graph.json" + nodes = {} + for nid, text in (("111111111111", "alpha red fact"), + ("222222222222", "alpha blue fact")): + nodes[nid] = { + "text": text, "weight": 1.0, "peak_weight": 1.0, + "confidence": 1.0, "access_count": 0, + "keys": ["alpha", "fact"], + "last_accessed": "2026-01-01T00:00:00", + "created": "2026-01-01T00:00:00", + "origin": {"by": "x"}, + "valid_from": "2026-01-01T00:00:00", "valid_to": None, + } + gpath.write_text(json.dumps({"nodes": nodes, "edges": {}}), "utf-8") + root = str(Path(__file__).resolve().parents[1]) + snippet = ( + "import sys;from pathlib import Path;sys.path.insert(0,%r);" + "import mind;h=mind.Hippocampus(Path(%r));" + "print(h.recall('alpha fact')[0][0][0])" % (root, str(gpath))) + outputs = [] + for seed in ("0", "1", "3", "7"): + result = subprocess.run( + [sys.executable, "-c", snippet], + env=dict(os.environ, PYTHONHASHSEED=seed), + capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + outputs.append(result.stdout.strip()) + self.assertEqual(len(set(outputs)), 1) + + def test_help_and_usage_errors_carry_real_script_path(self): + import subprocess + tools = self.tmp / "tools with space" + project = self.tmp / "project" + tools.mkdir() + project.mkdir() + script = tools / "mind.py" + shutil.copy(str(Path(__file__).resolve().parents[1] / "mind.py"), + str(script)) + help_result = subprocess.run( + [sys.executable, str(script), "--help"], cwd=str(project), + capture_output=True, text=True) + self.assertIn(str(script.resolve()), help_result.stdout) + bad = subprocess.run( + [sys.executable, str(script), "status", "extra"], + cwd=str(project), capture_output=True, text=True) + self.assertEqual(bad.returncode, 2) + self.assertIn(str(script.resolve()), bad.stderr) + + def test_hostile_loaded_text_and_actor_metadata_are_sanitized(self): + gpath = self.mind_dir / "graph.json" + gpath.write_text(json.dumps({"nodes": {"aaa": { + "text": "safe\u202e\x1b[31m fact", + }}, "edges": {}}), "utf-8") + h = self.hippo() + self.assertNotIn("\u202e", h.nodes["aaa"]["text"]) + self.assertNotIn("\x1b", h.nodes["aaa"]["text"]) + old_by = os.environ.get("MIND_BY") + old_session = os.environ.get("MIND_SESSION") + os.environ["MIND_BY"] = "writer\n" + "x" * 500 + os.environ["MIND_SESSION"] = "session\n" + "y" * 500 + try: + nid = h.remember("actor metadata target") + finally: + if old_by is None: + os.environ.pop("MIND_BY", None) + else: + os.environ["MIND_BY"] = old_by + if old_session is None: + os.environ.pop("MIND_SESSION", None) + else: + os.environ["MIND_SESSION"] = old_session + origin = h.nodes[nid]["origin"] + self.assertLessEqual(len(origin["by"]), 80) + self.assertLessEqual(len(origin["session"]), 120) + self.assertNotIn("\n", origin["by"] + origin["session"]) + + if __name__ == "__main__": unittest.main(verbosity=2)