Skip to content

[VEPEAGE-000] Procedural memory: skill schema, session-grounded experience distillation, and tool-message support#136

Open
nanxingw wants to merge 7 commits into
mainfrom
feat/procedural-memory-main-pr
Open

[VEPEAGE-000] Procedural memory: skill schema, session-grounded experience distillation, and tool-message support#136
nanxingw wants to merge 7 commits into
mainfrom
feat/procedural-memory-main-pr

Conversation

@nanxingw

@nanxingw nanxingw commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rework procedural memory into a skill-based schema with session-grounded experience distillation, and make tool activity a first-class learning signal end-to-end.

  • Skill schema: procedural_memory moves from flat summary/steps to name / description / instructions / triggers / examples / version with per-user unique skill names (DB-level UNIQUE (organization_id, user_id, name)).
  • Session conversation store: /memory/add and /memory/add_sync persist real per-role turns (user / assistant / tool) into a dedicated Conversation Message Store keyed by session_id — the single source of truth for skill distillation. The write is additive: a store failure degrades to a missed session, never a failed memory-add.
  • Experience distillation & evolution: sealed sessions are distilled into worth_learning / worth_avoiding skill experiences (importance × credibility prioritization), which evolve skills through the auto-dream procedural flow (POST /memory/auto_dream, mode: "procedural").
  • Tool messages ingest end-to-end: role: "tool" / "function" turns are stored (with [tool_name] prefixes), assistant tool_calls (OpenAI shape) are serialized into turn content, and the meta-agent role-collapse marks them [TOOL] — the distiller prompt's tool_error signal now actually receives tool turns instead of silently never matching.
  • Security fixes: bulk memory-erasure endpoints authenticated + tenant-scoped; client erasure 404 anti-probing semantics; PII lifecycle (conversation turns pruned after distillation per MIRIX_CONVERSATION_RETENTION_DAYS, erasure paths wired).
  • Migration hardening: scripts/migrate_procedural_to_skill.sql verified end-to-end on real Postgres against adversarial legacy data; startup fail-fast (_find_missing_columns) names the migration script when a legacy DB is detected; stale pre-skill Redis index is dropped and rebuilt.

Upgrade notes (existing deployments)

  1. Back up the database before upgrading. The migration renames/retypes columns in place and is not reversible by script.
  2. Run scripts/migrate_procedural_to_skill.sql once before deploying (the server fail-fasts on an un-migrated schema and names the script). New installs need nothing — create_all builds the final schema.
  3. New session-store migrations: migrate_add_conversation_message.sql (+_phase2), migrate_add_message_session_id.sql (+_phase2), migrate_add_skill_experience.sql, migrate_add_agent_trigger_state.sql. The _phase2 files use CREATE INDEX CONCURRENTLY and must run outside a transaction, after their phase-1 counterparts.
  4. New env knobs: MIRIX_DISTILLER_MAX_TRANSCRIPT_CHARS (default 80000), MIRIX_DISTILLER_MAX_MESSAGE_CHARS (default 8000), MIRIX_CONVERSATION_RETENTION_DAYS (default 30; 0 = keep forever).

Verification

  • pytest tests -q: 682 passed, 56 skipped (146 deselected = integration tests requiring live Postgres; recommend running them in CI: pytest -m integration).
  • Migration script exercised end-to-end on postgres:16-alpine with adversarial legacy data — whitespace/punctuation/NULL summaries, three-way slug collisions crossing a pre-existing suffixed name, legacy entry_type variants, >60-char summaries, cross-org isolation, and an empty table — 13/13 assertions pass.
  • Dashboard npm run build ✓; git diff --check ✓; ruff clean on changed files.
  • Multi-round adversarial review: two-axis (standards/spec) + three specialist (regression/API/security) agent review with per-finding adversarial verification, plus five codex review rounds — all confirmed findings fixed.

Known follow-ups (deliberately out of scope)

  • X-Client-ID keyless auth: the platform's existing keyless mode trusts this header on all endpoints; tightening it is a platform-level auth-model change and belongs in its own PR.
  • No unified migration runner/ordering manifest for the raw-SQL migrations (order documented above and in file comments).
  • distill_sessions fans out one LLM call per session without a concurrency cap (bounded by last_n_sessions, default 5).
  • Long-session distillation keeps head+tail and elides the middle; alternatives (chunked distill-merge, error-signal-priority retention) were discussed but not implemented.

🤖 Generated with Claude Code

nanxingw and others added 7 commits July 6, 2026 20:49
…ion evolution

Blocking/regression fixes:
- migrate_procedural_to_skill.sql: ALTER steps to TEXT before textification
  (assigning text to a json column fails at plan time with 42804 and aborted
  the whole migration on every existing database); normalize legacy free-form
  entry_type values into {workflow, guide, script}
- procedural entry_type validation: lenient normalize on read (to_pydantic
  runs validators via from_attributes; legacy rows like 'process' must not
  crash reads) while the write path stays strict in tool_validators
- startup schema-drift guard: fail fast with the exact migration scripts to
  run when an existing DB is missing ORM columns (create_all never ALTERs);
  README gains the upgrade-migration runbook
- Redis procedural index: detect a stale pre-skill index (no
  description_embedding attribute), drop index-only and rebuild so skill
  search doesn't silently fall back to PostgreSQL forever

Data lifecycle (PII/erasure):
- conversation_message + skill_experience now covered by the user/client
  memory purge paths (delete_by_user_id / delete_by_client_id)
- distilled conversation turns are pruned after
  MIRIX_CONVERSATION_RETENTION_DAYS (default 30, 0 = keep forever) at the end
  of each procedural distillation pass
- skill experiences carry client attribution (created_by_id audit column)

Interface:
- SDK search()/search_all_users(): search_method now defaults to None and is
  omitted from the request so the server per-type default (procedural ->
  hybrid) applies; docstrings document hybrid; local retrieve_memory mirrors
  the same per-type resolution
- restore DELETE /memory/procedural/{memory_id} for parity with the other
  memory types (skill writes remain evolution-only, no public write routes)
- drop the unreachable memory-agent tool_calls[:1] truncation and the unused
  AgentTriggerStateManager.get_state

Structure/hygiene:
- shared self-evicting KeyedLocks registry replaces three hand-rolled
  dict[key, asyncio.Lock] registries (fixes the unbounded raw-memory lock
  growth)
- get_server singleton moved to mirix.server.server; services no longer
  import the REST module
- shared owner_org + _ingest_session_turns helpers replace duplicated blocks;
  roadmap jargon (Goal-2/3, C3/C4) replaced with domain terms

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e endpoints

Codex review findings on the previous fix batch:
- DELETE /users/{user_id}/memories and DELETE /clients/{client_id}/memories
  performed an irreversible cross-table hard delete (now including verbatim
  conversation transcripts and skill experiences) with NO authentication and
  no tenant check — any caller who knew a foreign user_id/client_id could
  erase that tenant's data. Both endpoints now resolve the caller via
  JWT/Client API Key and 404 on cross-organization targets (same response as
  a missing id, so they can't be used as an existence oracle).
- Align the migration SQL entry_type mapping with normalize_entry_type():
  any legacy value containing "how" maps to 'guide' in both places, so the
  read-time normalizer and the migrated column can never disagree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry erasure

Codex re-review of the previous fix:
- DELETE /clients/{client_id}/memories compared the target to None, but
  client_manager.get_client_by_id RAISES NoResultFound on a miss (it never
  returns None), so a missing client_id fell through to a 500 while a
  cross-org client_id returned 404 — a distinguishable response that leaks
  client-id existence. Catch the miss and return the same 404 as the
  cross-org case, matching the user endpoint.
- test_deletion_apis: pass the test client's API key on the two now-
  authenticated /memories DELETE calls.

Not changed (out of scope): the keyless X-Client-ID auth mode is the
platform's documented trusted-network model used by the official SDK
(remote_client sends X-Client-ID when constructed without an api_key) and
by every memory endpoint; tightening it is a platform-wide auth-model
decision, not part of this feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…configurable transcript budgets

The distiller prompt has always treated tool errors/retries as strong
learning signals (signal_type: tool_error), but the ingestion seam dropped
every non-user/assistant message — the distiller never saw the work process
it was designed to learn from. Close that design/implementation gap:

- conversation_message role space: 'user' | 'assistant' | 'tool' (Literal
  change only; the ORM column is a plain String, no DB migration needed)
- _extract_conversation_turns keeps tool results (role 'tool'/'function',
  name-prefixed) and serializes assistant tool_calls (OpenAI shape) into the
  turn content; a pure tool-call assistant turn with empty content still
  yields a turn; [{"type":"text","text":...}] parts flatten to clean text
- role-collapse branches label tool/function messages [TOOL] instead of
  mislabeling them [ASSISTANT] for the meta-agent path
- transcript budgets are env-configurable and sized for tool-heavy sessions:
  MIRIX_DISTILLER_MAX_TRANSCRIPT_CHARS (default 80000, was hardcoded 16000)
  and MIRIX_DISTILLER_MAX_MESSAGE_CHARS (default 8000, was 2000)
- remove the orphaned round-based session_distiller.txt prompt (zero
  references; the production distiller uses auto_dream_agent/procedural.txt)
- README + SDK docstring document how to pass tool activity

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd malformed input

Two failure modes found while self-reviewing the tool-activity change:
- extraction ran OUTSIDE the additive try/except in _ingest_session_turns, so
  a malformed message shape could abort the primary memory add; it now runs
  inside the guard (degrades to a missed session, never a dropped add)
- one oversized turn (typically a huge tool result) failed the whole batch's
  Pydantic validation in record_turns, silently dropping every turn of the
  request; extraction now truncates at the store's per-row content cap with
  a visible marker

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fix collisions

Two defects in migrate_procedural_to_skill.sql, both reproduced end-to-end
against real Postgres 15/16 before the fix:

1. Step 2 slugification aborted the WHOLE migration on any row whose summary
   slugs to '' (whitespace-only, punctuation-only): NULLIF assigned NULL into
   the NOT NULL name column before the fallback UPDATE could run — NOT NULL is
   checked per row at assignment time, so the two-statement "set NULL, then
   patch" shape can never work. Slug and fallback are now one COALESCE in a
   single statement.

2. Step 2b's single-pass -N suffixing could mint a name that collides with a
   PRE-EXISTING slug (two 'task' rows produce 'task-2' while a 'task 2'
   summary already slugged to 'task-2'), failing the final UNIQUE constraint.
   The dedup now loops until no row needs a rename; terminates because renamed
   names strictly lengthen each pass.

Verified against postgres:16-alpine with adversarial legacy data (whitespace/
punctuation/NULL summaries, 3-way slug collisions crossing a pre-existing
suffixed name, legacy entry_type variants, >60-char summaries, cross-org
isolation) and on an empty table: 13/13 assertions pass.

Also close the two test gaps flagged in the ship-readiness review:
- _ingest_session_turns isolation guard rails (store failure, extraction
  failure, no-session_id skip, session stamping + turn recording) — pins the
  "additive write must never abort the primary memory add" contract.
- Distiller _render_transcript budgets: per-turn cap marker, whole-transcript
  head/tail elision, and the untouched-within-budget case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants