Add a new business memory kind end-to-end. Pick the storage combination (Markdown / SQLite / LanceDB), pick the markdown strategy (daily-log / skill-named / single-file), then wire up the schema(s), repo(s), and writer(s).
60
70%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Fix and improve this skill with Tessl
tessl review fix ./.claude/skills/add-memory-kind/SKILL.mdAdding a new persisted business entity (Episode, Case, Skill, AtomicFact, Foresight, Profile, or something custom). Multiple storage layers may be involved; this skill walks the decision then the wiring.
A memory kind does not have to use all three layers. Pick by what the kind actually needs:
| Need | Markdown | SQLite | LanceDB |
|---|---|---|---|
| Human-readable / agent-editable source-of-truth text | ✅ | ||
| Structured state, ACID transactions, joins, predicates | ✅ | ||
| Vector / BM25 / hybrid retrieval | ✅ |
Common combinations seen in EverOS:
| Combo | Example | Rationale |
|---|---|---|
| md only | scratch notes / dump bins | text-of-truth, no index needed |
| md + lancedb | episode / memcell / case | text-of-truth + semantic retrieval |
| md + sqlite | profile / playbook / soul.md state | text-of-truth + structured state to query |
| md + sqlite + lancedb | full-blown business records | when you need both transactional state AND retrieval |
| sqlite only | audit log / task queue / LSN watermark | system state, never user-facing |
| lancedb only | rare; usually you still want md | derived embeddings without text-of-truth |
Rule of thumb: markdown is the truth; sqlite and lancedb are derived indexes that can be rebuilt from md. Drop md only when the kind has no human-readable form (pure system state).
Three strategies — declared in the EverOS Markdown First spec:
| Strategy | Filename | Mutation | Examples |
|---|---|---|---|
| Daily-log append | <prefix>-YYYY-MM-DD.md | append entries | memcell / episode / case / atomic_fact / foresight |
| Skill-named in-place | skill_<name>.md | overwrite the file | skills (procedural memory) |
| Single-file rewrite | user.md / agent.md / soul.md / behaviors.md / tools.md | overwrite the file | profiles / playbooks |
This skill currently has a complete recipe for daily-log append.
Skill-named and single-file recipes are sketched at the bottom — their
base writers (BaseSkillWriter / BaseProfileWriter) land later in the
project; until then build a thin wrapper over MarkdownWriter
directly.
infra/persistence/markdown/mds/<name>.py"""Episode daily-log frontmatter."""
from __future__ import annotations
import datetime as _dt
from typing import ClassVar, Literal
from everos.core.persistence.markdown import UserScopedFrontmatter
class UserEpisodeDailyFrontmatter(UserScopedFrontmatter):
"""``users/<u>/episodes/episode-<YYYY-MM-DD>.md``."""
ENTRY_ID_PREFIX: ClassVar[str] = "ep"
DIR_NAME: ClassVar[str] = "episodes"
FILE_PREFIX: ClassVar[str] = "episode"
type: Literal["user_episode_daily"] = "user_episode_daily"
date: _dt.date
entry_count: int = 0
last_appended_at: _dt.datetime | None = NoneFor agent-track kinds subclass AgentScopedFrontmatter instead. If
user-track and agent-track share a kind name (e.g. memcell), give
each a distinct ENTRY_ID_PREFIX (e.g. umc vs amc) so reverse
lookup is unambiguous.
mds/__init__.pyfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatterinfra/persistence/markdown/writers/<name>.py"""Episode appender."""
from __future__ import annotations
from pathlib import Path
from everos.core.persistence import MarkdownReader
from ..mds import UserEpisodeDailyFrontmatter
from .base import BaseDailyWriter
class UserEpisodeAppender(BaseDailyWriter):
schema = UserEpisodeDailyFrontmatter
# OPTIONAL: override the count strategy. Default is len(entries);
# override to trust the frontmatter field instead.
def _current_count(self, path: Path) -> int:
if not path.exists():
return 0
return MarkdownReader.read(path).frontmatter.get("entry_count", 0)writers/__init__.pyfrom .episode import UserEpisodeAppender as UserEpisodeAppenderfrom everos.infra.persistence.markdown.writers import UserEpisodeAppender
appender = UserEpisodeAppender(memory_root)
eid = appender.append("u_jason", "I went to the doctor today.")
# → users/u_jason/episodes/episode-<today>.md
# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)Skip this section if the kind doesn't need structured state beyond markdown.
infra/persistence/sqlite/tables/<name>.pyfrom everos.core.persistence.sqlite import BaseTable, Field
class EpisodeState(BaseTable, table=True):
__tablename__ = "episode_state" # type: ignore[assignment]
id: int | None = Field(default=None, primary_key=True)
entry_id: str = Field(index=True, unique=True)
cluster_id: str | None = Field(default=None, index=True)
status: str = Field(default="active")BaseTable already provides created_at / updated_at (auto-bumped).
tables/__init__.pyfrom .episode import EpisodeState as EpisodeStateinfra/persistence/sqlite/repos/<name>.pyfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from everos.core.persistence.sqlite import RepoBase
from ..sqlite_manager import get_session_factory
from ..tables import EpisodeState
class _EpisodeStateRepo(RepoBase[EpisodeState]):
model = EpisodeState
def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:
return get_session_factory()
episode_state_repo = _EpisodeStateRepo()repos/__init__.pyfrom .episode import episode_state_repo as episode_state_repoSkip this section if the kind doesn't need vector / BM25 / hybrid retrieval.
infra/persistence/lancedb/tables/<name>.pyfrom everos.core.persistence.lancedb import BaseLanceTable, Vector
class EpisodeIndex(BaseLanceTable):
entry_id: str
text: str
tags: list[str]
vector: Vector(384) # type: ignore[valid-type]Vector(N) must match your embedding dimension.
tables/__init__.pyfrom .episode import EpisodeIndex as EpisodeIndexinfra/persistence/lancedb/repos/<name>.pyfrom lancedb import AsyncTable
from everos.core.persistence.lancedb import LanceRepoBase
from ..lancedb_manager import get_table
from ..tables import EpisodeIndex
class _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):
schema = EpisodeIndex
table_name = "episode_index"
async def _table_lookup(self) -> AsyncTable:
return await get_table(self.table_name, self.schema)
episode_index_repo = _EpisodeIndexRepo()repos/__init__.pyfrom .episode import episode_index_repo as episode_index_repoWhen the new memory kind needs:
BaseSkillWriter, or use MarkdownWriter.write_markdown directly
with a thin wrapper.user.md) — wait for
BaseProfileWriter, same fallback.These strategies do not use entry markers; their frontmatter schema
does not need ENTRY_ID_PREFIX (only id / type / schema_version plus
the scope mixin fields).
make lint — ruff + import-linter cleanmake test — existing manager / lifespan / writer tests still passTests by layer:
| Tests for | Location |
|---|---|
| Markdown frontmatter schema | tests/unit/test_infra/test_markdown/test_mds/ |
| Markdown business appender | tests/unit/test_infra/test_markdown/test_writers/ |
| SQLite RepoBase logic | tests/unit/test_core/test_persistence/test_sqlite/ |
| SQLite manager / lifespan | tests/unit/test_infra/test_sqlite/ |
| LanceDB LanceRepoBase logic | tests/unit/test_core/test_persistence/test_lancedb/ |
| LanceDB manager / lifespan | tests/unit/test_infra/test_lancedb/ |
| Mistake | Symptom | Fix |
|---|---|---|
Forgot ENTRY_ID_PREFIX / DIR_NAME / FILE_PREFIX on a daily-log schema | BaseDailyWriter.__init__ raises TypeError | Add all three ClassVars |
Same ENTRY_ID_PREFIX on user + agent variants | MemoryLayout.locate_for_entry collision error | Use distinct prefixes (e.g. umc vs amc) |
Imported RepoBase from infra.persistence.sqlite | ImportError | Lives in core.persistence.sqlite (moved earlier) |
| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each __init__.py |
Vector(N) mismatched with embedding dim | LanceDB raises on insert | Make N exactly match the model output |
Imported MemoryLayout from a writer (infra) | import-linter fails (infra → memory reverse dep) | Use MemoryRoot (in core) and let the schema's ClassVars drive paths |
Hand-rolling datetime.now() instead of today_with_timezone() | Day-boundary drift across timezones | Always go through everos.component.utils.datetime |
__init__.py re-export rules: ../../rules/init-py-and-reexport.md6ff07ef
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.