{"owner":"EverMind-AI","repo":"EverOS","hasSkills":true,"totalSkillsCount":20,"totalTokensCount":26010,"categories":["anthropic-skill","claude-rule","project-spec","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".claude/rules/architecture.md",".claude/rules/async-programming.md",".claude/rules/code-style.md",".claude/rules/datetime-handling.md",".claude/rules/imports.md",".claude/rules/init-py-and-reexport.md",".claude/rules/language-policy.md",".claude/rules/logging-observability.md",".claude/rules/module-docstring.md",".claude/rules/testing.md",".claude/skills/add-memory-kind/SKILL.md",".claude/skills/commit/SKILL.md",".claude/skills/new-branch/SKILL.md",".claude/skills/pr/SKILL.md",".claude/skills/release/SKILL.md","CLAUDE.md","docs/api.md","docs/architecture.md","use-cases/claude-code-plugin/plugin.json","use-cases/claude-code-plugin/skills/memory-tools.md"],"skills":{".claude/rules/architecture.md":"# Architecture rule (always loaded)\n\nEverOS is a DDD-layered framework. The dependency direction is **single, downward only**:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — CLI + HTTP API (presentation). No business logic.\n- `service/` — use-case orchestration (memorize / retrieve / evolve / manage).\n- `memory/` — domain (extract / search / cascade / prompt_slots / models).\n- `infra/` — storage adapters (markdown + sqlite + lancedb) and the OME subsystem.\n- `component/` — injectable providers (llm / embedding / config / utils).\n- `core/` — runtime base (observability / lifespan / context / persistence primitives).\n- `config/` — configuration data (Settings + default TOML).\n\n## Hard constraints (enforced by `import-linter`, run in `make lint`)\n\n1. **Layering**: an outer layer may import an inner layer, never the reverse.\n   `entrypoints → service → memory → infra`.\n2. **Private internals**: `service`, `memory`, and `entrypoints` must not import\n   `infra.persistence.{markdown,lancedb,sqlite}.**` internals — go through the\n   package facade (`from everos.infra.persistence.markdown import ...`).\n3. **OME isolation**: `infra.ome` must not import `persistence`, `memory`,\n   `service`, or `entrypoints`. It is a low-level scheduler with no domain knowledge.\n\nIf a change needs to cross a boundary the wrong way, the design is wrong — refactor,\ndon't add an exception.\n\nFull rationale: [docs/architecture.md](../../docs/architecture.md).\n",".claude/rules/async-programming.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Async programming rule\n\nThe write/read paths are async end-to-end. Keep them non-blocking.\n\n- **No blocking calls in async functions** — no synchronous file I/O, no `time.sleep`,\n  no blocking DB/network calls inside `async def`. Ruff `ASYNC` flags the common cases.\n- **Offload CPU/blocking work** with `anyio.to_thread.run_sync` (or the established\n  helper) rather than blocking the event loop.\n- **Concurrency** via `asyncio.gather` / `asyncio.TaskGroup` for independent awaits;\n  don't `await` in a loop when the calls are independent.\n- **Tests**: `pytest-asyncio` is in `auto` mode — an `async def test_*` just works,\n  no `@pytest.mark.asyncio` needed.\n- **Don't fire-and-forget** without holding a reference (`asyncio.create_task` results\n  must be tracked, or you lose exceptions). The OME subsystem owns the long-running\n  background loops — application code shouldn't spawn its own.\n",".claude/rules/code-style.md":"# Code style rule (always loaded)\n\n- **Formatter & linter**: `ruff` is the single tool (replaces black / isort / flake8).\n  Line length 88, target `py312`. Run `make format` to auto-fix; `make lint` checks.\n- **Active ruff rule sets**: `E F I N UP B SIM ASYNC`. Don't disable a rule inline\n  unless there's a genuine reason — prefer fixing the code.\n- **Type hints**: annotate every public function signature (params + return). The\n  codebase is ~100% typed; keep it that way.\n- **`from __future__ import annotations`** at the top of every module — annotations\n  are strings, so forward refs and `X | None` unions are free.\n- **Prefer `collections.abc`** (`Sequence`, `Mapping`) over concrete `list`/`dict`\n  in signatures; use `Protocol` for structural interfaces.\n- **No dead code**: no commented-out blocks, no unused imports, no speculative\n  abstractions. Delete rather than comment out.\n- **Naming**: `*Manager` (orchestrators), `*Provider` (injectable services),\n  `*Reader`/`*Writer` (persistence), `*Recaller` (search routes). Follow the\n  established suffix when adding a sibling.\n",".claude/rules/datetime-handling.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Datetime handling rule (two-zone discipline)\n\n**Never** construct or read \"now\" directly. All datetime flows through\n`everos.component.utils.datetime`. This is a **hard CI gate**\n(`make check-datetime`, wired into `make lint`).\n\n## Banned (the checker fails the build on these)\n\n- `datetime.now()`, `datetime.utcnow()`, `datetime.today()`\n- `time.time()`, `time.time_ns()`\n- `datetime(YYYY, ...)` without `tzinfo=`\n- `.astimezone(...)` / `.replace(tzinfo=...)` outside the helper module\n\n## Use instead\n\n| Need | Helper |\n|---|---|\n| \"now\" for **storage** (UTC) | `get_utc_now()` |\n| \"now\" for **display** (configured TZ) | `get_now_with_timezone()` |\n| today's date, display TZ | `today_with_timezone()` |\n| normalize a value to UTC | `ensure_utc(d)` |\n| render to display TZ | `to_display_tz(d)` |\n| parse ISO / epoch / str | `from_iso_format(v)`, `from_timestamp(ts)` |\n| serialize | `to_iso_format(d)`, `to_date_str(d)`, `to_timestamp_ms(d)` |\n\n**Two zones**: persist in UTC, present in the configured display TZ. Crossing them\ngoes through the helpers — never ad-hoc. See [docs/datetime.md](../../docs/datetime.md).\n",".claude/rules/imports.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Imports rule\n\n- **`from __future__ import annotations`** is the first import in every module.\n- **Import order** (ruff `I` enforces, `make format` fixes): stdlib → third-party\n  → first-party (`everalgo`, then `everos`). One group per blank-line-separated block.\n- **Absolute imports** for cross-package references (`from everos.memory import ...`).\n  Relative imports (`from .models import ...`) only **within** a package, typically\n  in its `__init__.py`.\n- **`TYPE_CHECKING` guard** for import cycles and type-only imports:\n  ```python\n  from typing import TYPE_CHECKING\n  if TYPE_CHECKING:\n      from everos.config import Settings\n  ```\n- Never import a private internal across a package boundary — respect the\n  `import-linter` contracts (see [architecture.md](architecture.md)).\n",".claude/rules/init-py-and-reexport.md":"---\npaths:\n  - \"src/**/__init__.py\"\n  - \"src/**/*.py\"\n---\n\n# `__init__.py` and re-export rule\n\nA package's `__init__.py` is its **public facade**. Consumers import from the\npackage, never from its internal modules.\n\n## Pattern\n\n```python\n\"\"\"One-paragraph module docstring: what this package is and how to use it.\"\"\"\n\nfrom .models import Episode as Episode\nfrom .models import MemCell as MemCell\n\n__all__ = [\n    \"Episode\",\n    \"MemCell\",\n]\n```\n\n- **Explicit `X as X` redundant-alias form** on each re-export. This is intentional:\n  it marks the name as a deliberate public re-export (ruff `F401` / `PLC0414` aware)\n  rather than an accidental unused import.\n- **`__all__`** lists every public name, alphabetically sorted, matching the\n  re-exports. It is the contract; keep it in sync.\n- **Internal modules stay private** — don't re-export helpers that aren't part of\n  the public API.\n- New subpackage? Add an `__init__.py` with a docstring + `__all__` even if it\n  starts small. Empty-but-documented beats missing.\n\nThis facade discipline is what lets `import-linter` forbid deep imports across\npackage boundaries (see [architecture.md](architecture.md)).\n",".claude/rules/language-policy.md":"# Language policy rule (always loaded)\n\nThe project targets a global audience and is **English-first**.\n\n- **Code, comments, docstrings, docs, commit messages, identifiers**: English only.\n- **CJK characters are allowed only in**:\n  - test fixtures under `tests/` and `data/` (multilingual input is the point), and\n  - locale-suffixed mirror files (e.g. `*_zh.json`).\n- Do **not** introduce CJK into `src/`, `docs/`, or config.\n\nEnforcement: `make check-cjk` scans for stray CJK outside the allowlist (advisory).\nKeep user-facing strings and error messages in English.\n",".claude/rules/logging-observability.md":"---\npaths:\n  - \"src/**/*.py\"\n---\n\n# Logging & observability rule\n\n- **Use the project logger**, never `print` or the stdlib `logging` directly:\n  ```python\n  from everos.core.observability.logging import get_logger\n  logger = get_logger(__name__)\n  ```\n- **Structured logging** (`structlog`): pass context as keyword fields, not f-strings.\n  ```python\n  logger.info(\"memory.search.completed\", owner_type=owner, n_results=len(items))\n  ```\n  Event name first (dotted, stable), structured kwargs after. This keeps logs\n  queryable and avoids leaking interpolated PII into the message string.\n- **Levels**: `debug` for developer detail, `info` for lifecycle milestones,\n  `warning` for recoverable anomalies, `error` for failures with a stack/context.\n- **Metrics** go through `core.observability.metrics` (Prometheus); don't invent\n  ad-hoc counters. Histograms/counters/gauges have registry helpers.\n- Don't log secrets, API keys, or full memory content at `info`/above.\n- **Tracing** (optional, `[otel]` extra, **off by default**): open spans with\n  `memory_span(...)` from `core.observability.tracing` — it stamps the Langfuse\n  `langfuse.*` attributes and is a no-op until `[observability] enabled`, so call\n  sites never branch on config. LLM / embedding token usage rides\n  `set_generation_usage` onto the active span (Langfuse computes cost).\n  Request/response content is emitted only when `capture_content` is on\n  (redaction hook + truncation). `request_id` is kept independent of the OTel\n  `trace_id`; an upstream `traceparent` header is continued when present.\n",".claude/rules/module-docstring.md":"---\npaths:\n  - \"src/everos/infra/**/*.py\"\n  - \"src/everos/memory/**/*.py\"\n  - \"src/everos/service/**/*.py\"\n  - \"src/everos/component/**/*.py\"\n  - \"src/everos/core/**/*.py\"\n---\n\n# Module docstring rule\n\nEvery non-trivial module in the domain/infra layers opens with a docstring that\nexplains **intent and contract**, not just a one-line label.\n\nA good module docstring states:\n\n- **What** the module is responsible for (one sentence).\n- **The load-bearing invariants** — the rules a reader must know to change it\n  safely (partition keys, what is/isn't written, defaults, ignored flags).\n- **External usage** when the module is a package facade (a short import example).\n\nExample (abbreviated, from `memory/search/manager.py`):\n\n```python\n\"\"\"SearchManager — top-level orchestrator for POST /api/v2/memory/search.\n\nHard partition by owner_type: user → episodes (+ profiles), agent →\nagent_cases + agent_skills. The manager never writes to storage; it only\nreads LanceDB + markdown.\n\"\"\"\n```\n\nPrefer prose that would save the next engineer a debugging session over\nboilerplate. If a module is genuinely trivial (a 3-line constant), a one-liner\nis fine — but most modules here are not.\n",".claude/rules/testing.md":"---\npaths:\n  - \"tests/**/*.py\"\n---\n\n# Testing rule\n\nTests mirror the source layout: `tests/unit/test_<layer>/...`,\n`tests/integration/...`, `tests/e2e/...`.\n\n- **Structure**: `tests/unit/` mirrors `src/everos/` package-for-package. Put a test\n  next to where its subject lives in the mirror.\n- **Async**: `pytest-asyncio` is in `auto` mode — write `async def test_*` directly,\n  no marker needed.\n- **Markers** (default run excludes both — `-m \"not slow and not live_llm\"`):\n  - `@pytest.mark.slow` — tests ≥ ~10s.\n  - `@pytest.mark.live_llm` — needs real LLM/embedder credentials.\n  Keep unit tests fast and credential-free; push anything needing real services\n  behind a marker or into `integration`/`e2e`.\n- **Fixtures**: shared fixtures live in the nearest `conftest.py`. The root conftest\n  resets module caches (settings/logging/datetime) per test — rely on that for\n  isolation rather than mutating globals.\n- **Module docstring** on each test file stating what contract it pins (see existing\n  tests for the style).\n- **Coverage gate**: `make cov` enforces 80% (`--cov-fail-under=80`). New code should\n  not drop coverage below the gate.\n- Run `make test` (unit) and `make integration` before pushing; both run in CI.\n",".claude/skills/add-memory-kind/SKILL.md":"---\nname: add-memory-kind\ndescription: 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).\n---\n\n# /add-memory-kind — Add a new business memory kind\n\n## When to invoke\n\nAdding a new persisted business entity (Episode, Case, Skill, AtomicFact,\nForesight, Profile, or something custom). Multiple storage layers may be\ninvolved; this skill walks the decision then the wiring.\n\n## 1. Decide the storage combination\n\nA memory kind **does not have to use all three** layers. Pick by what\nthe kind actually needs:\n\n| Need | Markdown | SQLite | LanceDB |\n|---|:-:|:-:|:-:|\n| Human-readable / agent-editable source-of-truth text | ✅ | | |\n| Structured state, ACID transactions, joins, predicates | | ✅ | |\n| Vector / BM25 / hybrid retrieval | | | ✅ |\n\nCommon combinations seen in EverOS:\n\n| Combo | Example | Rationale |\n|---|---|---|\n| **md only** | scratch notes / dump bins | text-of-truth, no index needed |\n| **md + lancedb** | episode / memcell / case | text-of-truth + semantic retrieval |\n| **md + sqlite** | profile / playbook / soul.md state | text-of-truth + structured state to query |\n| **md + sqlite + lancedb** | full-blown business records | when you need *both* transactional state AND retrieval |\n| **sqlite only** | audit log / task queue / LSN watermark | system state, never user-facing |\n| **lancedb only** | rare; usually you still want md | derived embeddings without text-of-truth |\n\nRule of thumb: **markdown is the truth**; sqlite and lancedb are derived\nindexes that can be rebuilt from md. Drop md only when the kind has no\nhuman-readable form (pure system state).\n\n## 2. Pick the markdown storage strategy (if md is in your combo)\n\nThree strategies — declared in the EverOS Markdown First spec:\n\n| Strategy | Filename | Mutation | Examples |\n|---|---|---|---|\n| **Daily-log append** | `<prefix>-YYYY-MM-DD.md` | append entries | memcell / episode / case / atomic_fact / foresight |\n| **Skill-named in-place** | `skill_<name>.md` | overwrite the file | skills (procedural memory) |\n| **Single-file rewrite** | `user.md` / `agent.md` / `soul.md` / `behaviors.md` / `tools.md` | overwrite the file | profiles / playbooks |\n\nThis skill currently has a **complete recipe for daily-log append**.\nSkill-named and single-file recipes are sketched at the bottom — their\nbase writers (`BaseSkillWriter` / `BaseProfileWriter`) land later in the\nproject; until then build a thin wrapper over `MarkdownWriter`\ndirectly.\n\n---\n\n## 3. Markdown daily-log: 4 steps\n\n### 3.1 Frontmatter schema — `infra/persistence/markdown/mds/<name>.py`\n\n```python\n\"\"\"Episode daily-log frontmatter.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime as _dt\nfrom typing import ClassVar, Literal\n\nfrom everos.core.persistence.markdown import UserScopedFrontmatter\n\n\nclass UserEpisodeDailyFrontmatter(UserScopedFrontmatter):\n    \"\"\"``users/<u>/episodes/episode-<YYYY-MM-DD>.md``.\"\"\"\n\n    ENTRY_ID_PREFIX: ClassVar[str] = \"ep\"\n    DIR_NAME: ClassVar[str] = \"episodes\"\n    FILE_PREFIX: ClassVar[str] = \"episode\"\n\n    type: Literal[\"user_episode_daily\"] = \"user_episode_daily\"\n    date: _dt.date\n    entry_count: int = 0\n    last_appended_at: _dt.datetime | None = None\n```\n\nFor agent-track kinds subclass `AgentScopedFrontmatter` instead. If\nuser-track and agent-track share a kind name (e.g. `memcell`), give\neach a **distinct** `ENTRY_ID_PREFIX` (e.g. `umc` vs `amc`) so reverse\nlookup is unambiguous.\n\n### 3.2 Re-export — `mds/__init__.py`\n\n```python\nfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatter\n```\n\n### 3.3 Business writer — `infra/persistence/markdown/writers/<name>.py`\n\n```python\n\"\"\"Episode appender.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom everos.core.persistence import MarkdownReader\n\nfrom ..mds import UserEpisodeDailyFrontmatter\nfrom .base import BaseDailyWriter\n\n\nclass UserEpisodeAppender(BaseDailyWriter):\n    schema = UserEpisodeDailyFrontmatter\n\n    # OPTIONAL: override the count strategy. Default is len(entries);\n    # override to trust the frontmatter field instead.\n    def _current_count(self, path: Path) -> int:\n        if not path.exists():\n            return 0\n        return MarkdownReader.read(path).frontmatter.get(\"entry_count\", 0)\n```\n\n### 3.4 Re-export — `writers/__init__.py`\n\n```python\nfrom .episode import UserEpisodeAppender as UserEpisodeAppender\n```\n\n### Done — usage\n\n```python\nfrom everos.infra.persistence.markdown.writers import UserEpisodeAppender\n\nappender = UserEpisodeAppender(memory_root)\neid = appender.append(\"u_jason\", \"I went to the doctor today.\")\n# → users/u_jason/episodes/episode-<today>.md\n# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)\n```\n\n---\n\n## 4. (Optional) SQLite table — 4 steps\n\nSkip this section if the kind doesn't need structured state beyond markdown.\n\n### 4.1 Schema — `infra/persistence/sqlite/tables/<name>.py`\n\n```python\nfrom everos.core.persistence.sqlite import BaseTable, Field\n\n\nclass EpisodeState(BaseTable, table=True):\n    __tablename__ = \"episode_state\"  # type: ignore[assignment]\n\n    id: int | None = Field(default=None, primary_key=True)\n    entry_id: str = Field(index=True, unique=True)\n    cluster_id: str | None = Field(default=None, index=True)\n    status: str = Field(default=\"active\")\n```\n\n`BaseTable` already provides `created_at` / `updated_at` (auto-bumped).\n\n### 4.2 Re-export — `tables/__init__.py`\n\n```python\nfrom .episode import EpisodeState as EpisodeState\n```\n\n### 4.3 Repo — `infra/persistence/sqlite/repos/<name>.py`\n\n```python\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\n\nfrom everos.core.persistence.sqlite import RepoBase\n\nfrom ..sqlite_manager import get_session_factory\nfrom ..tables import EpisodeState\n\n\nclass _EpisodeStateRepo(RepoBase[EpisodeState]):\n    model = EpisodeState\n\n    def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:\n        return get_session_factory()\n\n\nepisode_state_repo = _EpisodeStateRepo()\n```\n\n### 4.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_state_repo as episode_state_repo\n```\n\n---\n\n## 5. (Optional) Derived index — 4 steps\n\nSkip this section if the kind doesn't need vector / BM25 / hybrid retrieval.\n\n### 5.1 Record declaration — `infra/persistence/records/<name>.py`\n\n```python\nfrom typing import ClassVar\n\nfrom everos.core.persistence.lancedb import BaseLanceTable, Vector\n\n\nclass EpisodeIndex(BaseLanceTable):\n    TABLE_NAME: ClassVar[str] = \"episode_index\"\n    PRIMARY_KEY: ClassVar[tuple[str, ...]] = (\"app_id\", \"project_id\", \"id\")\n\n    id: str\n    app_id: str = \"default\"\n    project_id: str = \"default\"\n    entry_id: str\n    text: str\n    tags: list[str]\n    vector: Vector(1024) | None = None  # type: ignore[valid-type]\n```\n\n`TABLE_NAME` / `BM25_FIELDS` / `PRIMARY_KEY` are the backend-neutral\ndeclarations every index backend reads (`IndexRecordMeta`). `PRIMARY_KEY`\nhas **no default**: `check_primary_keys` refuses to start, and LanceDB\n`upsert` refuses to write, a table that does not declare one. `id` is unique\nonly within `(app_id, project_id)`, so every business table declares the\nthree-column key — LanceDB merges on it, Milvus keys the collection on a\ncomposed `__pk` and rejects key components containing `/`. `Vector(N)` must\nmatch your embedding dimension.\n\n### 5.2 Re-export — `records/__init__.py`\n\n```python\nfrom .episode import EpisodeIndex as EpisodeIndex\n```\n\n### 5.3 Repo — `infra/persistence/lancedb/repos/<name>.py`\n\n```python\nfrom lancedb import AsyncTable\n\nfrom everos.core.persistence.lancedb import LanceRepoBase\nfrom everos.infra.persistence.records import EpisodeIndex\n\nfrom ..lancedb_manager import get_table\n\n\nclass _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):\n    schema = EpisodeIndex\n\n    async def _table_lookup(self) -> AsyncTable:\n        return await get_table(self.schema.TABLE_NAME, self.schema)\n\n\nepisode_index_repo = _EpisodeIndexRepo()\n```\n\nBusiness code writes through the `everos.infra.persistence.index` facade:\n`upsert(records)` merges on the declared key and is the only write; look a\nrow up with `find_one_where(primary_key(app_id, project_id, row_id))`.\n\n### 5.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_index_repo as episode_index_repo\n```\n\n---\n\n## 6. (Future) Skill-named & single-file markdown strategies\n\nWhen the new memory kind needs:\n\n- **skill-named** files (one file per named skill, in-place rewrite) — wait\n  for `BaseSkillWriter`, or use `MarkdownWriter.write_markdown` directly\n  with a thin wrapper.\n- **single-file rewrite** (one fixed file like `user.md`) — wait for\n  `BaseProfileWriter`, same fallback.\n\nThese strategies do **not** use entry markers; their frontmatter schema\ndoes not need `ENTRY_ID_PREFIX` (only `id` / `type` / `schema_version` plus\nthe scope mixin fields).\n\n---\n\n## 7. Verification checklist\n\n- [ ] `make lint` — ruff + import-linter clean\n- [ ] `make test` — existing manager / lifespan / writer tests still pass\n- [ ] When the kind crosses **multiple** layers:\n  - [ ] markdown entry id is the join key for the sqlite / lancedb rows\n  - [ ] business code reads only via the repo singleton (no raw engine\n        access in service / memory)\n  - [ ] cascade daemon (when it lands) can rebuild sqlite / lancedb\n        from md alone — keep md as the truth\n\nTests by layer:\n\n| Tests for | Location |\n|---|---|\n| Markdown frontmatter schema | `tests/unit/test_infra/test_markdown/test_mds/` |\n| Markdown business appender | `tests/unit/test_infra/test_markdown/test_writers/` |\n| SQLite RepoBase logic | `tests/unit/test_core/test_persistence/test_sqlite/` |\n| SQLite manager / lifespan | `tests/unit/test_infra/test_sqlite/` |\n| LanceDB LanceRepoBase logic | `tests/unit/test_core/test_persistence/test_lancedb/` |\n| LanceDB manager / lifespan | `tests/unit/test_infra/test_lancedb/` |\n\n## 8. Common pitfalls\n\n| Mistake | Symptom | Fix |\n|---|---|---|\n| Forgot `ENTRY_ID_PREFIX` / `DIR_NAME` / `FILE_PREFIX` on a daily-log schema | `BaseDailyWriter.__init__` raises `TypeError` | Add all three ClassVars |\n| Same `ENTRY_ID_PREFIX` on user + agent variants | `MemoryLayout.locate_for_entry` collision error | Use distinct prefixes (e.g. `umc` vs `amc`) |\n| Imported `RepoBase` from `infra.persistence.sqlite` | `ImportError` | Lives in `core.persistence.sqlite` (moved earlier) |\n| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each `__init__.py` |\n| `Vector(N)` mismatched with embedding dim | LanceDB raises on insert | Make `N` exactly match the model output |\n| 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 |\n| Hand-rolling `datetime.now()` instead of `today_with_timezone()` | Day-boundary drift across timezones | Always go through `everos.component.utils.datetime` |\n\n## Background\n\n- Architecture: [../../rules/architecture.md](../../rules/architecture.md)\n- `__init__.py` re-export rules: [../../rules/init-py-and-reexport.md](../../rules/init-py-and-reexport.md)\n- Async programming: [../../rules/async-programming.md](../../rules/async-programming.md)\n- Datetime handling: [../../rules/datetime-handling.md](../../rules/datetime-handling.md)\n",".claude/skills/commit/SKILL.md":"---\nname: commit\ndescription: Stage and create a Conventional Commits message for the current change\n---\n\n# /commit\n\nCreate a well-formed commit following the [Conventional Commits](https://www.conventionalcommits.org)\nstandard. The format is enforced by `gitlint` in the `commit-msg` hook.\n\n## Steps\n\n1. Run `git status` and `git diff` (and `git diff --staged`) to see what changed.\n2. Review recent history for style: `git log --oneline -10`.\n3. Group the change into a single focused commit. If the working tree mixes\n   unrelated changes, stage selectively (`git add -p` / specific paths) rather\n   than committing everything at once.\n4. Write the message in **Conventional Commits** form:\n\n   ```\n   <type>[(scope)][!]: <imperative summary, ≤72 chars>\n\n   <optional body: what & why, wrapped at 72>\n\n   <optional footer: BREAKING CHANGE: …, Refs: #123>\n   ```\n\n5. Never use `--no-verify`. If pre-commit hooks fail, fix the cause and re-commit.\n6. Do not commit secrets, generated artifacts, or work-in-progress directly to\n   the protected `main` branch.\n\n## Types\n\n| Type | Use for |\n|---|---|\n| `feat` | new feature |\n| `fix` | bug fix |\n| `refactor` | behavior-preserving restructure |\n| `test` | add / update tests |\n| `docs` | documentation |\n| `style` | formatting only |\n| `perf` | performance |\n| `chore` | config / build / tooling |\n| `build` | build system or dependencies |\n| `ci` | CI configuration |\n| `revert` | revert a previous commit |\n\n## Notes\n\n- No emoji — the title must start with the `type`.\n- One logical change per commit; keep the history bisectable.\n- The summary is imperative mood: \"add\", not \"added\" / \"adds\".\n- `scope` is optional: `fix(search): …`. A `!` before the colon (or a\n  `BREAKING CHANGE:` footer) marks a breaking change.\n",".claude/skills/new-branch/SKILL.md":"---\nname: new-branch\ndescription: Create a GitHub branch from main with the project naming convention\n---\n\n# /new-branch\n\nCut a new branch for the GitHub repository.\n\n## Branch model\n\n```\nmain       = default and protected branch\nfeat/*     = feature work\nfix/*      = bug fixes\ndocs/*     = documentation-only changes\nci/*       = CI, build, and developer-experience changes\nchore/*    = repository maintenance\nrefactor/* = behavior-preserving code structure changes\n```\n\n## Steps\n\n1. Ask (or infer) the change type: `feat`, `fix`, `docs`, `ci`, `chore`, or\n   `refactor`.\n2. Update `main` first:\n   ```bash\n   git checkout main\n   git pull --ff-only\n   ```\n3. Create the branch with a kebab-case slug:\n   ```bash\n   git checkout -b <type>/<short-slug>\n   ```\n4. Keep the branch scoped to one purpose and open a pull request back to\n   `main`.\n\n## Naming\n\n- `feat/add-agentic-rerank`, `fix/empty-profile-crash`,\n  `docs/quickstart-config`, `ci/check-github-docs`.\n- Lowercase, hyphen-separated, no spaces, concise.\n\nNever commit directly to `main` — always use a branch and pull request.\n",".claude/skills/pr/SKILL.md":"---\nname: pr\ndescription: Open a GitHub PR targeting the correct branch with the project template\n---\n\n# /pr\n\nOpen a pull request on GitHub using the `gh` CLI and the repo's PR template.\n\n## Steps\n\n1. Confirm the branch and target:\n   - Base branch is **`main`**.\n   - Head branch should be a scoped branch such as `feat/*`, `fix/*`,\n     `docs/*`, `ci/*`, `chore/*`, or `refactor/*`.\n2. Ensure local checks pass first:\n   ```bash\n   make ci\n   ```\n   Do not open a PR with failing lint/tests.\n3. Push the branch:\n   ```bash\n   git push -u origin HEAD\n   ```\n4. Create the PR, filling the template\n   ([.github/PULL_REQUEST_TEMPLATE.md](../../../.github/PULL_REQUEST_TEMPLATE.md)):\n   ```bash\n   gh pr create --base main --fill-first\n   ```\n   Then edit the body to complete each section:\n   - **Summary** — what changed and why.\n   - **Area** — tick the relevant box (architecture / benchmark / use case /\n     docs / DX / CI-build-release).\n   - **Verification** — paste the commands you ran (`make ci`, manual checks).\n   - **Checklist** — tick honestly; don't tick boxes you didn't satisfy.\n   - **Notes for Reviewers** — anything subtle.\n\n## Notes\n\n- Keep the PR scoped to one area. Split unrelated changes.\n- If `make ci` was not fully run, say so in Verification rather than implying it passed.\n- Never retarget a community PR away from `main` unless a maintainer explicitly\n  asks for it.\n",".claude/skills/release/SKILL.md":"---\nname: release\ndescription: Cut a versioned release and publish everos to PyPI via the tag-triggered workflow\n---\n\n# /release\n\nPublish a new version of `everos` to PyPI. Publishing is automated: pushing a\n`vX.Y.Z` tag triggers [.github/workflows/release.yml](../../../.github/workflows/release.yml),\nwhich builds, smoke-tests, and uploads via PyPI **Trusted Publishing** (OIDC —\nno stored token) behind the `release` environment's manual-approval gate, then\ndrafts the GitHub Release page from the CHANGELOG.\n\nA release is not finished when PyPI accepts the upload: the GitHub Release page\nis drafted, never auto-published, and someone has to write its lead summary and\nclick **Publish**.\n\n## Preconditions\n\n- On `main`, up to date, with green CI (the tag builds from `main`'s tree).\n- Decide the version per SemVer: patch = fixes, minor = back-compatible\n  features, major = breaking changes.\n\n## Steps\n\n```\n1. Bump the version    → pyproject.toml [project] version = \"X.Y.Z\"\n   (single source; everos.__version__ reads installed package metadata)\n2. Update CHANGELOG.md → move the Unreleased entries under a new\n   ## [X.Y.Z] - <date> heading, and write the release page's prose here\n   (lead paragraph + `### Upgrade` group — see \"The release page\")\n3. Commit              → git commit -m \"chore(release): vX.Y.Z\"\n4. Open a PR, merge to main after green CI\n5. Tag main + push     → git tag -a vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z\n6. Approve             → the release.yml run pauses on the `release`\n   environment; a reviewer approves in the Actions run\n7. Verify              → https://pypi.org/project/everos/X.Y.Z/\n8. Publish the page    → the run leaves a DRAFT GitHub Release, already\n   complete if step 2 was done properly. Read it once, click Publish.\n```\n\nThe tag must equal the `pyproject.toml` version — the workflow refuses to\npublish on a mismatch. A stable tag with no matching `## [X.Y.Z]` CHANGELOG\nsection fails the release job for the same reason.\n\n## The release page\n\n**The whole page is written in CHANGELOG.md, during the release PR.** Nothing\nis meant to be composed at publish time — by then the changes are weeks old and\nthe text gets no review. Write the version section like this:\n\n```markdown\n## [X.Y.Z] - 2026-09-01\n\n**What this release is for.** One paragraph, prose, no bullets — it becomes the\nlead of the release page. Say what changed for a user, not what was refactored.\n\n### Added\n### Changed\n### Fixed\n\n### Upgrade\n\nWhat a reader must know before upgrading: what happens on first startup, which\ncommand recovers a bad state, which pins moved. Omit the group entirely when a\nplain `pip install --upgrade` is all there is — 1.1.4 and 1.2.0 have nothing\nhere. Do not write filler.\n```\n\nCI turns that into the page: everything above `### Upgrade` is lifted verbatim\nwith the group headings demoted to `##`, and the Upgrade prose is wrapped in the\nboilerplate — pip line above it, compare link below — which is the shape every\nrelease since 1.1.3 has. See\n[1.2.1](https://github.com/EverMind-AI/EverOS/releases/tag/v1.2.1).\n\nSo publishing is a read-through and a click. If the draft looks wrong, the fix\nbelongs in CHANGELOG.md on `main`, not only in the draft — otherwise the two\ndrift apart and the next release inherits the habit.\n\n> While it is a draft, GitHub serves the release at\n> `releases/tag/untagged-<hash>`, and that URL keeps serving a stale page after\n> publication with no redirect. Never share it — a reader who opens it later\n> concludes the release never went out. Link `releases/tag/vX.Y.Z` instead; the\n> job prints both URLs in its step summary.\n\nPublish with the **Publish release** button in the web UI. Publishing through\nthe API by flipping `draft` alone drops the tag: GitHub rebinds the release to\nthe `untagged-<hash>` placeholder and creates a git tag by that name against the\ndefault branch (observed on 1.2.2). Pass `tag_name` if you must do it from the\nCLI:\n\n```bash\ngh api -X PATCH \"repos/EverMind-AI/EverOS/releases/<id>\" \\\n  -F draft=false -f tag_name=vX.Y.Z -f make_latest=true\n```\n\nRe-running the release job replaces its own draft and leaves an already-published\nrelease untouched, so a re-run is always safe.\n\n## Pre-releases\n\nPEP 440 pre-release tags publish too (PyPI accepts them; `pip install everos`\nignores them unless `--pre`): `vX.Y.ZrcN`, `vX.Y.ZaN`, `vX.Y.ZbN`. Set the same\nsuffix in `pyproject.toml` version before tagging.\n\nTheir release page is drafted as a **pre-release** and never becomes\n`/releases/latest`. A pre-release does not need its own CHANGELOG section — the\ndraft falls back to a one-line placeholder body when there is none.\n\n## One-time setup (project owner, not doable from CI)\n\n1. **PyPI trusted publisher** — PyPI → project `everos` → Settings →\n   Publishing → add: owner `EverMind-AI`, repo `EverOS`, workflow\n   `release.yml`, environment `release`.\n2. **GitHub environment** — repo Settings → Environments → create `release`\n   with required reviewers, so every publish needs a manual approval.\n\nNo PyPI API token is ever stored; the workflow mints a short-lived OIDC token\nat publish time.\n","CLAUDE.md":"# EverOS — md-first Memory Extraction Framework\n\nThis is a Python framework for md-first memory extraction (lightweight; single-user or small-team).\n\n## Quick commands\n\n```bash\nuv sync                    # install deps\nmake lint                  # ruff (check + format-check) + import-linter\nmake format                # auto-fix formatting\nmake test                  # pytest tests/unit\nmake integration           # pytest tests/integration\nmake ci                    # full CI: lint + test + integration\n```\n\n## Architecture\n\nDDD 5 layers + cross-cutting:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — cli + api (Presentation)\n- `service/` — use case orchestration (memorize / retrieve / evolve / manage)\n- `memory/` — domain (extract + search + cascade + prompt_slots + models)\n- `infra/` — storage adapters (markdown + sqlite + lancedb)\n- `component/` — injectable providers (llm / embedding / config / utils)\n- `core/` — runtime base (observability / lifespan / context)\n- `config/` — configuration data (Settings + default.toml)\n\n**Dependency rule**: `entrypoints → service → memory → infra`. Single-direction, enforced by `import-linter`.\n\nDetailed: [docs/architecture.md](docs/architecture.md).\n\n## Engineering practices\n\n- **Coding rules** auto-loaded from [.claude/rules/](.claude/rules/) (10 rules; the three always-loaded ones cover architecture / code-style / language-policy, the rest are path-scoped and load when Claude Code opens a matching file)\n- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`, `/release`\n- **Project-level decisions** in [docs/](docs/) (low-frequency, human-judgment-required)\n- **Language policy**: the project targets a global audience — docs and code are English; CJK appears only in test fixtures and locale-suffixed mirrors. Scanned by `make check-cjk`.\n- **Datetime discipline**: never call `datetime.now()` / `time.time()` directly — use `everos.component.utils.datetime`. Enforced by `make check-datetime`.\n\nContributor engineering reference — build, test, CI gates, branch & commit conventions: [docs/engineering.md](docs/engineering.md).\n\n## Branch strategy\n\n`main` is the default and protected branch. Create scoped branches from\n`main` (`feat/*`, `fix/*`, `docs/*`, `ci/*`, `chore/*`, `refactor/*`) and open\npull requests back to `main` after the required checks pass.\n\nSee [.claude/skills/new-branch/SKILL.md](.claude/skills/new-branch/SKILL.md)\nfor the full branch workflow.\n\n## GitHub sync guard\n\nWhen refreshing this repository from an internal source archive, preserve\nGitHub-only contributor and automation files. Do not overwrite `CLAUDE.md`,\n`.claude/skills/*`, `CONTRIBUTING.md`, or `.github/*` workflow/template files\nwithout checking [docs/github-sync.md](docs/github-sync.md).\n\n## Storage three-piece set\n\n```\nMarkdown (truth)  +  SQLite (state)  +  LanceDB (vector + BM25 + scalar)\n```\n\n- Memory root: `~/.everos/{agents,users,knowledge}/` (md files = single source of truth)\n- System DB: `~/.everos/.index/sqlite/system.db` (state + audit + queue + metadata)\n- Index: `~/.everos/.index/lancedb/` (rebuildable from md)\n\nSelection rationale: [docs/architecture.md](docs/architecture.md).\n\n## Source layout\n\n**src layout** (`src/everos/<...>`): standard PyPA project structure — code lives under `src/` so the working tree is not on the import path until installed, preventing accidental imports of in-development modules.\n\nAlgorithm assets (prompts, extractors) live in the separate [`everalgo`](https://github.com/EverMind-AI/EverAlgo) library, consumed here as the `everalgo-*` PyPI packages.\n\n## Where things go\n\n| Want to... | Look at |\n|---|---|\n| Understand architecture | [docs/architecture.md](docs/architecture.md) |\n| Understand storage choice | [docs/architecture.md](docs/architecture.md) (storage section) |\n| Build, test, CI & conventions | [docs/engineering.md](docs/engineering.md) |\n| Add a new module | [.claude/rules/init-py-and-reexport.md](.claude/rules/init-py-and-reexport.md) |\n| Make a commit | use `/commit` |\n| Open a branch / PR | use `/new-branch` / `/pr` |\n| Run checks before pushing | `make ci` |\n","docs/api.md":"# EverOS HTTP API (v2)\n\nHuman-readable reference for the EverOS HTTP API. Schema names, types\nand validation constraints mirror the OpenAPI spec served at\n`GET /openapi.json` (when the server runs with `ENV=DEV`; see\n[OpenAPI spec source](#openapi-spec-source)). This document adds the\nbusiness semantics the raw spec does not carry.\n\n## Table of contents\n\n- [Overview](#overview)\n  - [Base URL and versioning](#base-url-and-versioning)\n  - [Content type](#content-type)\n  - [Authentication](#authentication)\n  - [Response envelope](#response-envelope)\n  - [Eventual consistency](#eventual-consistency)\n  - [Conventions](#conventions)\n  - [ScopeId: `app_id` and `project_id`](#scopeid-app_id-and-project_id)\n- [Errors](#errors)\n- [Common types](#common-types)\n  - [MessageItem](#messageitem)\n  - [ContentItem](#contentitem)\n  - [ToolCall](#toolcall)\n  - [ToolFunction](#toolfunction)\n  - [FilterNode (filter DSL)](#filternode-filter-dsl)\n  - [SearchMethod](#searchmethod)\n  - [GetMemoryType](#getmemorytype)\n- [Endpoints](#endpoints)\n  - [POST /api/v2/memory/add](#post-apiv2memoryadd)\n  - [POST /api/v2/memory/flush](#post-apiv2memoryflush)\n  - [Asynchronous mode](#asynchronous-mode)\n  - [GET /api/v2/tasks/{task_id}](#get-apiv2taskstask_id)\n  - [GET /api/v2/tasks](#get-apiv2tasks)\n  - [POST /api/v2/memory/search](#post-apiv2memorysearch)\n  - [POST /api/v2/memory/get](#post-apiv2memoryget)\n  - [POST /api/v2/ome/trigger](#post-apiv2ometrigger)\n  - [Knowledge endpoints](#knowledge-endpoints)\n- [OpenAPI spec source](#openapi-spec-source)\n\n## Overview\n\n### Base URL and versioning\n\n| Setting | Default | Override |\n|---|---|---|\n| Host | `127.0.0.1` (loopback only) | `EVEROS_API__HOST` env var or `--host` flag |\n| Port | `8000` | `EVEROS_API__PORT` env var or `--port` flag |\n| Version prefix | `/api/v2` | — |\n\nBusiness endpoints live under `/api/v2/memory/`, `/api/v2/ome/`, and\n`/api/v2/knowledge/`. Knowledge endpoints have their own dedicated\nreference at [docs/knowledge.md](knowledge.md) and are cross-referenced\nbelow. The operational endpoints `GET /health`, `GET /metrics` and\n`POST /api/v2/cascade/quiesce` exist but are intentionally outside this\nreference — they are runtime probes and shutdown hooks for deployment, not\npart of the application contract. `quiesce` drains the md → index queue and\nstops the watcher; calling it twice is safe, and it returns a bare status body\n(`quiesced`, `drained`, `pending_before`, `pending_after`,\n`failed_permanent`), see `docs/openapi.json`. One note for anyone keying off\n`/health`: `capabilities.embed` reports whether the embedding service\nanswered a dimension probe **when the process started**, not whether it is\nreachable now -- a service that recovers later keeps reading `false` until\nthe next restart.\n\n`/api/v2` is the canonical prefix, aligned with the EverOS Cloud API. Every\nbusiness endpoint is **also** served under `/api/v1`, kept as a legacy\ncompatibility alias: the two prefixes resolve to the same handlers with\nidentical request/response contracts, so existing `/api/v1` integrations keep\nworking today. The alias carries no long-term guarantee — it may be removed in\na future major release, and it will be announced in the changelog with a\ndeprecation window before that happens. Write new integrations against\n`/api/v2`, and migrate existing ones when convenient.\n\n### Content type\n\nAll `POST` endpoints require `Content-Type: application/json`. Request\nand response bodies are UTF-8 JSON.\n\n### Authentication\n\nEverOS ships **no built-in authentication**. The server binds to\n`127.0.0.1` by default; place your own gateway or auth layer in front\nbefore exposing the API on any other interface. See\n[../SECURITY.md](../SECURITY.md) for the threat model.\n\n### Response envelope\n\nSuccessful responses from the business endpoints (`/memory/*`,\n`/knowledge/*`) wrap the payload in the envelope below (`200 OK`, or\n`201 Created` for a knowledge document upload). `POST /api/v2/ome/trigger`\nand the operational endpoints return their body bare — see each section.\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"data\": { /* endpoint-specific payload */ }\n}\n```\n\n`request_id` is generated server-side (32 lowercase hex chars) and is\nechoed by structured logs / tracing / metrics, so a single request can\nbe correlated end-to-end. Error responses carry their own envelope —\n`request_id` at the top level alongside a nested `error` object (not a\nbare FastAPI `detail`); see [Errors](#errors).\n\n### Eventual consistency\n\n`/add` and `/flush` write the markdown file (the source of truth)\n**synchronously** — when the call returns with `status: \"extracted\"`,\nthe new entry exists on disk. The configured vector / BM25 / scalar\nindex backend is rebuilt by the in-process **cascade coroutine\nasynchronously**.\n\nThat means `/search` and `/get` may not see a record immediately after\nthe `/flush` that produced it. Typical sync latency is sub-second, but\nunder load it can reach ~10–15 seconds. If you need read-your-write\nsemantics, retry with backoff. The markdown file is durable\nregardless; index lag never loses data.\n\nThe same applies one level up for everything an OME strategy produces\n**after** the extraction call returns: agent cases and skills (agent track),\nprofiles, atomic facts. `/flush` returns once the episode is on disk; the\n`extract_agent_case` strategy then runs its own LLM call, and skill\nclustering runs after that, so `/get` with `memory_type: \"agent_case\"` can\nlegitimately report `total_count: 0` for tens of seconds after a flush that\nreturned `\"extracted\"` (14 s for the case and 32 s for the skill on a\nlaptop with a hosted LLM). `GET /health`'s `cascade.pending` does **not**\ncover this window — it only tracks the md → index queue, which is empty\nwhile the strategy is still thinking. Poll `/get` with a budget instead.\nThe agent track also has extraction gates of its own, applied before any\nLLM call: a trajectory with fewer than three tool-call rounds is rejected as\ntoo simple (`AgentCaseExtractor(min_tool_call_rounds=3)`, the EverOS\ndefault — so a session with no tool use never yields a case), and so is one\nthat does not end on an assistant text message. Trajectories that pass\nthose gates can still be dropped by the extractor's own \"worth extracting\"\nLLM filter. EverOS logs the skip as `agent_case_skipped_by_algo` with the\nmemcell and session ids; the reason itself is on the adjacent everalgo log\nline, `skipping memcell (n_items=N): <reason> <detail>`. The same\n\"seconds later\" rule for every OME-produced kind is summarised in\n[how-memory-works.md](how-memory-works.md).\n\n### Conventions\n\n#### Timestamps\n\nEverOS runs a **two-zone discipline**: every stored byte is UTC; every\nrendered value carries the configured **display timezone**. The\ndisplay timezone is set by `EVEROS_MEMORY__TIMEZONE` (env var) or\n`[memory] timezone` (TOML); default `\"UTC\"`.\n\n| Direction | Format | Notes |\n|---|---|---|\n| **Request** — `messages[].timestamp` (`/add`) | Integer, **Unix epoch milliseconds (ms)** | Server auto-detects `>= 10^12` as ms for backward compat, but the contract is ms |\n| **Request** — `filters.timestamp.{gte,lt,…}` (`/search` / `/get`) | Integer, **Unix epoch ms** *or* ISO-8601 string | Same auto-detect; pick whichever you have |\n| **Response** — every `timestamp` field | ISO-8601 string with **explicit timezone offset** (`+HH:MM` or `Z`) | Always rendered in the configured display tz; never naive |\n\nWith the default `EVEROS_MEMORY__TIMEZONE=UTC` an episode produced\nat 11:30 UTC renders as `\"2026-05-28T11:30:36Z\"` (Pydantic\ncanonicalises `timezone.utc` to `Z`). Switch to\n`EVEROS_MEMORY__TIMEZONE=Asia/Shanghai` and the same UTC instant\nrenders as `\"2026-05-28T19:30:36+08:00\"` — bytes on disk are\nunchanged.\n\n**Fallback for naive input.** If you submit a `filters.timestamp` ISO\nstring without an offset (e.g. `\"2026-05-28T11:30:00\"`), the server\ntreats it as already display-tz-local before comparing against\nstorage. This is the same rule users see when reading rendered output:\n\"if you didn't say a zone, we assume your zone.\"\n\n**For internal architecture.** The storage / display split lives in\n`everos.component.utils.datetime`\n(`get_utc_now` / `ensure_utc` / `UtcDatetime` for storage;\n`get_now_with_timezone` / `to_display_tz` for display). See\n[datetime.md](datetime.md) for the design rationale.\n\n#### Other conventions\n\n- **Server-generated IDs** follow `<owner>_<kind>_<YYYYMMDD>_<NNN>`,\n  e.g. `alice_ep_20260528_00000001` for an episode, `alice_af_...`\n  for an atomic fact. See\n  [storage_layout.md §4](storage_layout.md) for the encoding.\n- **All endpoints are POST** for `/api/v2/memory/*` even when the\n  semantics look like a read (`/search`, `/get`) — the request bodies\n  are too rich (filters, methods, paging) to encode in a query string.\n\n### ScopeId: `app_id` and `project_id`\n\n`app_id` and `project_id` partition memory at the disk layer. Every\nwrite lands under\n`~/.everos/<app>/<project>/users/<user_id>/...` (or `.../agents/...`\nfor the agent track). The default scope materialises on disk as\n`default_app` / `default_project` (the `_app` / `_project` suffix is\nadded only for the literal id `\"default\"` so the default space stays\nvisually distinct from user-named scopes).\n\nA `/search` or `/get` query never crosses scopes — different\n`(app_id, project_id)` pairs are isolated.\n\nBoth fields share the same validation:\n\n| Setting | Value |\n|---|---|\n| Type | `string` |\n| Default | `\"default\"` |\n| Length | 1–128 chars |\n| Charset | `^[a-zA-Z0-9_.-]+$` |\n| Rejected literals | `\".\"` and `\"..\"` (path-traversal guard) |\n\n## Errors\n\nEvery non-2xx response uses a uniform error envelope — `request_id` at\nthe top level (mirroring the success envelope) alongside a nested\n`error` object. It is **not** a bare FastAPI `detail`:\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Document 'abc123' not found\",\n    \"timestamp\": \"2026-06-01T12:24:46+00:00\",\n    \"path\": \"/api/v2/knowledge/documents/abc123\"\n  }\n}\n```\n\n### error.code values\n\n`error.code` is a machine-readable `ErrorCode` enum. Clients can switch\non this value to decide retry / display / routing behaviour without\nparsing the human-readable `message` field.\n\n| `error.code` | HTTP | Retryable? | When |\n|---|---|---|---|\n| `NOT_FOUND` | `404` | No | Requested resource does not exist |\n| `CONFLICT` | `409` | No | Operation conflicts with existing state (e.g. duplicate document) |\n| `INVALID_INPUT` | `422` | No | Request-body validation failure. Also covers `/search` / `/get` filter-DSL compile errors — the compile reason rides in `message` — and, on `/add` and `/flush`, a session whose buffered messages the model could not segment on three consecutive calls: the buffer is moved to the `quarantine` track of `unprocessed_buffer` so the session can continue, and `message` names the session and the number of messages set aside |\n| `EXTRACTION_EMPTY` | `422` | No | Knowledge document parsed but the extractor produced no topics. An empty or whitespace-only upload is rejected earlier as `INVALID_INPUT` (`\"Uploaded file has no valid content.\"`) |\n| `BAD_REQUEST` | `400` | No | Path traversal attempt or other malformed input |\n| `UNSUPPORTED_FORMAT` | `415` | No | File format or modality not supported (e.g. unsupported `ContentItem` type, missing `ext` for `base64`) |\n| `PAYLOAD_TOO_LARGE` | `413` | No | An `async_mode` request body exceeds `memorize.async_queue.max_payload_bytes`; the batch was not accepted |\n| `QUEUE_SATURATED` | `429` | Yes | The memorize queue is at its per-session or global cap; the batch was not accepted — retry later or keep it client-side |\n| `EXTERNAL_SERVICE_UNAVAILABLE` | `503` | **Yes** | An external service (LLM, embedding, rerank) returned an error or timed out |\n| `CAPABILITY_UNAVAILABLE` | `503` | No | A required server-side capability is missing (e.g. `everos[multimodal]` extra not installed, LibreOffice absent) — requires admin action, not retry |\n| `CONFIGURATION_ERROR` | `500` | No | A required configuration is missing or invalid (e.g. embedding model not set) |\n| `INTERNAL_ERROR` | `500` | No | Unhandled exception (internal details are logged, never leaked) |\n\n### error object\n\n| Field | Type | Description |\n|---|---|---|\n| `code` | `string` | One of the `ErrorCode` values listed above |\n| `message` | `string` | Human-readable reason. For `INVALID_INPUT` from request validation, **only the first** validation error is surfaced, formatted `\"<msg>: <dotted-loc>\"` with the leading `body` segment stripped (e.g. `\"Field required: messages\"`); a model-level validator with no field location surfaces just `\"<msg>\"` (e.g. `\"Value error, exactly one of user_id / agent_id must be provided\"`) |\n| `timestamp` | `string` | ISO-8601 with timezone offset (display tz) |\n| `path` | `string` | Request path, e.g. `/api/v2/memory/add` |\n\n> Unlike FastAPI's default, the full per-field validation array is **not**\n> returned — only the first error's message. A client that needs the\n> offending field can read the `<loc>` suffix in `message`.\n\n## Common types\n\n### MessageItem\n\nOne turn in a `/add` batch. Shape mirrors the OpenAI Chat Completions\nmessage structure plus a stable `sender_id` for indexing.\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `sender_id` | `string` | yes | — | `minLength=1` |\n| `sender_name` | `string \\| null` | no | `null` | — |\n| `role` | `\"user\" \\| \"assistant\" \\| \"tool\"` | yes | — | — |\n| `timestamp` | `integer` | yes | — | `> 0` — **Unix epoch milliseconds (ms)** per v1 contract |\n| `content` | `string \\| array<ContentItem>` | yes | — | — |\n| `tool_calls` | `array<ToolCall> \\| null` | no | `null` | — |\n| `tool_call_id` | `string \\| null` | no | `null` | — |\n\n**`sender_id`** — Stable identifier of the entity producing this turn.\nFor `role: \"user\"`, this is the `user_id` the server will index the\nextracted memory under: markdown lands at\n`users/<sender_id>/episodes/...`, and `/search` / `/get` queries with\n`user_id: \"<sender_id>\"` reach it. For `role: \"assistant\"` or\n`\"tool\"`, the field is informational (the LLM sees it during\nextraction but it is not used as an indexing key).\n\n**`sender_name`** — Optional human-readable display name. Lets the LLM\nuse a real name during extraction without changing the indexing key.\nFor example, set `sender_id: \"u_42\"` (stable internal id) and\n`sender_name: \"Alice\"` (what the LLM sees).\n\n**`role`** — Speaker role; one of:\n- `\"user\"` — content originates from a human (or human-acting client).\n- `\"assistant\"` — content from the AI assistant.\n- `\"tool\"` — output of a tool call; pair with `tool_call_id`.\n\n**`timestamp`** — Wall-clock anchor in **Unix epoch milliseconds (ms)**\nper the v1 API contract. Used by extraction to anchor the resulting\nepisode and by the daily-log writer to bucket the entry into the right\nfile (`episode-<YYYY-MM-DD>.md` etc.).\n\n> Implementation note: the algo layer auto-detects seconds vs ms (values\n> `>= 10^12` are treated as ms, smaller as seconds) for backward compat,\n> but **clients should send ms** to honour the contract.\n\n**`content`** — The message body.\n- A bare **string** is shorthand for a single text content item.\n- An **array of `ContentItem`** is for mixed-modality input (text +\n  image / pdf / audio / ...); non-text items are parsed by the\n  multimodal LLM configured via `EVEROS_MULTIMODAL__*` env vars. See\n  [ContentItem](#contentitem).\n\n**`tool_calls`** — When `role: \"assistant\"`, the tool calls the\nassistant emitted in this turn (OpenAI Chat Completions shape).\n\n**`tool_call_id`** — When `role: \"tool\"`, the `id` of the call this\nmessage is the response to. Required for `role: \"tool\"`; a tool row\nwithout it is rejected with `422`.\n\n### ContentItem\n\nMixed-modality message-body element. Carry the payload in exactly one\nof `text` / `uri` / `base64`; the others must be `null`. For\n`type: \"text\"` use `text`; for every **non-text** type use `uri`\n(`http(s)://`) or `base64` (with `ext`). Non-text items are routed\nthrough the multimodal parser, which needs a fetchable or decodable\npayload — a non-text item carrying only `text` returns `415`.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `type` | `\"text\" \\| \"image\" \\| \"audio\" \\| \"doc\" \\| \"pdf\" \\| \"html\" \\| \"email\"` | yes | — | — |\n| `text` | `string \\| null` | no | `null` | Required when `type: \"text\"` |\n| `uri` | `string \\| null` | no | `null` | `http(s)://` (fetched server-side) or `file://` (read from the server's local fs, guardrailed) pointer |\n| `base64` | `string \\| null` | no | `null` | Inline payload, plain base64 (no `data:` prefix) |\n| `ext` | `string \\| null` | no | `null` | File-extension hint when `uri` lacks one |\n| `name` | `string \\| null` | no | `null` | Display filename, used in logs |\n| `extras` | `object \\| null` | no | `null` | Provider-specific metadata, opaque to EverOS |\n\n**`type`** — The content kind. Each non-text type is dispatched to the\nmultimodal LLM. If the multimodal endpoint cannot handle the supplied\npayload, `/add` returns `415 Unsupported Media Type`.\n\n**`text`** — The literal text payload; valid **only** for\n`type: \"text\"`. A non-text type (including `\"html\"`) is always routed\nto the parser and must carry `uri` or `base64`; passing only `text` on\na non-text item returns `415`. To inline HTML as plain text, send it\nas `type: \"text\"`.\n\n**`uri`** — `http(s)://` or `file://` pointer to the asset. An\n`http(s)` uri is fetched by the server and dispatched by the response\nContent-Type (use it for assets hosted elsewhere — S3 / OSS presigned\nURL, http server). A `file://` uri is read from the **server's** local\nfilesystem (the path must be reachable by the server process), subject\nto these guardrails:\n\n- the resolved path (symlinks followed) must be an existing regular file;\n- its size must be ≤ `EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES` (default 50 MiB);\n- when `EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS` is set, the path must lie\n  within one of the listed roots (unset = any readable file, the\n  local-first default).\n\nA guardrail violation surfaces as `415`. Mutually exclusive with `base64`.\n\n**`base64`** — Inline binary payload, base64-encoded (no `data:`\nprefix). Mutually exclusive with `uri`.\n\n> **Size caution.** base64 inflates the payload ~4/3× on the wire, and\n> the encoded blob is held **verbatim in the server's staging buffer\n> (SQLite) from `/add` until the session is flushed** — a multi-MB PDF\n> becomes multi-MB of SQLite text for the buffer's lifetime. It is *not*\n> persisted past extraction (the memory cell and episode store only the\n> parsed text, never the raw bytes), but the transient footprint is\n> real, and a large inline blob also slows request parsing. **Prefer\n> `uri` (`http(s)://`) for large assets**: the buffer then holds only\n> the URL plus the parsed text, and the bytes are fetched transiently at\n> parse time rather than stored. Reserve `base64` for small assets or\n> when no reachable URL exists.\n\n**`ext`** — File-extension hint (`\"pdf\"`, `\"png\"`, `\"html\"`, ...). For\n`base64` payloads this drives modality dispatch and is effectively\n**required** (without it the server falls back to `mime`, then `415`s\nif neither resolves). For `uri` payloads it is optional — the fetched\nContent-Type usually suffices.\n\n**`name`** — Filename / human label for logging and traceability.\nDoes not affect parsing.\n\n**`extras`** — Free-form bag of provider-specific metadata (e.g.\ncaption, page hints). Opaque to EverOS; passed through to the\nmultimodal LLM context.\n\n### ToolCall\n\nOpenAI-shaped tool invocation attached to an assistant turn.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `id` | `string` | yes | — | Stable id; echoed by the paired `role: \"tool\"` turn via `tool_call_id` |\n| `type` | `string` | no | `\"function\"` | Only `\"function\"` is meaningful today |\n| `function` | [ToolFunction](#toolfunction) | yes | — | The function being called |\n\n### ToolFunction\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `name` | `string` | yes | Function name as registered with the agent |\n| `arguments` | `string` | yes | **JSON-encoded string** (OpenAI convention — not an object) |\n\n### FilterNode (filter DSL)\n\nA recursive boolean tree of predicates. Used by `/search.filters` and\n`/get.filters`. The Pydantic envelope only checks the recursive\ncombinator shape; field-level validity (which scalar fields are\nfilterable, which operators apply, value coercion) runs when the\nnode is compiled to a backend-specific filter clause server-side. Compile\nerrors surface as `422` with the offending field / operator in\n`error.message`.\n\n#### Shape\n\nA node is a JSON object whose keys are one of:\n\n| Key | Value | Notes |\n|---|---|---|\n| `AND` | `array<FilterNode>` | All child nodes must match. Omit if not needed |\n| `OR` | `array<FilterNode>` | At least one child node must match. Omit if not needed |\n| *<allowed field>* | scalar or operator map | Predicate on that field — see [Allowed fields](#allowed-fields) and [Operators](#operators) |\n\n`AND`, `OR`, and scalar predicates **mix freely at the same level**;\nthey are implicitly joined with `AND`. A node with only scalar keys is\nfine — `AND` / `OR` arrays are not required.\n\nExample — `session_id == \"demo-001\"` AND (`sender_id` contains `\"alice\"` OR `\"bob\"`):\n\n```json\n{\n  \"session_id\": \"demo-001\",\n  \"OR\": [\n    { \"sender_id\": \"alice\" },\n    { \"sender_id\": \"bob\" }\n  ]\n}\n```\n\n<a id=\"filter-allowed-fields\"></a>\n\n#### Allowed fields\n\nThese are the only field names that may appear as a predicate key.\nAnything else returns `422`.\n\n| Field | Column kind | Value type | Notes |\n|---|---|---|---|\n| `session_id` | string | `string` | Session the row was extracted from |\n| `parent_type` | string | `string` | Parent kind, e.g. `\"memcell\"` |\n| `parent_id` | string | `string` | Parent id |\n| `timestamp` | timestamp | `integer` (Unix epoch **ms** per v1 contract) or `string` (ISO-8601) | Implementation auto-detects `>= 10^12` as ms / smaller as sec for backward compat, but the contract is ms |\n| `sender_id` | array of strings | `string` | Matches via `array_has(sender_ids, <value>)` — checks whether the row's `sender_ids` list contains the given id |\n\nThe following names are **reserved** and rejected inside `filters`.\nThey must be set at the top of the request (`/search` or `/get`),\nnot inside the DSL:\n\n```\nowner_id   owner_type   app_id   project_id\n```\n\n<a id=\"filter-operators\"></a>\n\n#### Operators\n\nA predicate value may be a **scalar shorthand** (equality) or an\n**operator map** (multiple operators in one dict are AND-joined):\n\n| Operator | SQL | Applies to |\n|---|---|---|\n| `eq` | `=` | string, timestamp; on `sender_id` becomes `array_has(...)` |\n| `ne` | `!=` | string, timestamp |\n| `gt` | `>` | timestamp |\n| `gte` | `>=` | timestamp |\n| `lt` | `<` | timestamp |\n| `lte` | `<=` | timestamp |\n| `in` | `IN (...)` | string (list of values); on `sender_id` becomes `OR` of `array_has` |\n\nExamples:\n\n```json\n// Equality shorthand\n{ \"session_id\": \"demo-001\" }\n\n// Operator map (operators inside one dict are AND-joined)\n{ \"timestamp\": { \"gte\": 1748390400000, \"lt\": 1748400000000 } }\n\n// IN list\n{ \"session_id\": { \"in\": [\"demo-001\", \"demo-002\"] } }\n\n// Array-membership on sender_ids\n{ \"sender_id\": \"alice\" }                       // array_has(sender_ids, 'alice')\n{ \"sender_id\": { \"in\": [\"alice\", \"bob\"] } }    // OR of two array_has\n```\n\n### SearchMethod\n\n| Value | Behaviour |\n|---|---|\n| `\"keyword\"` | BM25 only — pure lexical match, no embedding cost |\n| `\"vector\"` | Dense vector ANN only — semantic recall, no lexical |\n| `\"hybrid\"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter against the configured derived index backend |\n| `\"agentic\"` | Iterative cluster-path retrieval driven by a cross-encoder rerank loop; higher quality at higher latency / cost |\n\n`\"hybrid\"` is the default because it balances recall and precision\nwithout requiring the agentic loop. `\"agentic\"` calls the LLM in a loop\nand should be reserved for offline or background workflows.\n\n### GetMemoryType\n\n| Value | Track | Returned in `data.<plural>` |\n|---|---|---|\n| `\"episode\"` | user | `data.episodes` — [GetEpisodeItem](#getepisodeitem) |\n| `\"profile\"` | user | `data.profiles` — [GetProfileItem](#getprofileitem) |\n| `\"agent_case\"` | agent | `data.agent_cases` — [GetAgentCaseItem](#getagentcaseitem) |\n| `\"agent_skill\"` | agent | `data.agent_skills` — [GetAgentSkillItem](#getagentskillitem) |\n\n`memory_type` must match the requested owner kind: `\"episode\"` /\n`\"profile\"` require `user_id`; `\"agent_case\"` / `\"agent_skill\"`\nrequire `agent_id`. The mismatching combinations are rejected with\n`422`.\n\n## Endpoints\n\n### POST /api/v2/memory/add\n\nAppend a batch of messages to a session buffer. The server\naccumulates messages until the boundary detector decides the session\nis complete (or you force it with `/flush`); on boundary the LLM\nextracts a memory cell and the writers persist markdown + index rows.\n\nThe response distinguishes \"buffered\" from \"extracted this call\" via\nthe `status` field — see [Response body](#response-body) below.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `messages` | `array<MessageItem>` | yes | — | 1–500 items |\n| `defer_extraction` | `boolean` | no | `false` | buffer durably without boundary or extraction LLM calls |\n| `async_mode` | `boolean` | no | `false` | queue the batch and return `202` immediately; see [Asynchronous mode](#asynchronous-mode) |\n\n**`session_id`** — Identifies the conversation buffer on the server.\nMessages POSTed with the same `(session_id, app_id, project_id)`\naccumulate into one extraction batch. Different `session_id`s never\nshare a buffer, even within the same scope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n**`messages`** — Ordered list of [MessageItem](#messageitem) to\nappend. Order matters (it is preserved when the buffer is later\nextracted). The 1–500 cap is a per-request safety bound, not a\nsession lifetime cap — you can call `/add` many times for the same\n`session_id`.\n\n**`defer_extraction`** — When `true`, `/add` only merges the messages\ninto the durable SQLite `unprocessed_buffer`. It skips boundary\ndetection, memcell creation, and all extraction pipelines, so batching\nclients can capture every turn cheaply and call `/flush` after an idle\nwindow, a size threshold, or a session switch. A successful deferred\nadd always reports `status: \"accumulated\"`.\n\n**`async_mode`** — When `true`, `/add` persists the batch to the durable\nmemorize queue and returns `202 Accepted` without running boundary\ndetection or extraction; a background worker delivers queued batches in\narrival order per session, several sessions at a time. See\n[Asynchronous mode](#asynchronous-mode) for the `202` body, the ordering\nrule and how to poll progress.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `status` | `\"accumulated\" \\| \"extracted\"` | What the server did with the batch |\n\n**`message_count`** — Always equal to `len(messages)` on a `200` —\nthere is no partial accept. The field is present mainly for log\ncorrelation.\n\n**`status`** — Two-state outcome:\n- `\"accumulated\"` — Messages were added to the buffer; the boundary\n  detector did **not** trigger this call. The buffer remains open.\n- `\"extracted\"` — During this call the boundary detector tripped; the\n  LLM ran extraction and the writers persisted at least one memory\n  cell. The buffer is empty after the call returns.\n\n`202 Accepted` — only with `async_mode`, or when the session already has\nqueued batches (see [Asynchronous mode](#asynchronous-mode)) — returns a\nSuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"queued\"` | The batch is persisted; delivery happens in the background |\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `pending` | `integer` | Unfinished queued batches of this session, this one included |\n\nThe envelope's `request_id` is the handle for\n[`GET /tasks/{task_id}`](#get-apiv2taskstask_id).\n\n#### cURL example\n\n```bash\nTS=$(( $(date +%s) * 1000 ))\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/add \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"session_id\\\": \\\"demo-002\\\",\n    \\\"app_id\\\": \\\"default\\\",\n    \\\"project_id\\\": \\\"default\\\",\n    \\\"messages\\\": [\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $TS, \\\"content\\\": \\\"I love climbing in Yosemite every spring.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+10)), \\\"content\\\": \\\"My favorite coffee shop is Blue Bottle in SOMA.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+20)), \\\"content\\\": \\\"I bike to work most days.\\\"}\n    ]\n  }\"\n```\n\nResponse (real capture):\n\n```json\n{\n    \"request_id\": \"ae78d3f689c941eea135893e702fd171\",\n    \"data\": {\n        \"message_count\": 3,\n        \"status\": \"accumulated\"\n    }\n}\n```\n\n### POST /api/v2/memory/flush\n\nForce the boundary detector to decide **now** for the given session\nbuffer. The LLM runs extraction (one call) regardless of whether the\nheuristic would have tripped on its own. Useful at the end of a chat\nor agent run to make sure pending context becomes durable memory.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n\n**`session_id`** — Identifies which buffer to flush. Must match the\n`session_id` of prior `/add` calls in the same `(app_id, project_id)`\nscope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"extracted\" \\| \"no_extraction\"` | What happened |\n\n**`status`**:\n- `\"extracted\"` — The buffer had at least one message; extraction ran\n  and produced at least one memory cell.\n- `\"no_extraction\"` — The buffer was empty (no prior `/add` calls for\n  this `(session_id, app_id, project_id)`, or it was already flushed).\n\n`202 Accepted` with the same body as `/add` (`message_count` is `0`) only\nwhen the session already has queued batches, so the flush runs after them;\n`/flush` has no `async_mode` of its own. See\n[Asynchronous mode](#asynchronous-mode).\n\nA `200` from `/flush` is synchronous with respect to markdown persistence: by the\ntime the response returns, the new entry is on disk. Derived index\nsync is still asynchronous — see\n[Eventual consistency](#eventual-consistency).\n\n#### cURL example\n\n```bash\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/flush \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"session_id\":\"demo-002\",\"app_id\":\"default\",\"project_id\":\"default\"}'\n```\n\nResponse (real capture; this call took several seconds because of the\nextraction LLM call):\n\n```json\n{\n    \"request_id\": \"e65bcf4f56c042e39cdf50866810672c\",\n    \"data\": {\n        \"status\": \"extracted\"\n    }\n}\n```\n\n### Asynchronous mode\n\n`async_mode: true` on `/add` turns the call into \"accept and return\": the\nrequest body is written to the SQLite `memorize_inbox` table in one\ntransaction and the response is `202 Accepted`. `/flush` has no\n`async_mode` (the Cloud API has no asynchronous flush either): it runs\ninline, except that a flush on a session with queued batches joins the\nqueue behind them and answers `202` (see Ordering). An in-process\nworker then delivers queued batches to the same code path a synchronous\ncall uses: each session's batches in the order they were accepted, several\nsessions at a time. The field name, the `202` body\n(`status: \"queued\"` plus the envelope's `request_id`) and the status\nvocabulary (`queued` / `processing` / `success` / `failed`) match the\nEverOS Cloud gateway, so one client can talk to both.\n\n**Ordering.** Batches of one session are delivered strictly in arrival\norder, and a `/flush` runs after every `/add` accepted before it. To keep\nthat true, a synchronous call (`async_mode` omitted or `false`) on a\nsession that still has queued batches is queued too and answered with\n`202` — otherwise it would overtake work the client was already told is\naccepted. Clients that never use `async_mode` never see this: their\nsessions never have queued batches. Sessions are independent of each\nother: up to `max_concurrency` batches (default 4) run at once, never two\nfrom one session, so one slow session does not hold up the others, and\nthere is no ordering between sessions. Like the Cloud consumer, which runs\ngroups in parallel and each group in order.\n\n**Backpressure.** The queue is bounded. A batch is rejected before it is\nstored when:\n\n| Condition | HTTP | `error.code` | Default limit |\n|---|---|---|---|\n| Serialised body too large | `413` | `PAYLOAD_TOO_LARGE` | 4 MiB |\n| Session has too many unfinished batches | `429` | `QUEUE_SATURATED` | 50 |\n| Process has too many unfinished batches | `429` | `QUEUE_SATURATED` | 200 |\n\nThe caps are checked before the insert without a lock, so a burst of\nconcurrent requests can overshoot them by a few rows. A rejected batch was\n**not** accepted: retry later or keep it client-side.\n`pending` in every `202` body lets a client slow down before hitting `429`.\nLimits are configured under `[memorize.async_queue]`, see\n[configuration.md](configuration.md#memorizeasync_queue).\n\n**Failure.** A batch whose delivery raises is marked `failed` and skipped\nso its session keeps moving; `GET /tasks/{request_id}` shows `error`.\nThere is no automatic retry by default (`max_attempts = 1`), because the\nmemorize pipeline is not atomic: boundary detection writes the memcell and\nconsumes the session buffer before extraction runs. A failure before that\npoint (the common one: the boundary LLM call failed) leaves nothing behind,\nand re-sending the batch is safe. A failure after it leaves the memcell\nwithout its episode; re-sending then cuts a second memcell, and a `/flush`\nfinds an empty buffer. `last_error` does not say which step failed. The\nserver never re-drives a failed batch and deletes its row with the others\nafter seven days. Synchronous calls have the same property; they just hand\nthe decision to the client as a 5xx.\n\n**Delivery is at-least-once across a crash.** A row that was in flight\nwhen the process died is delivered again on the next start. If the first\ndelivery had already written its memcell, an `/add` replay duplicates that\nmemory; a `/flush` replay finds an empty buffer and does nothing, so a crash\nthat fell between the memcell write and the episode write leaves that\nmemcell without its episode. A\nnormal shutdown waits up to 30 seconds for the rows in flight (up to\n`max_concurrency` of them, one per session); a row that takes longer, or a\nprocess manager that kills earlier (`docker stop` sends SIGKILL after 10\nseconds by default), is replayed the same way, so one restart can replay\nup to `max_concurrency` batches. So is a\nbatch whose delivery had just failed when the process died before its\n`failed` mark was written, and a batch whose status write was lost (see\nStuck rows) if the server restarts before the row is marked. Replays are\nbounded: a batch that has been replayed `max_replays` times (default 3)\nwithout ever finishing is marked `failed` with `error_code`\n`replays_exhausted` instead of being requeued again, so a batch that kills\nthe process cannot crash-loop it.\n\n**Operations.** `GET /health` carries a `memorize_queue` block: `healthy`\n(the delivery loop is alive), `pending`, `oldest_pending_age_s`, `failed`,\n`loop_restarts` and `last_loop_error_at`. The probe only observes: a loop\nthat dies relaunches itself (with backoff) and is relaunched by the next\n`/add`, and `loop_restarts` counts those. Alert on `healthy` and on\n`loop_restarts` changing; a growing `oldest_pending_age_s` with a live loop\nmeans delivery is slower than arrival.\n\n**Retention.** Every queue row, whatever its status, is deleted seven days\n(`row_retention_hours`) after it was created. `GET /tasks/{task_id}` returns `404`\nafter that. A delivered row drops its request body at once. A `failed` row\nkeeps its body (in `memorize_inbox.payload_json`, found by `request_id`)\nuntil purged, and failed rows are not capped: an outage in which every batch\nfails keeps every body accepted during it for the retention period. SQLite\nreuses the freed pages but does not shrink `system.db`. A row that ages out\nwhile still `queued` was never delivered; the purge logs a warning when that\nhappens.\n\n**Stuck rows.** A row left `processing` because its status write was lost\n(SQLite busy past its timeout, disk full) is marked `failed` before the\nworker claims its next batch, or at its next scheduling pass if that write\nfails too, so a session's `pending` does not stay non-zero because of it;\nbatches the worker is still running are never swept. Its `last_error`\nreads `status write lost; outcome unknown`:\nthe batch may well have been delivered in full (the lost write is usually\nthe `success` one), so check the session's memories before re-sending it.\nIf the server restarts before that mark lands, the row is replayed like a\ncrash (see above).\n\nSetting `[memorize.async_queue] enabled = false` closes the door:\n`async_mode` is accepted but ignored and new calls run synchronously unless\ntheir session still has queued rows. Rows accepted while it was on are still\ndelivered by the worker, and a session that still has such rows still queues\nits synchronous calls behind them, so no drain is needed before switching.\nDowngrading to a release without `async_mode` is different: queued rows are\nneither delivered nor deleted there. Set `enabled = false` and restart first,\nso `async_mode` requests stop adding rows (a synchronous call still queues\nbehind its session's backlog until it drains); wait until\n`select count(*) from memorize_inbox where status in ('queued', 'processing')`\non `system.db` is 0; then stop and downgrade.\n\n### GET /api/v2/tasks/{task_id}\n\nThe outcome of one accepted request. The path and the body are the EverOS\nCloud gateway's (`GET /api/v2/tasks/{task_id}`), so a client polls both the\nsame way; `task_id` is the `request_id` a `202` carried.\n\n`200 OK`, or `404 NOT_FOUND` when the row never existed or has aged out of\n`row_retention_hours`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | The request id |\n| `status` | `\"queued\" \\| \"processing\" \\| \"success\" \\| \"failed\"` | Where the batch is. Cloud can also report `pending`; this build never does |\n| `task_type` | `\"memory_add\" \\| \"memory_flush\"` | — |\n| `error` | `string \\| null` | Summary of the failure, if any |\n| `error_code` | `string \\| null` | `status_write_lost` for a row the sweep failed, `replays_exhausted` for one replayed `max_replays` times without finishing, otherwise the exception type that failed the batch |\n| `created_at` | `string` (ISO 8601) | Accepted at |\n| `finished_at` | `string \\| null` | Set once `status` is `success` or `failed` |\n\n### GET /api/v2/tasks\n\nAccepted requests, newest first, one page at a time. Mirrors the Cloud\ngateway's listing.\n\n#### Query parameters\n\n| Parameter | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `session_id` | `string` | no | — | Only this session's requests |\n| `app_id` / `project_id` | `string` | no | `\"default\"` | Scope for `session_id` |\n| `status` | `string` | no | — | One of the four statuses; anything else is `422 INVALID_INPUT` |\n| `page` | `integer` | no | `1` | `≥ 1` |\n| `page_size` | `integer` | no | `20` | `1..100`, larger values are capped |\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `total` | `integer` | Rows matching the filters, all pages |\n| `page` / `page_size` | `integer` | As requested (`page_size` after capping) |\n| `items` | `array<TaskItem>` | The page, newest first; fields as in `GET /tasks/{task_id}` |\n\nDrain a session before exiting by polling `?session_id=X&status=queued` and\n`?session_id=X&status=processing` until both `total` are `0`. That means\nevery batch reached the memorize pipeline (unless rows aged out before\ndelivery, see Retention); it does not mean every message is an episode yet:\nmessages still in the session buffer wait for the boundary detector or the\nnext `/flush`.\n\n### POST /api/v2/memory/search\n\nHybrid retrieval over the memory store. Combines BM25, dense vector\nANN, optional scalar filtering, optional cross-encoder rerank, and\noptional final LLM rerank. Returns ranked items grouped by kind.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `user_id` | `string \\| null` | XOR with `agent_id` | `null` | `minLength=1` if set |\n| `agent_id` | `string \\| null` | XOR with `user_id` | `null` | `minLength=1` if set |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `query` | `string` | yes | — | `minLength=1` |\n| `method` | [SearchMethod](#searchmethod) | no | `\"hybrid\"` | — |\n| `top_k` | `integer` | no | `-1` | `-1` or `1..100` |\n| `radius` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `min_score` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `include_profile` | `boolean` | no | `false` | — |\n| `enable_llm_rerank` | `boolean` | no | `false` | — |\n| `filters` | [FilterNode](#filternode-filter-dsl) `\\| null` | no | `null` | — |\n\n**`user_id` / `agent_id`** — **Exactly one** must be set. Determines\nwhich track is searched: `user_id` → user-memory (episodes /\nprofiles); `agent_id` → agent-memory (cases / skills).\n\n**`app_id` / `project_id`** — Scope identifiers; results never cross\nscopes.\n\n**`query`** — The retrieval query. The same string is fed to BM25\ntokenization and to the embedding model, then the two recall lists\nare fused.\n\n**`method`** — Retrieval method; see [SearchMethod](#searchmethod).\nDefault `\"hybrid\"` is the recommended starting point.\n\n**`top_k`** — Maximum number of items to return per kind. Two modes:\n- `-1` (default) — \"Use server defaults.\" Recall falls back to a\n  fixed internal cap, and a server-side `radius` default kicks in if\n  the caller did not pass one (see `radius` below). Use for \"give me\n  whatever you find worth returning.\"\n- `1..100` — Explicit cap. Recall is sized as `top_k × multiplier`\n  and **no** default `radius` is applied unless you set it yourself.\n\nValues of `0` or outside `-1` / `1..100` are rejected with `422`.\n\n**`radius`** — Optional **cosine-similarity threshold** in `[0.0, 1.0]`.\nCandidates whose score is below this value are dropped — use it to cut\na long tail of weak matches. Three-level fallback for the effective\nradius:\n\n1. Caller-supplied `radius` (including the literal `0.0`) always wins.\n2. With `top_k=-1` and no caller-supplied `radius`, a server-side\n   default radius kicks in.\n3. With `top_k>0` and no caller-supplied `radius`, no threshold is\n   applied (`null`).\n\n**`min_score`** — Optional **post-fusion relevance floor** in\n`[0.0, 1.0]`. Results below this score are evicted after fusion,\nindependent of `radius` (which is a per-recall cosine threshold).\n\n**`include_profile`** — When `user_id` is set, also fetch the user's\nprofile and include it in `data.profiles`. The profile is not\nranked; `score` is `null`. Ignored when `agent_id` is set.\n\n**`enable_llm_rerank`** — Opt-in LLM rerank pass for\n`method: \"hybrid\"`. Applies to `agent_case` and `agent_skill` fusion\nonly; the episode hybrid path has built-in fact eviction and\nignores this flag. Adds one LLM call per request. Ignored by\n`keyword` / `vector` (no fusion to rerank) and `agentic` (uses its\nown cross-encoder loop).\n\n**`filters`** — Optional filter-DSL node; see\n[FilterNode](#filternode-filter-dsl). Applied **before** ranking, so\nit does not perturb the ranker.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All five\narrays are always present so client code can iterate without\nbranching on owner type; arrays that do not apply to the requested\nowner kind stay as `[]`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set |\n| `profiles` | `array<SearchProfileItem>` | Populated when `user_id` is set **and** `include_profile=true` |\n| `agent_cases` | `array<SearchAgentCaseItem>` | Populated when `agent_id` is set |\n| `agent_skills` | `array<SearchAgentSkillItem>` | Populated when `agent_id` is set |\n| `unprocessed_messages` | `array<UnprocessedMessageDTO>` | Populated **only** when `filters.session_id` is a top-level eq scalar; otherwise stays `[]`. Independent of `user_id` / `agent_id` (buffer rows have no owner attribution — boundary detection runs before owner inference) |\n\n#### SearchEpisodeItem\n\nUser-track conversation episode hit. `score` is the fused retrieval\nscore; `atomic_facts` lists single-sentence facts that matched the\nquery within this episode (already nested, no separate call needed).\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_ep_<YYYYMMDD>_<NNN>` |\n| `user_id` | `string \\| null` | Owner of this episode |\n| `app_id` | `string` | Scope where the episode lives |\n| `project_id` | `string` | Scope where the episode lives |\n| `session_id` | `string` | The `session_id` whose messages produced this episode |\n| `timestamp` | `string` | ISO-8601 with timezone offset; default `UTC` renders as `Z`, non-UTC defaults render as `±HH:MM` — see [Conventions](#conventions) |\n| `sender_ids` | `array<string>` | Distinct `sender_id`s in the underlying messages |\n| `summary` | `string` | Short summary (~200 chars), suitable for list-view rendering |\n| `subject` | `string` | One-line subject / title |\n| `episode` | `string` | Full extracted narrative |\n| `type` | `\"Conversation\"` | Reserved; today only conversation-derived episodes ship |\n| `score` | `number` | Fused retrieval score for this episode |\n| `atomic_facts` | `array<SearchAtomicFactItem>` | Sub-facts extracted from the same episode that matched the query |\n\n> **`id` is unique within `(app_id, project_id)`.** The same `id` can appear\n> under two projects (each project keeps its own daily files and sequence);\n> address a row globally by `(app_id, project_id, id)`. Within one response\n> ids never repeat, because a request is always scoped to one project.\n\n#### SearchAtomicFactItem\n\nA single-sentence fact pulled out of an episode during extraction.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_af_<YYYYMMDD>_<NNN>` |\n| `content` | `string` | The fact as a single sentence |\n| `score` | `number` | Same score scale as the parent episode |\n\n#### SearchProfileItem\n\nUser profile. Only populated when `include_profile=true` and\n`user_id` is set.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Profile id |\n| `user_id` | `string \\| null` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `profile_data` | `object` | Free-form structured profile fields produced by extraction (schema is profile-specific) |\n| `score` | `number \\| null` | `null` for direct fetches (no query-aware profile ranking yet) |\n\n#### SearchAgentCaseItem\n\nAgent-track case hit. Returned only when the request uses `agent_id`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_ac_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `session_id` | `string` | The agent run this case came from |\n| `task_intent` | `string` | What the agent was trying to do (one-sentence framing) |\n| `approach` | `string` | How the agent went about it |\n| `quality_score` | `number` | Quality assessed at extraction time, in `[0, 1]` |\n| `key_insight` | `string \\| null` | One-line takeaway; `null` if extraction did not produce one |\n| `timestamp` | `string` | ISO-8601 UTC of when the run happened |\n| `score` | `number` | Fused retrieval score for this case |\n\n#### SearchAgentSkillItem\n\nAgent-track skill hit — a named procedural memory produced by\nclustering related cases.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_sk_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `name` | `string` | Skill name (a stable, human-meaningful slug) |\n| `description` | `string` | One-line description |\n| `content` | `string` | Full skill body (procedure / playbook text) |\n| `confidence` | `number` | Extraction confidence in `[0, 1]` |\n| `maturity_score` | `number` | Maturity assessed at clustering time, in `[0, 1]` |\n| `source_case_ids` | `array<string>` | `agent_case` ids that contributed to this skill |\n| `score` | `number` | Fused retrieval score for this skill |\n\n#### UnprocessedMessageDTO\n\nA raw message still sitting in the boundary-detection buffer — sent to\n`/add` but not yet carved into an episode / case. Returned **only**\nwhen the request's `filters` contains `session_id` as a top-level eq\nscalar (`{\"session_id\": \"<sid>\"}`); compound shapes (`AND` / `OR`\ncombinators, operator maps such as `{\"eq\": ...}` / `{\"in\": ...}`) do\nnot trigger the lookup because there is no defensible buffer-scope\nmapping for them. Buffer rows have no `user_id` / `agent_id`\nattribution, so `session_id` is the only meaningful query dimension.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Original `message_id` from `/add` |\n| `app_id` | `string` | Scope where the buffered message lives |\n| `project_id` | `string` | Scope where the buffered message lives |\n| `session_id` | `string` | Matches the requested `filters.session_id` |\n| `sende\n\n<!-- [TRUNCATED AT 50KB] -->","docs/architecture.md":"# Architecture\n\n> Companion: [.claude/rules/architecture.md](../.claude/rules/architecture.md) (auto-loaded coding rules)\n\n## DDD layered architecture\n\n```\n┌──────────────────────────────────────────────────────┐\n│  entrypoints/  (Presentation)                         │\n│    cli + api                                          │\n├──────────────────────────────────────────────────────┤\n│  service/      (Application — Use Case orchestration) │\n│    memorize / search / get / knowledge                │\n├──────────────────────────────────────────────────────┤\n│  memory/       (Domain — Business core)               │\n│    models + extract + search + cascade + prompt_slots │\n│    + reflection + strategies + get + events            │\n├──────────────────────────────────────────────────────┤\n│  infra/persistence  (Storage adapters; infra/ may host other adapter types)    │\n│    markdown + sqlite + derived index                  │\n└──────────────────────────────────────────────────────┘\n\nCross-cutting (used by all layers, depends on none):\n  component/  ← Injectable providers (LLM / Embedding / parser / config / utils)\n  core/       ← Runtime base (observability / lifespan / context / errors / persistence / middleware)\n  config/     ← Configuration data (Settings schema + default.toml)\n```\n\n## Dependency direction (single-direction, enforced)\n\n```\nentrypoints → service → memory → infra\n```\n\n| from → to | Allowed? |\n|---|---|\n| entrypoints → service | ✅ |\n| entrypoints → memory / infra | ❌ (must go through service) |\n| service → memory | ✅ |\n| memory → infra | ✅ |\n| memory → service | ❌ |\n| infra → memory | ❌ |\n| infra cross-subpackage (e.g. lancedb → markdown within persistence/) | ❌ (use service to orchestrate); one carve-out: every index backend reads the shared record declarations in `persistence/records/` (`lancedb → records` is whitelisted in import-linter) |\n| any → component / core / config | ✅ (cross-cutting) |\n| `infra/persistence/index → component.embedding` | ✅ — the only network-capable provider `infra` reaches for. Startup measures the embedding model's real output width before the index is opened for use; nothing else can answer that question, and asking it from a higher layer would mean every index user had to remember to ask. Elsewhere `infra` uses `component` only for `utils.datetime`. |\n\nEnforced via `import-linter` in CI:\n\n```toml\n[tool.importlinter]\nroot_packages = [\"everos\"]\n\n[[tool.importlinter.contracts]]\nname = \"Layered architecture\"\ntype = \"layers\"\nlayers = [\n    \"everos.entrypoints\",\n    \"everos.service\",\n    \"everos.memory\",\n    \"everos.infra\",\n]\n```\n\n## Storage three-piece set\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│             md-first storage stack                              │\n└────────────────────────────────────────────────────────────────┘\n\n   ┌──────────────┐   ┌──────────────┐   ┌─────────────────┐\n   │   Markdown   │   │   SQLite     │   │ Derived index   │\n   │  (truth)     │   │  (state)     │   │ LanceDB/Milvus  │\n   ├──────────────┤   ├──────────────┤   ├─────────────────┤\n   │ entries +    │   │ change queue │   │ vector ANN      │\n   │ frontmatter  │   │ + state/LSN  │   │ BM25 (Tantivy)  │\n   │ Git friendly │   │ buffer /     │   │ scalar filter   │\n   │ Obsidian OK  │   │   audit      │   │ multi-modal     │\n   └──────────────┘   └──────────────┘   └─────────────────┘\n          │                  │                    │\n          ▼                  ▼                    ▼\n    memory-root/         .index/sqlite/      .index/<backend>/\n   (truth source)       (system data)       (rebuildable)\n```\n\n## Write path\n\n```\nExternal message\n       │\n       ▼\n1. service.memorize           (entrypoint of write path)\n       │\n       ▼\n2. memory.extract.pipeline    (calls everalgo)\n       │\n       ▼\n3. infra.persistence.markdown.write       (atomic: tmp + fsync + rename)\n       │  ✅ md write success → return immediately\n       │\n   ┌───┴────┐\n   │        │\n   ▼        ▼\n4a. SQLite   4b. memory.cascade  (async daemon)\n    audit        watches md → diff entries → index sync\n```\n\n**Key guarantee**: md write is strongly consistent (fsync). The derived\nindex is eventually consistent. Index backend unavailability does not block\nresponse — changes buffer in the SQLite `md_change_state` queue, replayed on\nrecovery.\n\n## Read path\n\n```\nUser query\n   │\n   ▼\n1. service.search\n   │\n   ▼\n2. memory.search (hybrid)     BM25 + vector ANN + scalar filter\n                              through the configured index backend\n   │\n   ▼\n3. (optional) read md         original markdown for context\n   │\n   ▼\n   Return\n```\n\n## Key components\n\n### `memory/extract/`\n\n```\nextract/\n├── ingest/      Standardized message intake + multi-modal parser dispatch\n├── parser/      Input parsing (format normalization, message preprocessing)\n├── pipeline/    Main extraction pipeline (calls everalgo + dual-track split + writes store)\n└── evolution/   Async memory evolution (event/counter/cron triggers)\n```\n\n### `memory/cascade/`\n\nDaemon that watches markdown changes and syncs to the configured derived\nindex backend:\n\n- inotify / FSEvents file watcher (cross-platform via `watchdog`)\n- 500ms debounce\n- Entry-level diff (added / changed / removed)\n- Per-entry index upsert / delete (text + vector columns update together)\n- LSN-based crash recovery via the SQLite `md_change_state` queue\n- Handlers for all eight business kinds: episode, atomic_fact, foresight,\n  user_profile, agent_case, agent_skill, knowledge_document, knowledge_topic\n\n### `memory/prompt_slots/`\n\nThree-layer prompt overlay:\n\n```\nconfig/prompt_slots/*.yaml          (Layer 1: defaults, ships with package)\n       ↓\n~/.everos/prompt_slots/*.yaml       (Layer 2: app-level override)\n       ↓\nruntime override                    (Layer 3: per-call override)\n```\n\nExtractors may accept a prompt-override parameter; EverOS supplies overrides for episode and boundary-detection prompts, and falls back to the algo-bundled default elsewhere — no hardcoded prompts in algorithm code.\n\n### `memory/reflection/`\n\nOffline memory self-improvement. The orchestrator (`orchestrator.py`)\nimplements the Select → Merge → Re-extract → Deprecate pipeline, merging\nfragmented episodes within a cluster into a single coherent narrative. Driven\nby the `reflect_episodes` OME strategy (cron, disabled by default).\n\n### `memory/strategies/`\n\nOME strategy implementations — one file per strategy:\n\n- `extract_atomic_facts` / `extract_foresight` / `extract_user_profile` — user pipeline\n- `extract_agent_case` / `extract_agent_skill` — agent pipeline\n- `reflect_episodes` — offline episode consolidation (cron)\n- `trigger_profile_clustering` / `trigger_skill_clustering` — clustering triggers\n\n### `core/observability/`\n\nThree-piece observability:\n\n- `metrics/` — Prometheus counter / gauge / histogram + global registry\n- `logging/` — structlog with context processor (trace_id propagation)\n- `tracing/` — OpenTelemetry tracer + span helpers\n\n## Markdown layout\n\n```\n~/.everos/                                  # memory root (default; EVEROS_ROOT)\n└── <app_id>/<project_id>/                  # scope (\"default\" → default_app/default_project)\n    ├── users/<user_id>/\n    │   ├── user.md                                     # profile (single-file rewrite)\n    │   ├── episodes/episode-<YYYY-MM-DD>.md            # daily-log append\n    │   ├── .atomic_facts/atomic_fact-<YYYY-MM-DD>.md   # hidden, framework-derived\n    │   └── .foresights/foresight-<YYYY-MM-DD>.md       # hidden, framework-derived\n    ├── agents/<agent_id>/\n    │   ├── .cases/agent_case-<YYYY-MM-DD>.md           # hidden, framework-derived\n    │   └── skills/skill_<name>/SKILL.md                # named-dir\n    └── knowledge/                                      # global shared knowledge\n```\n\nSystem-managed entries (`.index/`, `.tmp/`) and `ome.toml` live directly\nunder the memory root.\nFull tree + frontmatter chassis: [storage_layout.md](storage_layout.md) and\n[how-memory-works.md](how-memory-works.md). Frontmatter has 4-tier field\nprotection (L1 read-only / L2 system / L3 business / L4 user).\n\n## everalgo boundary\n\n`everalgo` is a set of PyPI-published packages (`everalgo-user-memory`,\n`everalgo-agent-memory`, `everalgo-rank`, `everalgo-knowledge`, plus the\noptional `everalgo-parser` extra), imported under the `everalgo` namespace,\nholding **only memory extraction algorithms**:\n\n- `everalgo.parser` — multi-modal parsing (optional `[multimodal]` extra)\n- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile extractors\n- `everalgo.agent_memory` — AgentMemCell / Case / Skill extractors\n- `everalgo.rank` — boundary detection / fusion + rerank\n- `everalgo.knowledge` — KnowledgeExtractor (document parse + topic extraction)\n\neveralgo is:\n\n- **Stateless** — pure functions, no class hierarchy\n- **No I/O** — does not touch md files, derived indexes, or SQLite\n- **No prompts inline** — extractors that accept a prompt-override parameter use the project-supplied value; others use their algo-bundled defaults\n\nThis boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).\n\n## Error handling architecture\n\n### Exception hierarchy\n\nAll application exceptions derive from `AppError` (`core/errors.py`),\nsplit into four branches by nature:\n\n```\nAppError\n├── DomainError                    (client-side / business-rule violations)\n│   ├── NotFoundError              → 404 NOT_FOUND\n│   │   ├── DocumentNotFoundError\n│   │   └── TopicNotFoundError\n│   ├── ConflictError              → 409 CONFLICT\n│   │   └── DuplicateDocumentError\n│   ├── InvalidInputError          → 422 INVALID_INPUT\n│   │   ├── ExtractionEmptyError   → 422 EXTRACTION_EMPTY\n│   │   └── FilterError\n│   ├── PathTraversalError         → 400 BAD_REQUEST\n│   └── UnsupportedModalityError   → 415 UNSUPPORTED_FORMAT\n├── InfrastructureError            (transient, retryable)     → 503\n│   ├── StorageError\n│   ├── VectorStoreError\n│   └── ExternalServiceError\n│       ├── LLMServiceError\n│       ├── EmbeddingServiceError\n│       └── RerankServiceError\n├── CapabilityError                (permanent, not retryable) → 503\n│   └── MultimodalNotEnabledError\n└── ConfigurationError             (misconfiguration)         → 500\n```\n\n### Error propagation strategy\n\nExceptions are raised at the layer where the error is detected and\npropagate naturally — **service and route layers do not catch-and-wrap**.\nThe entrypoints layer registers per-type exception handlers\n(`entrypoints/api/exception_handlers.py`) via Starlette's MRO dispatch.\nEach handler converts the exception into a canonical error envelope with\nan `ErrorCode` enum value and the appropriate HTTP status code.\n\n### Boundary translation\n\nThird-party exception types are translated at the component boundary to\nprevent external types from leaking into upper layers:\n\n- `everalgo.llm.LLMError` → `LLMServiceError` at `component/parser/_core.py`\n- Embedding / rerank provider errors → `EmbeddingServiceError` / `RerankServiceError` at their respective protocol modules\n\n## Further reading\n\n- [docs/overview.md](overview.md) — vision and scope\n- [docs/engineering.md](engineering.md) — engineering tooling and CI / CD\n- [.claude/rules/architecture.md](../.claude/rules/architecture.md) — short-form rules for Claude Code\n","use-cases/claude-code-plugin/plugin.json":"{\n  \"name\": \"evermem\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Automatically recalls relevant memories from past sessions and injects them into Claude's context\",\n  \"author\": {\n    \"name\": \"EverMem\"\n  },\n  \"keywords\": [\"memory\", \"context\", \"recall\", \"persistence\"]\n}\n","use-cases/claude-code-plugin/skills/memory-tools.md":"---\ndescription: Guidance for using EverMem memory tools to recall past session context\nalwaysInclude: true\n---\n\n# EverMem Memory Tools\n\nYou have access to memory tools that can recall context from the user's past coding sessions. Use these tools proactively when they would help provide better assistance.\n\n## Available Tools\n\n- **evermem_search**: Search past conversations using semantic + keyword matching. Params: `query` (required), `limit` (default 10, max 20)\n\n## When to Use Memory Search\n\n**DO search memories when:**\n- User asks about past work, decisions, or implementations (\"how did we handle X?\")\n- User references previous sessions (\"remember when\", \"last time\", \"we discussed\")\n- User is debugging something that may have been solved before\n- User asks about project patterns, conventions, or architecture decisions\n- Context from previous sessions would improve your response\n- User seems to expect you to know something from before\n\n**DON'T search memories when:**\n- The question is self-contained and doesn't need historical context\n- User explicitly provides all needed context in their message\n- It's a general knowledge question unrelated to their project history\n- You've already searched for this topic in the current session\n\n## Best Practices\n\n1. **Be selective**: Don't search for every query - only when past context adds value\n2. **Use specific queries**: Search for relevant terms, not the entire user message\n3. **Synthesize results**: When you find relevant memories, integrate them naturally into your response\n4. **Be transparent**: Mention when your response is informed by past session context\n"},"files":{".claude/rules/architecture.md":"# Architecture rule (always loaded)\n\nEverOS is a DDD-layered framework. The dependency direction is **single, downward only**:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — CLI + HTTP API (presentation). No business logic.\n- `service/` — use-case orchestration (memorize / retrieve / evolve / manage).\n- `memory/` — domain (extract / search / cascade / prompt_slots / models).\n- `infra/` — storage adapters (markdown + sqlite + lancedb) and the OME subsystem.\n- `component/` — injectable providers (llm / embedding / config / utils).\n- `core/` — runtime base (observability / lifespan / context / persistence primitives).\n- `config/` — configuration data (Settings + default TOML).\n\n## Hard constraints (enforced by `import-linter`, run in `make lint`)\n\n1. **Layering**: an outer layer may import an inner layer, never the reverse.\n   `entrypoints → service → memory → infra`.\n2. **Private internals**: `service`, `memory`, and `entrypoints` must not import\n   `infra.persistence.{markdown,lancedb,sqlite}.**` internals — go through the\n   package facade (`from everos.infra.persistence.markdown import ...`).\n3. **OME isolation**: `infra.ome` must not import `persistence`, `memory`,\n   `service`, or `entrypoints`. It is a low-level scheduler with no domain knowledge.\n\nIf a change needs to cross a boundary the wrong way, the design is wrong — refactor,\ndon't add an exception.\n\nFull rationale: [docs/architecture.md](../../docs/architecture.md).\n",".claude/rules/async-programming.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Async programming rule\n\nThe write/read paths are async end-to-end. Keep them non-blocking.\n\n- **No blocking calls in async functions** — no synchronous file I/O, no `time.sleep`,\n  no blocking DB/network calls inside `async def`. Ruff `ASYNC` flags the common cases.\n- **Offload CPU/blocking work** with `anyio.to_thread.run_sync` (or the established\n  helper) rather than blocking the event loop.\n- **Concurrency** via `asyncio.gather` / `asyncio.TaskGroup` for independent awaits;\n  don't `await` in a loop when the calls are independent.\n- **Tests**: `pytest-asyncio` is in `auto` mode — an `async def test_*` just works,\n  no `@pytest.mark.asyncio` needed.\n- **Don't fire-and-forget** without holding a reference (`asyncio.create_task` results\n  must be tracked, or you lose exceptions). The OME subsystem owns the long-running\n  background loops — application code shouldn't spawn its own.\n",".claude/rules/code-style.md":"# Code style rule (always loaded)\n\n- **Formatter & linter**: `ruff` is the single tool (replaces black / isort / flake8).\n  Line length 88, target `py312`. Run `make format` to auto-fix; `make lint` checks.\n- **Active ruff rule sets**: `E F I N UP B SIM ASYNC`. Don't disable a rule inline\n  unless there's a genuine reason — prefer fixing the code.\n- **Type hints**: annotate every public function signature (params + return). The\n  codebase is ~100% typed; keep it that way.\n- **`from __future__ import annotations`** at the top of every module — annotations\n  are strings, so forward refs and `X | None` unions are free.\n- **Prefer `collections.abc`** (`Sequence`, `Mapping`) over concrete `list`/`dict`\n  in signatures; use `Protocol` for structural interfaces.\n- **No dead code**: no commented-out blocks, no unused imports, no speculative\n  abstractions. Delete rather than comment out.\n- **Naming**: `*Manager` (orchestrators), `*Provider` (injectable services),\n  `*Reader`/`*Writer` (persistence), `*Recaller` (search routes). Follow the\n  established suffix when adding a sibling.\n",".claude/rules/datetime-handling.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Datetime handling rule (two-zone discipline)\n\n**Never** construct or read \"now\" directly. All datetime flows through\n`everos.component.utils.datetime`. This is a **hard CI gate**\n(`make check-datetime`, wired into `make lint`).\n\n## Banned (the checker fails the build on these)\n\n- `datetime.now()`, `datetime.utcnow()`, `datetime.today()`\n- `time.time()`, `time.time_ns()`\n- `datetime(YYYY, ...)` without `tzinfo=`\n- `.astimezone(...)` / `.replace(tzinfo=...)` outside the helper module\n\n## Use instead\n\n| Need | Helper |\n|---|---|\n| \"now\" for **storage** (UTC) | `get_utc_now()` |\n| \"now\" for **display** (configured TZ) | `get_now_with_timezone()` |\n| today's date, display TZ | `today_with_timezone()` |\n| normalize a value to UTC | `ensure_utc(d)` |\n| render to display TZ | `to_display_tz(d)` |\n| parse ISO / epoch / str | `from_iso_format(v)`, `from_timestamp(ts)` |\n| serialize | `to_iso_format(d)`, `to_date_str(d)`, `to_timestamp_ms(d)` |\n\n**Two zones**: persist in UTC, present in the configured display TZ. Crossing them\ngoes through the helpers — never ad-hoc. See [docs/datetime.md](../../docs/datetime.md).\n",".claude/rules/imports.md":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Imports rule\n\n- **`from __future__ import annotations`** is the first import in every module.\n- **Import order** (ruff `I` enforces, `make format` fixes): stdlib → third-party\n  → first-party (`everalgo`, then `everos`). One group per blank-line-separated block.\n- **Absolute imports** for cross-package references (`from everos.memory import ...`).\n  Relative imports (`from .models import ...`) only **within** a package, typically\n  in its `__init__.py`.\n- **`TYPE_CHECKING` guard** for import cycles and type-only imports:\n  ```python\n  from typing import TYPE_CHECKING\n  if TYPE_CHECKING:\n      from everos.config import Settings\n  ```\n- Never import a private internal across a package boundary — respect the\n  `import-linter` contracts (see [architecture.md](architecture.md)).\n",".claude/rules/init-py-and-reexport.md":"---\npaths:\n  - \"src/**/__init__.py\"\n  - \"src/**/*.py\"\n---\n\n# `__init__.py` and re-export rule\n\nA package's `__init__.py` is its **public facade**. Consumers import from the\npackage, never from its internal modules.\n\n## Pattern\n\n```python\n\"\"\"One-paragraph module docstring: what this package is and how to use it.\"\"\"\n\nfrom .models import Episode as Episode\nfrom .models import MemCell as MemCell\n\n__all__ = [\n    \"Episode\",\n    \"MemCell\",\n]\n```\n\n- **Explicit `X as X` redundant-alias form** on each re-export. This is intentional:\n  it marks the name as a deliberate public re-export (ruff `F401` / `PLC0414` aware)\n  rather than an accidental unused import.\n- **`__all__`** lists every public name, alphabetically sorted, matching the\n  re-exports. It is the contract; keep it in sync.\n- **Internal modules stay private** — don't re-export helpers that aren't part of\n  the public API.\n- New subpackage? Add an `__init__.py` with a docstring + `__all__` even if it\n  starts small. Empty-but-documented beats missing.\n\nThis facade discipline is what lets `import-linter` forbid deep imports across\npackage boundaries (see [architecture.md](architecture.md)).\n",".claude/rules/language-policy.md":"# Language policy rule (always loaded)\n\nThe project targets a global audience and is **English-first**.\n\n- **Code, comments, docstrings, docs, commit messages, identifiers**: English only.\n- **CJK characters are allowed only in**:\n  - test fixtures under `tests/` and `data/` (multilingual input is the point), and\n  - locale-suffixed mirror files (e.g. `*_zh.json`).\n- Do **not** introduce CJK into `src/`, `docs/`, or config.\n\nEnforcement: `make check-cjk` scans for stray CJK outside the allowlist (advisory).\nKeep user-facing strings and error messages in English.\n",".claude/rules/logging-observability.md":"---\npaths:\n  - \"src/**/*.py\"\n---\n\n# Logging & observability rule\n\n- **Use the project logger**, never `print` or the stdlib `logging` directly:\n  ```python\n  from everos.core.observability.logging import get_logger\n  logger = get_logger(__name__)\n  ```\n- **Structured logging** (`structlog`): pass context as keyword fields, not f-strings.\n  ```python\n  logger.info(\"memory.search.completed\", owner_type=owner, n_results=len(items))\n  ```\n  Event name first (dotted, stable), structured kwargs after. This keeps logs\n  queryable and avoids leaking interpolated PII into the message string.\n- **Levels**: `debug` for developer detail, `info` for lifecycle milestones,\n  `warning` for recoverable anomalies, `error` for failures with a stack/context.\n- **Metrics** go through `core.observability.metrics` (Prometheus); don't invent\n  ad-hoc counters. Histograms/counters/gauges have registry helpers.\n- Don't log secrets, API keys, or full memory content at `info`/above.\n- **Tracing** (optional, `[otel]` extra, **off by default**): open spans with\n  `memory_span(...)` from `core.observability.tracing` — it stamps the Langfuse\n  `langfuse.*` attributes and is a no-op until `[observability] enabled`, so call\n  sites never branch on config. LLM / embedding token usage rides\n  `set_generation_usage` onto the active span (Langfuse computes cost).\n  Request/response content is emitted only when `capture_content` is on\n  (redaction hook + truncation). `request_id` is kept independent of the OTel\n  `trace_id`; an upstream `traceparent` header is continued when present.\n",".claude/rules/module-docstring.md":"---\npaths:\n  - \"src/everos/infra/**/*.py\"\n  - \"src/everos/memory/**/*.py\"\n  - \"src/everos/service/**/*.py\"\n  - \"src/everos/component/**/*.py\"\n  - \"src/everos/core/**/*.py\"\n---\n\n# Module docstring rule\n\nEvery non-trivial module in the domain/infra layers opens with a docstring that\nexplains **intent and contract**, not just a one-line label.\n\nA good module docstring states:\n\n- **What** the module is responsible for (one sentence).\n- **The load-bearing invariants** — the rules a reader must know to change it\n  safely (partition keys, what is/isn't written, defaults, ignored flags).\n- **External usage** when the module is a package facade (a short import example).\n\nExample (abbreviated, from `memory/search/manager.py`):\n\n```python\n\"\"\"SearchManager — top-level orchestrator for POST /api/v2/memory/search.\n\nHard partition by owner_type: user → episodes (+ profiles), agent →\nagent_cases + agent_skills. The manager never writes to storage; it only\nreads LanceDB + markdown.\n\"\"\"\n```\n\nPrefer prose that would save the next engineer a debugging session over\nboilerplate. If a module is genuinely trivial (a 3-line constant), a one-liner\nis fine — but most modules here are not.\n",".claude/rules/testing.md":"---\npaths:\n  - \"tests/**/*.py\"\n---\n\n# Testing rule\n\nTests mirror the source layout: `tests/unit/test_<layer>/...`,\n`tests/integration/...`, `tests/e2e/...`.\n\n- **Structure**: `tests/unit/` mirrors `src/everos/` package-for-package. Put a test\n  next to where its subject lives in the mirror.\n- **Async**: `pytest-asyncio` is in `auto` mode — write `async def test_*` directly,\n  no marker needed.\n- **Markers** (default run excludes both — `-m \"not slow and not live_llm\"`):\n  - `@pytest.mark.slow` — tests ≥ ~10s.\n  - `@pytest.mark.live_llm` — needs real LLM/embedder credentials.\n  Keep unit tests fast and credential-free; push anything needing real services\n  behind a marker or into `integration`/`e2e`.\n- **Fixtures**: shared fixtures live in the nearest `conftest.py`. The root conftest\n  resets module caches (settings/logging/datetime) per test — rely on that for\n  isolation rather than mutating globals.\n- **Module docstring** on each test file stating what contract it pins (see existing\n  tests for the style).\n- **Coverage gate**: `make cov` enforces 80% (`--cov-fail-under=80`). New code should\n  not drop coverage below the gate.\n- Run `make test` (unit) and `make integration` before pushing; both run in CI.\n",".claude/skills/add-memory-kind/SKILL.md":"---\nname: add-memory-kind\ndescription: 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).\n---\n\n# /add-memory-kind — Add a new business memory kind\n\n## When to invoke\n\nAdding a new persisted business entity (Episode, Case, Skill, AtomicFact,\nForesight, Profile, or something custom). Multiple storage layers may be\ninvolved; this skill walks the decision then the wiring.\n\n## 1. Decide the storage combination\n\nA memory kind **does not have to use all three** layers. Pick by what\nthe kind actually needs:\n\n| Need | Markdown | SQLite | LanceDB |\n|---|:-:|:-:|:-:|\n| Human-readable / agent-editable source-of-truth text | ✅ | | |\n| Structured state, ACID transactions, joins, predicates | | ✅ | |\n| Vector / BM25 / hybrid retrieval | | | ✅ |\n\nCommon combinations seen in EverOS:\n\n| Combo | Example | Rationale |\n|---|---|---|\n| **md only** | scratch notes / dump bins | text-of-truth, no index needed |\n| **md + lancedb** | episode / memcell / case | text-of-truth + semantic retrieval |\n| **md + sqlite** | profile / playbook / soul.md state | text-of-truth + structured state to query |\n| **md + sqlite + lancedb** | full-blown business records | when you need *both* transactional state AND retrieval |\n| **sqlite only** | audit log / task queue / LSN watermark | system state, never user-facing |\n| **lancedb only** | rare; usually you still want md | derived embeddings without text-of-truth |\n\nRule of thumb: **markdown is the truth**; sqlite and lancedb are derived\nindexes that can be rebuilt from md. Drop md only when the kind has no\nhuman-readable form (pure system state).\n\n## 2. Pick the markdown storage strategy (if md is in your combo)\n\nThree strategies — declared in the EverOS Markdown First spec:\n\n| Strategy | Filename | Mutation | Examples |\n|---|---|---|---|\n| **Daily-log append** | `<prefix>-YYYY-MM-DD.md` | append entries | memcell / episode / case / atomic_fact / foresight |\n| **Skill-named in-place** | `skill_<name>.md` | overwrite the file | skills (procedural memory) |\n| **Single-file rewrite** | `user.md` / `agent.md` / `soul.md` / `behaviors.md` / `tools.md` | overwrite the file | profiles / playbooks |\n\nThis skill currently has a **complete recipe for daily-log append**.\nSkill-named and single-file recipes are sketched at the bottom — their\nbase writers (`BaseSkillWriter` / `BaseProfileWriter`) land later in the\nproject; until then build a thin wrapper over `MarkdownWriter`\ndirectly.\n\n---\n\n## 3. Markdown daily-log: 4 steps\n\n### 3.1 Frontmatter schema — `infra/persistence/markdown/mds/<name>.py`\n\n```python\n\"\"\"Episode daily-log frontmatter.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime as _dt\nfrom typing import ClassVar, Literal\n\nfrom everos.core.persistence.markdown import UserScopedFrontmatter\n\n\nclass UserEpisodeDailyFrontmatter(UserScopedFrontmatter):\n    \"\"\"``users/<u>/episodes/episode-<YYYY-MM-DD>.md``.\"\"\"\n\n    ENTRY_ID_PREFIX: ClassVar[str] = \"ep\"\n    DIR_NAME: ClassVar[str] = \"episodes\"\n    FILE_PREFIX: ClassVar[str] = \"episode\"\n\n    type: Literal[\"user_episode_daily\"] = \"user_episode_daily\"\n    date: _dt.date\n    entry_count: int = 0\n    last_appended_at: _dt.datetime | None = None\n```\n\nFor agent-track kinds subclass `AgentScopedFrontmatter` instead. If\nuser-track and agent-track share a kind name (e.g. `memcell`), give\neach a **distinct** `ENTRY_ID_PREFIX` (e.g. `umc` vs `amc`) so reverse\nlookup is unambiguous.\n\n### 3.2 Re-export — `mds/__init__.py`\n\n```python\nfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatter\n```\n\n### 3.3 Business writer — `infra/persistence/markdown/writers/<name>.py`\n\n```python\n\"\"\"Episode appender.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom everos.core.persistence import MarkdownReader\n\nfrom ..mds import UserEpisodeDailyFrontmatter\nfrom .base import BaseDailyWriter\n\n\nclass UserEpisodeAppender(BaseDailyWriter):\n    schema = UserEpisodeDailyFrontmatter\n\n    # OPTIONAL: override the count strategy. Default is len(entries);\n    # override to trust the frontmatter field instead.\n    def _current_count(self, path: Path) -> int:\n        if not path.exists():\n            return 0\n        return MarkdownReader.read(path).frontmatter.get(\"entry_count\", 0)\n```\n\n### 3.4 Re-export — `writers/__init__.py`\n\n```python\nfrom .episode import UserEpisodeAppender as UserEpisodeAppender\n```\n\n### Done — usage\n\n```python\nfrom everos.infra.persistence.markdown.writers import UserEpisodeAppender\n\nappender = UserEpisodeAppender(memory_root)\neid = appender.append(\"u_jason\", \"I went to the doctor today.\")\n# → users/u_jason/episodes/episode-<today>.md\n# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)\n```\n\n---\n\n## 4. (Optional) SQLite table — 4 steps\n\nSkip this section if the kind doesn't need structured state beyond markdown.\n\n### 4.1 Schema — `infra/persistence/sqlite/tables/<name>.py`\n\n```python\nfrom everos.core.persistence.sqlite import BaseTable, Field\n\n\nclass EpisodeState(BaseTable, table=True):\n    __tablename__ = \"episode_state\"  # type: ignore[assignment]\n\n    id: int | None = Field(default=None, primary_key=True)\n    entry_id: str = Field(index=True, unique=True)\n    cluster_id: str | None = Field(default=None, index=True)\n    status: str = Field(default=\"active\")\n```\n\n`BaseTable` already provides `created_at` / `updated_at` (auto-bumped).\n\n### 4.2 Re-export — `tables/__init__.py`\n\n```python\nfrom .episode import EpisodeState as EpisodeState\n```\n\n### 4.3 Repo — `infra/persistence/sqlite/repos/<name>.py`\n\n```python\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\n\nfrom everos.core.persistence.sqlite import RepoBase\n\nfrom ..sqlite_manager import get_session_factory\nfrom ..tables import EpisodeState\n\n\nclass _EpisodeStateRepo(RepoBase[EpisodeState]):\n    model = EpisodeState\n\n    def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:\n        return get_session_factory()\n\n\nepisode_state_repo = _EpisodeStateRepo()\n```\n\n### 4.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_state_repo as episode_state_repo\n```\n\n---\n\n## 5. (Optional) Derived index — 4 steps\n\nSkip this section if the kind doesn't need vector / BM25 / hybrid retrieval.\n\n### 5.1 Record declaration — `infra/persistence/records/<name>.py`\n\n```python\nfrom typing import ClassVar\n\nfrom everos.core.persistence.lancedb import BaseLanceTable, Vector\n\n\nclass EpisodeIndex(BaseLanceTable):\n    TABLE_NAME: ClassVar[str] = \"episode_index\"\n    PRIMARY_KEY: ClassVar[tuple[str, ...]] = (\"app_id\", \"project_id\", \"id\")\n\n    id: str\n    app_id: str = \"default\"\n    project_id: str = \"default\"\n    entry_id: str\n    text: str\n    tags: list[str]\n    vector: Vector(1024) | None = None  # type: ignore[valid-type]\n```\n\n`TABLE_NAME` / `BM25_FIELDS` / `PRIMARY_KEY` are the backend-neutral\ndeclarations every index backend reads (`IndexRecordMeta`). `PRIMARY_KEY`\nhas **no default**: `check_primary_keys` refuses to start, and LanceDB\n`upsert` refuses to write, a table that does not declare one. `id` is unique\nonly within `(app_id, project_id)`, so every business table declares the\nthree-column key — LanceDB merges on it, Milvus keys the collection on a\ncomposed `__pk` and rejects key components containing `/`. `Vector(N)` must\nmatch your embedding dimension.\n\n### 5.2 Re-export — `records/__init__.py`\n\n```python\nfrom .episode import EpisodeIndex as EpisodeIndex\n```\n\n### 5.3 Repo — `infra/persistence/lancedb/repos/<name>.py`\n\n```python\nfrom lancedb import AsyncTable\n\nfrom everos.core.persistence.lancedb import LanceRepoBase\nfrom everos.infra.persistence.records import EpisodeIndex\n\nfrom ..lancedb_manager import get_table\n\n\nclass _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):\n    schema = EpisodeIndex\n\n    async def _table_lookup(self) -> AsyncTable:\n        return await get_table(self.schema.TABLE_NAME, self.schema)\n\n\nepisode_index_repo = _EpisodeIndexRepo()\n```\n\nBusiness code writes through the `everos.infra.persistence.index` facade:\n`upsert(records)` merges on the declared key and is the only write; look a\nrow up with `find_one_where(primary_key(app_id, project_id, row_id))`.\n\n### 5.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_index_repo as episode_index_repo\n```\n\n---\n\n## 6. (Future) Skill-named & single-file markdown strategies\n\nWhen the new memory kind needs:\n\n- **skill-named** files (one file per named skill, in-place rewrite) — wait\n  for `BaseSkillWriter`, or use `MarkdownWriter.write_markdown` directly\n  with a thin wrapper.\n- **single-file rewrite** (one fixed file like `user.md`) — wait for\n  `BaseProfileWriter`, same fallback.\n\nThese strategies do **not** use entry markers; their frontmatter schema\ndoes not need `ENTRY_ID_PREFIX` (only `id` / `type` / `schema_version` plus\nthe scope mixin fields).\n\n---\n\n## 7. Verification checklist\n\n- [ ] `make lint` — ruff + import-linter clean\n- [ ] `make test` — existing manager / lifespan / writer tests still pass\n- [ ] When the kind crosses **multiple** layers:\n  - [ ] markdown entry id is the join key for the sqlite / lancedb rows\n  - [ ] business code reads only via the repo singleton (no raw engine\n        access in service / memory)\n  - [ ] cascade daemon (when it lands) can rebuild sqlite / lancedb\n        from md alone — keep md as the truth\n\nTests by layer:\n\n| Tests for | Location |\n|---|---|\n| Markdown frontmatter schema | `tests/unit/test_infra/test_markdown/test_mds/` |\n| Markdown business appender | `tests/unit/test_infra/test_markdown/test_writers/` |\n| SQLite RepoBase logic | `tests/unit/test_core/test_persistence/test_sqlite/` |\n| SQLite manager / lifespan | `tests/unit/test_infra/test_sqlite/` |\n| LanceDB LanceRepoBase logic | `tests/unit/test_core/test_persistence/test_lancedb/` |\n| LanceDB manager / lifespan | `tests/unit/test_infra/test_lancedb/` |\n\n## 8. Common pitfalls\n\n| Mistake | Symptom | Fix |\n|---|---|---|\n| Forgot `ENTRY_ID_PREFIX` / `DIR_NAME` / `FILE_PREFIX` on a daily-log schema | `BaseDailyWriter.__init__` raises `TypeError` | Add all three ClassVars |\n| Same `ENTRY_ID_PREFIX` on user + agent variants | `MemoryLayout.locate_for_entry` collision error | Use distinct prefixes (e.g. `umc` vs `amc`) |\n| Imported `RepoBase` from `infra.persistence.sqlite` | `ImportError` | Lives in `core.persistence.sqlite` (moved earlier) |\n| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each `__init__.py` |\n| `Vector(N)` mismatched with embedding dim | LanceDB raises on insert | Make `N` exactly match the model output |\n| 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 |\n| Hand-rolling `datetime.now()` instead of `today_with_timezone()` | Day-boundary drift across timezones | Always go through `everos.component.utils.datetime` |\n\n## Background\n\n- Architecture: [../../rules/architecture.md](../../rules/architecture.md)\n- `__init__.py` re-export rules: [../../rules/init-py-and-reexport.md](../../rules/init-py-and-reexport.md)\n- Async programming: [../../rules/async-programming.md](../../rules/async-programming.md)\n- Datetime handling: [../../rules/datetime-handling.md](../../rules/datetime-handling.md)\n",".claude/skills/commit/SKILL.md":"---\nname: commit\ndescription: Stage and create a Conventional Commits message for the current change\n---\n\n# /commit\n\nCreate a well-formed commit following the [Conventional Commits](https://www.conventionalcommits.org)\nstandard. The format is enforced by `gitlint` in the `commit-msg` hook.\n\n## Steps\n\n1. Run `git status` and `git diff` (and `git diff --staged`) to see what changed.\n2. Review recent history for style: `git log --oneline -10`.\n3. Group the change into a single focused commit. If the working tree mixes\n   unrelated changes, stage selectively (`git add -p` / specific paths) rather\n   than committing everything at once.\n4. Write the message in **Conventional Commits** form:\n\n   ```\n   <type>[(scope)][!]: <imperative summary, ≤72 chars>\n\n   <optional body: what & why, wrapped at 72>\n\n   <optional footer: BREAKING CHANGE: …, Refs: #123>\n   ```\n\n5. Never use `--no-verify`. If pre-commit hooks fail, fix the cause and re-commit.\n6. Do not commit secrets, generated artifacts, or work-in-progress directly to\n   the protected `main` branch.\n\n## Types\n\n| Type | Use for |\n|---|---|\n| `feat` | new feature |\n| `fix` | bug fix |\n| `refactor` | behavior-preserving restructure |\n| `test` | add / update tests |\n| `docs` | documentation |\n| `style` | formatting only |\n| `perf` | performance |\n| `chore` | config / build / tooling |\n| `build` | build system or dependencies |\n| `ci` | CI configuration |\n| `revert` | revert a previous commit |\n\n## Notes\n\n- No emoji — the title must start with the `type`.\n- One logical change per commit; keep the history bisectable.\n- The summary is imperative mood: \"add\", not \"added\" / \"adds\".\n- `scope` is optional: `fix(search): …`. A `!` before the colon (or a\n  `BREAKING CHANGE:` footer) marks a breaking change.\n",".claude/skills/new-branch/SKILL.md":"---\nname: new-branch\ndescription: Create a GitHub branch from main with the project naming convention\n---\n\n# /new-branch\n\nCut a new branch for the GitHub repository.\n\n## Branch model\n\n```\nmain       = default and protected branch\nfeat/*     = feature work\nfix/*      = bug fixes\ndocs/*     = documentation-only changes\nci/*       = CI, build, and developer-experience changes\nchore/*    = repository maintenance\nrefactor/* = behavior-preserving code structure changes\n```\n\n## Steps\n\n1. Ask (or infer) the change type: `feat`, `fix`, `docs`, `ci`, `chore`, or\n   `refactor`.\n2. Update `main` first:\n   ```bash\n   git checkout main\n   git pull --ff-only\n   ```\n3. Create the branch with a kebab-case slug:\n   ```bash\n   git checkout -b <type>/<short-slug>\n   ```\n4. Keep the branch scoped to one purpose and open a pull request back to\n   `main`.\n\n## Naming\n\n- `feat/add-agentic-rerank`, `fix/empty-profile-crash`,\n  `docs/quickstart-config`, `ci/check-github-docs`.\n- Lowercase, hyphen-separated, no spaces, concise.\n\nNever commit directly to `main` — always use a branch and pull request.\n",".claude/skills/pr/SKILL.md":"---\nname: pr\ndescription: Open a GitHub PR targeting the correct branch with the project template\n---\n\n# /pr\n\nOpen a pull request on GitHub using the `gh` CLI and the repo's PR template.\n\n## Steps\n\n1. Confirm the branch and target:\n   - Base branch is **`main`**.\n   - Head branch should be a scoped branch such as `feat/*`, `fix/*`,\n     `docs/*`, `ci/*`, `chore/*`, or `refactor/*`.\n2. Ensure local checks pass first:\n   ```bash\n   make ci\n   ```\n   Do not open a PR with failing lint/tests.\n3. Push the branch:\n   ```bash\n   git push -u origin HEAD\n   ```\n4. Create the PR, filling the template\n   ([.github/PULL_REQUEST_TEMPLATE.md](../../../.github/PULL_REQUEST_TEMPLATE.md)):\n   ```bash\n   gh pr create --base main --fill-first\n   ```\n   Then edit the body to complete each section:\n   - **Summary** — what changed and why.\n   - **Area** — tick the relevant box (architecture / benchmark / use case /\n     docs / DX / CI-build-release).\n   - **Verification** — paste the commands you ran (`make ci`, manual checks).\n   - **Checklist** — tick honestly; don't tick boxes you didn't satisfy.\n   - **Notes for Reviewers** — anything subtle.\n\n## Notes\n\n- Keep the PR scoped to one area. Split unrelated changes.\n- If `make ci` was not fully run, say so in Verification rather than implying it passed.\n- Never retarget a community PR away from `main` unless a maintainer explicitly\n  asks for it.\n",".claude/skills/release/SKILL.md":"---\nname: release\ndescription: Cut a versioned release and publish everos to PyPI via the tag-triggered workflow\n---\n\n# /release\n\nPublish a new version of `everos` to PyPI. Publishing is automated: pushing a\n`vX.Y.Z` tag triggers [.github/workflows/release.yml](../../../.github/workflows/release.yml),\nwhich builds, smoke-tests, and uploads via PyPI **Trusted Publishing** (OIDC —\nno stored token) behind the `release` environment's manual-approval gate, then\ndrafts the GitHub Release page from the CHANGELOG.\n\nA release is not finished when PyPI accepts the upload: the GitHub Release page\nis drafted, never auto-published, and someone has to write its lead summary and\nclick **Publish**.\n\n## Preconditions\n\n- On `main`, up to date, with green CI (the tag builds from `main`'s tree).\n- Decide the version per SemVer: patch = fixes, minor = back-compatible\n  features, major = breaking changes.\n\n## Steps\n\n```\n1. Bump the version    → pyproject.toml [project] version = \"X.Y.Z\"\n   (single source; everos.__version__ reads installed package metadata)\n2. Update CHANGELOG.md → move the Unreleased entries under a new\n   ## [X.Y.Z] - <date> heading, and write the release page's prose here\n   (lead paragraph + `### Upgrade` group — see \"The release page\")\n3. Commit              → git commit -m \"chore(release): vX.Y.Z\"\n4. Open a PR, merge to main after green CI\n5. Tag main + push     → git tag -a vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z\n6. Approve             → the release.yml run pauses on the `release`\n   environment; a reviewer approves in the Actions run\n7. Verify              → https://pypi.org/project/everos/X.Y.Z/\n8. Publish the page    → the run leaves a DRAFT GitHub Release, already\n   complete if step 2 was done properly. Read it once, click Publish.\n```\n\nThe tag must equal the `pyproject.toml` version — the workflow refuses to\npublish on a mismatch. A stable tag with no matching `## [X.Y.Z]` CHANGELOG\nsection fails the release job for the same reason.\n\n## The release page\n\n**The whole page is written in CHANGELOG.md, during the release PR.** Nothing\nis meant to be composed at publish time — by then the changes are weeks old and\nthe text gets no review. Write the version section like this:\n\n```markdown\n## [X.Y.Z] - 2026-09-01\n\n**What this release is for.** One paragraph, prose, no bullets — it becomes the\nlead of the release page. Say what changed for a user, not what was refactored.\n\n### Added\n### Changed\n### Fixed\n\n### Upgrade\n\nWhat a reader must know before upgrading: what happens on first startup, which\ncommand recovers a bad state, which pins moved. Omit the group entirely when a\nplain `pip install --upgrade` is all there is — 1.1.4 and 1.2.0 have nothing\nhere. Do not write filler.\n```\n\nCI turns that into the page: everything above `### Upgrade` is lifted verbatim\nwith the group headings demoted to `##`, and the Upgrade prose is wrapped in the\nboilerplate — pip line above it, compare link below — which is the shape every\nrelease since 1.1.3 has. See\n[1.2.1](https://github.com/EverMind-AI/EverOS/releases/tag/v1.2.1).\n\nSo publishing is a read-through and a click. If the draft looks wrong, the fix\nbelongs in CHANGELOG.md on `main`, not only in the draft — otherwise the two\ndrift apart and the next release inherits the habit.\n\n> While it is a draft, GitHub serves the release at\n> `releases/tag/untagged-<hash>`, and that URL keeps serving a stale page after\n> publication with no redirect. Never share it — a reader who opens it later\n> concludes the release never went out. Link `releases/tag/vX.Y.Z` instead; the\n> job prints both URLs in its step summary.\n\nPublish with the **Publish release** button in the web UI. Publishing through\nthe API by flipping `draft` alone drops the tag: GitHub rebinds the release to\nthe `untagged-<hash>` placeholder and creates a git tag by that name against the\ndefault branch (observed on 1.2.2). Pass `tag_name` if you must do it from the\nCLI:\n\n```bash\ngh api -X PATCH \"repos/EverMind-AI/EverOS/releases/<id>\" \\\n  -F draft=false -f tag_name=vX.Y.Z -f make_latest=true\n```\n\nRe-running the release job replaces its own draft and leaves an already-published\nrelease untouched, so a re-run is always safe.\n\n## Pre-releases\n\nPEP 440 pre-release tags publish too (PyPI accepts them; `pip install everos`\nignores them unless `--pre`): `vX.Y.ZrcN`, `vX.Y.ZaN`, `vX.Y.ZbN`. Set the same\nsuffix in `pyproject.toml` version before tagging.\n\nTheir release page is drafted as a **pre-release** and never becomes\n`/releases/latest`. A pre-release does not need its own CHANGELOG section — the\ndraft falls back to a one-line placeholder body when there is none.\n\n## One-time setup (project owner, not doable from CI)\n\n1. **PyPI trusted publisher** — PyPI → project `everos` → Settings →\n   Publishing → add: owner `EverMind-AI`, repo `EverOS`, workflow\n   `release.yml`, environment `release`.\n2. **GitHub environment** — repo Settings → Environments → create `release`\n   with required reviewers, so every publish needs a manual approval.\n\nNo PyPI API token is ever stored; the workflow mints a short-lived OIDC token\nat publish time.\n","CLAUDE.md":"# EverOS — md-first Memory Extraction Framework\n\nThis is a Python framework for md-first memory extraction (lightweight; single-user or small-team).\n\n## Quick commands\n\n```bash\nuv sync                    # install deps\nmake lint                  # ruff (check + format-check) + import-linter\nmake format                # auto-fix formatting\nmake test                  # pytest tests/unit\nmake integration           # pytest tests/integration\nmake ci                    # full CI: lint + test + integration\n```\n\n## Architecture\n\nDDD 5 layers + cross-cutting:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — cli + api (Presentation)\n- `service/` — use case orchestration (memorize / retrieve / evolve / manage)\n- `memory/` — domain (extract + search + cascade + prompt_slots + models)\n- `infra/` — storage adapters (markdown + sqlite + lancedb)\n- `component/` — injectable providers (llm / embedding / config / utils)\n- `core/` — runtime base (observability / lifespan / context)\n- `config/` — configuration data (Settings + default.toml)\n\n**Dependency rule**: `entrypoints → service → memory → infra`. Single-direction, enforced by `import-linter`.\n\nDetailed: [docs/architecture.md](docs/architecture.md).\n\n## Engineering practices\n\n- **Coding rules** auto-loaded from [.claude/rules/](.claude/rules/) (10 rules; the three always-loaded ones cover architecture / code-style / language-policy, the rest are path-scoped and load when Claude Code opens a matching file)\n- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`, `/release`\n- **Project-level decisions** in [docs/](docs/) (low-frequency, human-judgment-required)\n- **Language policy**: the project targets a global audience — docs and code are English; CJK appears only in test fixtures and locale-suffixed mirrors. Scanned by `make check-cjk`.\n- **Datetime discipline**: never call `datetime.now()` / `time.time()` directly — use `everos.component.utils.datetime`. Enforced by `make check-datetime`.\n\nContributor engineering reference — build, test, CI gates, branch & commit conventions: [docs/engineering.md](docs/engineering.md).\n\n## Branch strategy\n\n`main` is the default and protected branch. Create scoped branches from\n`main` (`feat/*`, `fix/*`, `docs/*`, `ci/*`, `chore/*`, `refactor/*`) and open\npull requests back to `main` after the required checks pass.\n\nSee [.claude/skills/new-branch/SKILL.md](.claude/skills/new-branch/SKILL.md)\nfor the full branch workflow.\n\n## GitHub sync guard\n\nWhen refreshing this repository from an internal source archive, preserve\nGitHub-only contributor and automation files. Do not overwrite `CLAUDE.md`,\n`.claude/skills/*`, `CONTRIBUTING.md`, or `.github/*` workflow/template files\nwithout checking [docs/github-sync.md](docs/github-sync.md).\n\n## Storage three-piece set\n\n```\nMarkdown (truth)  +  SQLite (state)  +  LanceDB (vector + BM25 + scalar)\n```\n\n- Memory root: `~/.everos/{agents,users,knowledge}/` (md files = single source of truth)\n- System DB: `~/.everos/.index/sqlite/system.db` (state + audit + queue + metadata)\n- Index: `~/.everos/.index/lancedb/` (rebuildable from md)\n\nSelection rationale: [docs/architecture.md](docs/architecture.md).\n\n## Source layout\n\n**src layout** (`src/everos/<...>`): standard PyPA project structure — code lives under `src/` so the working tree is not on the import path until installed, preventing accidental imports of in-development modules.\n\nAlgorithm assets (prompts, extractors) live in the separate [`everalgo`](https://github.com/EverMind-AI/EverAlgo) library, consumed here as the `everalgo-*` PyPI packages.\n\n## Where things go\n\n| Want to... | Look at |\n|---|---|\n| Understand architecture | [docs/architecture.md](docs/architecture.md) |\n| Understand storage choice | [docs/architecture.md](docs/architecture.md) (storage section) |\n| Build, test, CI & conventions | [docs/engineering.md](docs/engineering.md) |\n| Add a new module | [.claude/rules/init-py-and-reexport.md](.claude/rules/init-py-and-reexport.md) |\n| Make a commit | use `/commit` |\n| Open a branch / PR | use `/new-branch` / `/pr` |\n| Run checks before pushing | `make ci` |\n","docs/api.md":"# EverOS HTTP API (v2)\n\nHuman-readable reference for the EverOS HTTP API. Schema names, types\nand validation constraints mirror the OpenAPI spec served at\n`GET /openapi.json` (when the server runs with `ENV=DEV`; see\n[OpenAPI spec source](#openapi-spec-source)). This document adds the\nbusiness semantics the raw spec does not carry.\n\n## Table of contents\n\n- [Overview](#overview)\n  - [Base URL and versioning](#base-url-and-versioning)\n  - [Content type](#content-type)\n  - [Authentication](#authentication)\n  - [Response envelope](#response-envelope)\n  - [Eventual consistency](#eventual-consistency)\n  - [Conventions](#conventions)\n  - [ScopeId: `app_id` and `project_id`](#scopeid-app_id-and-project_id)\n- [Errors](#errors)\n- [Common types](#common-types)\n  - [MessageItem](#messageitem)\n  - [ContentItem](#contentitem)\n  - [ToolCall](#toolcall)\n  - [ToolFunction](#toolfunction)\n  - [FilterNode (filter DSL)](#filternode-filter-dsl)\n  - [SearchMethod](#searchmethod)\n  - [GetMemoryType](#getmemorytype)\n- [Endpoints](#endpoints)\n  - [POST /api/v2/memory/add](#post-apiv2memoryadd)\n  - [POST /api/v2/memory/flush](#post-apiv2memoryflush)\n  - [Asynchronous mode](#asynchronous-mode)\n  - [GET /api/v2/tasks/{task_id}](#get-apiv2taskstask_id)\n  - [GET /api/v2/tasks](#get-apiv2tasks)\n  - [POST /api/v2/memory/search](#post-apiv2memorysearch)\n  - [POST /api/v2/memory/get](#post-apiv2memoryget)\n  - [POST /api/v2/ome/trigger](#post-apiv2ometrigger)\n  - [Knowledge endpoints](#knowledge-endpoints)\n- [OpenAPI spec source](#openapi-spec-source)\n\n## Overview\n\n### Base URL and versioning\n\n| Setting | Default | Override |\n|---|---|---|\n| Host | `127.0.0.1` (loopback only) | `EVEROS_API__HOST` env var or `--host` flag |\n| Port | `8000` | `EVEROS_API__PORT` env var or `--port` flag |\n| Version prefix | `/api/v2` | — |\n\nBusiness endpoints live under `/api/v2/memory/`, `/api/v2/ome/`, and\n`/api/v2/knowledge/`. Knowledge endpoints have their own dedicated\nreference at [docs/knowledge.md](knowledge.md) and are cross-referenced\nbelow. The operational endpoints `GET /health`, `GET /metrics` and\n`POST /api/v2/cascade/quiesce` exist but are intentionally outside this\nreference — they are runtime probes and shutdown hooks for deployment, not\npart of the application contract. `quiesce` drains the md → index queue and\nstops the watcher; calling it twice is safe, and it returns a bare status body\n(`quiesced`, `drained`, `pending_before`, `pending_after`,\n`failed_permanent`), see `docs/openapi.json`. One note for anyone keying off\n`/health`: `capabilities.embed` reports whether the embedding service\nanswered a dimension probe **when the process started**, not whether it is\nreachable now -- a service that recovers later keeps reading `false` until\nthe next restart.\n\n`/api/v2` is the canonical prefix, aligned with the EverOS Cloud API. Every\nbusiness endpoint is **also** served under `/api/v1`, kept as a legacy\ncompatibility alias: the two prefixes resolve to the same handlers with\nidentical request/response contracts, so existing `/api/v1` integrations keep\nworking today. The alias carries no long-term guarantee — it may be removed in\na future major release, and it will be announced in the changelog with a\ndeprecation window before that happens. Write new integrations against\n`/api/v2`, and migrate existing ones when convenient.\n\n### Content type\n\nAll `POST` endpoints require `Content-Type: application/json`. Request\nand response bodies are UTF-8 JSON.\n\n### Authentication\n\nEverOS ships **no built-in authentication**. The server binds to\n`127.0.0.1` by default; place your own gateway or auth layer in front\nbefore exposing the API on any other interface. See\n[../SECURITY.md](../SECURITY.md) for the threat model.\n\n### Response envelope\n\nSuccessful responses from the business endpoints (`/memory/*`,\n`/knowledge/*`) wrap the payload in the envelope below (`200 OK`, or\n`201 Created` for a knowledge document upload). `POST /api/v2/ome/trigger`\nand the operational endpoints return their body bare — see each section.\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"data\": { /* endpoint-specific payload */ }\n}\n```\n\n`request_id` is generated server-side (32 lowercase hex chars) and is\nechoed by structured logs / tracing / metrics, so a single request can\nbe correlated end-to-end. Error responses carry their own envelope —\n`request_id` at the top level alongside a nested `error` object (not a\nbare FastAPI `detail`); see [Errors](#errors).\n\n### Eventual consistency\n\n`/add` and `/flush` write the markdown file (the source of truth)\n**synchronously** — when the call returns with `status: \"extracted\"`,\nthe new entry exists on disk. The configured vector / BM25 / scalar\nindex backend is rebuilt by the in-process **cascade coroutine\nasynchronously**.\n\nThat means `/search` and `/get` may not see a record immediately after\nthe `/flush` that produced it. Typical sync latency is sub-second, but\nunder load it can reach ~10–15 seconds. If you need read-your-write\nsemantics, retry with backoff. The markdown file is durable\nregardless; index lag never loses data.\n\nThe same applies one level up for everything an OME strategy produces\n**after** the extraction call returns: agent cases and skills (agent track),\nprofiles, atomic facts. `/flush` returns once the episode is on disk; the\n`extract_agent_case` strategy then runs its own LLM call, and skill\nclustering runs after that, so `/get` with `memory_type: \"agent_case\"` can\nlegitimately report `total_count: 0` for tens of seconds after a flush that\nreturned `\"extracted\"` (14 s for the case and 32 s for the skill on a\nlaptop with a hosted LLM). `GET /health`'s `cascade.pending` does **not**\ncover this window — it only tracks the md → index queue, which is empty\nwhile the strategy is still thinking. Poll `/get` with a budget instead.\nThe agent track also has extraction gates of its own, applied before any\nLLM call: a trajectory with fewer than three tool-call rounds is rejected as\ntoo simple (`AgentCaseExtractor(min_tool_call_rounds=3)`, the EverOS\ndefault — so a session with no tool use never yields a case), and so is one\nthat does not end on an assistant text message. Trajectories that pass\nthose gates can still be dropped by the extractor's own \"worth extracting\"\nLLM filter. EverOS logs the skip as `agent_case_skipped_by_algo` with the\nmemcell and session ids; the reason itself is on the adjacent everalgo log\nline, `skipping memcell (n_items=N): <reason> <detail>`. The same\n\"seconds later\" rule for every OME-produced kind is summarised in\n[how-memory-works.md](how-memory-works.md).\n\n### Conventions\n\n#### Timestamps\n\nEverOS runs a **two-zone discipline**: every stored byte is UTC; every\nrendered value carries the configured **display timezone**. The\ndisplay timezone is set by `EVEROS_MEMORY__TIMEZONE` (env var) or\n`[memory] timezone` (TOML); default `\"UTC\"`.\n\n| Direction | Format | Notes |\n|---|---|---|\n| **Request** — `messages[].timestamp` (`/add`) | Integer, **Unix epoch milliseconds (ms)** | Server auto-detects `>= 10^12` as ms for backward compat, but the contract is ms |\n| **Request** — `filters.timestamp.{gte,lt,…}` (`/search` / `/get`) | Integer, **Unix epoch ms** *or* ISO-8601 string | Same auto-detect; pick whichever you have |\n| **Response** — every `timestamp` field | ISO-8601 string with **explicit timezone offset** (`+HH:MM` or `Z`) | Always rendered in the configured display tz; never naive |\n\nWith the default `EVEROS_MEMORY__TIMEZONE=UTC` an episode produced\nat 11:30 UTC renders as `\"2026-05-28T11:30:36Z\"` (Pydantic\ncanonicalises `timezone.utc` to `Z`). Switch to\n`EVEROS_MEMORY__TIMEZONE=Asia/Shanghai` and the same UTC instant\nrenders as `\"2026-05-28T19:30:36+08:00\"` — bytes on disk are\nunchanged.\n\n**Fallback for naive input.** If you submit a `filters.timestamp` ISO\nstring without an offset (e.g. `\"2026-05-28T11:30:00\"`), the server\ntreats it as already display-tz-local before comparing against\nstorage. This is the same rule users see when reading rendered output:\n\"if you didn't say a zone, we assume your zone.\"\n\n**For internal architecture.** The storage / display split lives in\n`everos.component.utils.datetime`\n(`get_utc_now` / `ensure_utc` / `UtcDatetime` for storage;\n`get_now_with_timezone` / `to_display_tz` for display). See\n[datetime.md](datetime.md) for the design rationale.\n\n#### Other conventions\n\n- **Server-generated IDs** follow `<owner>_<kind>_<YYYYMMDD>_<NNN>`,\n  e.g. `alice_ep_20260528_00000001` for an episode, `alice_af_...`\n  for an atomic fact. See\n  [storage_layout.md §4](storage_layout.md) for the encoding.\n- **All endpoints are POST** for `/api/v2/memory/*` even when the\n  semantics look like a read (`/search`, `/get`) — the request bodies\n  are too rich (filters, methods, paging) to encode in a query string.\n\n### ScopeId: `app_id` and `project_id`\n\n`app_id` and `project_id` partition memory at the disk layer. Every\nwrite lands under\n`~/.everos/<app>/<project>/users/<user_id>/...` (or `.../agents/...`\nfor the agent track). The default scope materialises on disk as\n`default_app` / `default_project` (the `_app` / `_project` suffix is\nadded only for the literal id `\"default\"` so the default space stays\nvisually distinct from user-named scopes).\n\nA `/search` or `/get` query never crosses scopes — different\n`(app_id, project_id)` pairs are isolated.\n\nBoth fields share the same validation:\n\n| Setting | Value |\n|---|---|\n| Type | `string` |\n| Default | `\"default\"` |\n| Length | 1–128 chars |\n| Charset | `^[a-zA-Z0-9_.-]+$` |\n| Rejected literals | `\".\"` and `\"..\"` (path-traversal guard) |\n\n## Errors\n\nEvery non-2xx response uses a uniform error envelope — `request_id` at\nthe top level (mirroring the success envelope) alongside a nested\n`error` object. It is **not** a bare FastAPI `detail`:\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Document 'abc123' not found\",\n    \"timestamp\": \"2026-06-01T12:24:46+00:00\",\n    \"path\": \"/api/v2/knowledge/documents/abc123\"\n  }\n}\n```\n\n### error.code values\n\n`error.code` is a machine-readable `ErrorCode` enum. Clients can switch\non this value to decide retry / display / routing behaviour without\nparsing the human-readable `message` field.\n\n| `error.code` | HTTP | Retryable? | When |\n|---|---|---|---|\n| `NOT_FOUND` | `404` | No | Requested resource does not exist |\n| `CONFLICT` | `409` | No | Operation conflicts with existing state (e.g. duplicate document) |\n| `INVALID_INPUT` | `422` | No | Request-body validation failure. Also covers `/search` / `/get` filter-DSL compile errors — the compile reason rides in `message` — and, on `/add` and `/flush`, a session whose buffered messages the model could not segment on three consecutive calls: the buffer is moved to the `quarantine` track of `unprocessed_buffer` so the session can continue, and `message` names the session and the number of messages set aside |\n| `EXTRACTION_EMPTY` | `422` | No | Knowledge document parsed but the extractor produced no topics. An empty or whitespace-only upload is rejected earlier as `INVALID_INPUT` (`\"Uploaded file has no valid content.\"`) |\n| `BAD_REQUEST` | `400` | No | Path traversal attempt or other malformed input |\n| `UNSUPPORTED_FORMAT` | `415` | No | File format or modality not supported (e.g. unsupported `ContentItem` type, missing `ext` for `base64`) |\n| `PAYLOAD_TOO_LARGE` | `413` | No | An `async_mode` request body exceeds `memorize.async_queue.max_payload_bytes`; the batch was not accepted |\n| `QUEUE_SATURATED` | `429` | Yes | The memorize queue is at its per-session or global cap; the batch was not accepted — retry later or keep it client-side |\n| `EXTERNAL_SERVICE_UNAVAILABLE` | `503` | **Yes** | An external service (LLM, embedding, rerank) returned an error or timed out |\n| `CAPABILITY_UNAVAILABLE` | `503` | No | A required server-side capability is missing (e.g. `everos[multimodal]` extra not installed, LibreOffice absent) — requires admin action, not retry |\n| `CONFIGURATION_ERROR` | `500` | No | A required configuration is missing or invalid (e.g. embedding model not set) |\n| `INTERNAL_ERROR` | `500` | No | Unhandled exception (internal details are logged, never leaked) |\n\n### error object\n\n| Field | Type | Description |\n|---|---|---|\n| `code` | `string` | One of the `ErrorCode` values listed above |\n| `message` | `string` | Human-readable reason. For `INVALID_INPUT` from request validation, **only the first** validation error is surfaced, formatted `\"<msg>: <dotted-loc>\"` with the leading `body` segment stripped (e.g. `\"Field required: messages\"`); a model-level validator with no field location surfaces just `\"<msg>\"` (e.g. `\"Value error, exactly one of user_id / agent_id must be provided\"`) |\n| `timestamp` | `string` | ISO-8601 with timezone offset (display tz) |\n| `path` | `string` | Request path, e.g. `/api/v2/memory/add` |\n\n> Unlike FastAPI's default, the full per-field validation array is **not**\n> returned — only the first error's message. A client that needs the\n> offending field can read the `<loc>` suffix in `message`.\n\n## Common types\n\n### MessageItem\n\nOne turn in a `/add` batch. Shape mirrors the OpenAI Chat Completions\nmessage structure plus a stable `sender_id` for indexing.\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `sender_id` | `string` | yes | — | `minLength=1` |\n| `sender_name` | `string \\| null` | no | `null` | — |\n| `role` | `\"user\" \\| \"assistant\" \\| \"tool\"` | yes | — | — |\n| `timestamp` | `integer` | yes | — | `> 0` — **Unix epoch milliseconds (ms)** per v1 contract |\n| `content` | `string \\| array<ContentItem>` | yes | — | — |\n| `tool_calls` | `array<ToolCall> \\| null` | no | `null` | — |\n| `tool_call_id` | `string \\| null` | no | `null` | — |\n\n**`sender_id`** — Stable identifier of the entity producing this turn.\nFor `role: \"user\"`, this is the `user_id` the server will index the\nextracted memory under: markdown lands at\n`users/<sender_id>/episodes/...`, and `/search` / `/get` queries with\n`user_id: \"<sender_id>\"` reach it. For `role: \"assistant\"` or\n`\"tool\"`, the field is informational (the LLM sees it during\nextraction but it is not used as an indexing key).\n\n**`sender_name`** — Optional human-readable display name. Lets the LLM\nuse a real name during extraction without changing the indexing key.\nFor example, set `sender_id: \"u_42\"` (stable internal id) and\n`sender_name: \"Alice\"` (what the LLM sees).\n\n**`role`** — Speaker role; one of:\n- `\"user\"` — content originates from a human (or human-acting client).\n- `\"assistant\"` — content from the AI assistant.\n- `\"tool\"` — output of a tool call; pair with `tool_call_id`.\n\n**`timestamp`** — Wall-clock anchor in **Unix epoch milliseconds (ms)**\nper the v1 API contract. Used by extraction to anchor the resulting\nepisode and by the daily-log writer to bucket the entry into the right\nfile (`episode-<YYYY-MM-DD>.md` etc.).\n\n> Implementation note: the algo layer auto-detects seconds vs ms (values\n> `>= 10^12` are treated as ms, smaller as seconds) for backward compat,\n> but **clients should send ms** to honour the contract.\n\n**`content`** — The message body.\n- A bare **string** is shorthand for a single text content item.\n- An **array of `ContentItem`** is for mixed-modality input (text +\n  image / pdf / audio / ...); non-text items are parsed by the\n  multimodal LLM configured via `EVEROS_MULTIMODAL__*` env vars. See\n  [ContentItem](#contentitem).\n\n**`tool_calls`** — When `role: \"assistant\"`, the tool calls the\nassistant emitted in this turn (OpenAI Chat Completions shape).\n\n**`tool_call_id`** — When `role: \"tool\"`, the `id` of the call this\nmessage is the response to. Required for `role: \"tool\"`; a tool row\nwithout it is rejected with `422`.\n\n### ContentItem\n\nMixed-modality message-body element. Carry the payload in exactly one\nof `text` / `uri` / `base64`; the others must be `null`. For\n`type: \"text\"` use `text`; for every **non-text** type use `uri`\n(`http(s)://`) or `base64` (with `ext`). Non-text items are routed\nthrough the multimodal parser, which needs a fetchable or decodable\npayload — a non-text item carrying only `text` returns `415`.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `type` | `\"text\" \\| \"image\" \\| \"audio\" \\| \"doc\" \\| \"pdf\" \\| \"html\" \\| \"email\"` | yes | — | — |\n| `text` | `string \\| null` | no | `null` | Required when `type: \"text\"` |\n| `uri` | `string \\| null` | no | `null` | `http(s)://` (fetched server-side) or `file://` (read from the server's local fs, guardrailed) pointer |\n| `base64` | `string \\| null` | no | `null` | Inline payload, plain base64 (no `data:` prefix) |\n| `ext` | `string \\| null` | no | `null` | File-extension hint when `uri` lacks one |\n| `name` | `string \\| null` | no | `null` | Display filename, used in logs |\n| `extras` | `object \\| null` | no | `null` | Provider-specific metadata, opaque to EverOS |\n\n**`type`** — The content kind. Each non-text type is dispatched to the\nmultimodal LLM. If the multimodal endpoint cannot handle the supplied\npayload, `/add` returns `415 Unsupported Media Type`.\n\n**`text`** — The literal text payload; valid **only** for\n`type: \"text\"`. A non-text type (including `\"html\"`) is always routed\nto the parser and must carry `uri` or `base64`; passing only `text` on\na non-text item returns `415`. To inline HTML as plain text, send it\nas `type: \"text\"`.\n\n**`uri`** — `http(s)://` or `file://` pointer to the asset. An\n`http(s)` uri is fetched by the server and dispatched by the response\nContent-Type (use it for assets hosted elsewhere — S3 / OSS presigned\nURL, http server). A `file://` uri is read from the **server's** local\nfilesystem (the path must be reachable by the server process), subject\nto these guardrails:\n\n- the resolved path (symlinks followed) must be an existing regular file;\n- its size must be ≤ `EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES` (default 50 MiB);\n- when `EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS` is set, the path must lie\n  within one of the listed roots (unset = any readable file, the\n  local-first default).\n\nA guardrail violation surfaces as `415`. Mutually exclusive with `base64`.\n\n**`base64`** — Inline binary payload, base64-encoded (no `data:`\nprefix). Mutually exclusive with `uri`.\n\n> **Size caution.** base64 inflates the payload ~4/3× on the wire, and\n> the encoded blob is held **verbatim in the server's staging buffer\n> (SQLite) from `/add` until the session is flushed** — a multi-MB PDF\n> becomes multi-MB of SQLite text for the buffer's lifetime. It is *not*\n> persisted past extraction (the memory cell and episode store only the\n> parsed text, never the raw bytes), but the transient footprint is\n> real, and a large inline blob also slows request parsing. **Prefer\n> `uri` (`http(s)://`) for large assets**: the buffer then holds only\n> the URL plus the parsed text, and the bytes are fetched transiently at\n> parse time rather than stored. Reserve `base64` for small assets or\n> when no reachable URL exists.\n\n**`ext`** — File-extension hint (`\"pdf\"`, `\"png\"`, `\"html\"`, ...). For\n`base64` payloads this drives modality dispatch and is effectively\n**required** (without it the server falls back to `mime`, then `415`s\nif neither resolves). For `uri` payloads it is optional — the fetched\nContent-Type usually suffices.\n\n**`name`** — Filename / human label for logging and traceability.\nDoes not affect parsing.\n\n**`extras`** — Free-form bag of provider-specific metadata (e.g.\ncaption, page hints). Opaque to EverOS; passed through to the\nmultimodal LLM context.\n\n### ToolCall\n\nOpenAI-shaped tool invocation attached to an assistant turn.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `id` | `string` | yes | — | Stable id; echoed by the paired `role: \"tool\"` turn via `tool_call_id` |\n| `type` | `string` | no | `\"function\"` | Only `\"function\"` is meaningful today |\n| `function` | [ToolFunction](#toolfunction) | yes | — | The function being called |\n\n### ToolFunction\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `name` | `string` | yes | Function name as registered with the agent |\n| `arguments` | `string` | yes | **JSON-encoded string** (OpenAI convention — not an object) |\n\n### FilterNode (filter DSL)\n\nA recursive boolean tree of predicates. Used by `/search.filters` and\n`/get.filters`. The Pydantic envelope only checks the recursive\ncombinator shape; field-level validity (which scalar fields are\nfilterable, which operators apply, value coercion) runs when the\nnode is compiled to a backend-specific filter clause server-side. Compile\nerrors surface as `422` with the offending field / operator in\n`error.message`.\n\n#### Shape\n\nA node is a JSON object whose keys are one of:\n\n| Key | Value | Notes |\n|---|---|---|\n| `AND` | `array<FilterNode>` | All child nodes must match. Omit if not needed |\n| `OR` | `array<FilterNode>` | At least one child node must match. Omit if not needed |\n| *<allowed field>* | scalar or operator map | Predicate on that field — see [Allowed fields](#allowed-fields) and [Operators](#operators) |\n\n`AND`, `OR`, and scalar predicates **mix freely at the same level**;\nthey are implicitly joined with `AND`. A node with only scalar keys is\nfine — `AND` / `OR` arrays are not required.\n\nExample — `session_id == \"demo-001\"` AND (`sender_id` contains `\"alice\"` OR `\"bob\"`):\n\n```json\n{\n  \"session_id\": \"demo-001\",\n  \"OR\": [\n    { \"sender_id\": \"alice\" },\n    { \"sender_id\": \"bob\" }\n  ]\n}\n```\n\n<a id=\"filter-allowed-fields\"></a>\n\n#### Allowed fields\n\nThese are the only field names that may appear as a predicate key.\nAnything else returns `422`.\n\n| Field | Column kind | Value type | Notes |\n|---|---|---|---|\n| `session_id` | string | `string` | Session the row was extracted from |\n| `parent_type` | string | `string` | Parent kind, e.g. `\"memcell\"` |\n| `parent_id` | string | `string` | Parent id |\n| `timestamp` | timestamp | `integer` (Unix epoch **ms** per v1 contract) or `string` (ISO-8601) | Implementation auto-detects `>= 10^12` as ms / smaller as sec for backward compat, but the contract is ms |\n| `sender_id` | array of strings | `string` | Matches via `array_has(sender_ids, <value>)` — checks whether the row's `sender_ids` list contains the given id |\n\nThe following names are **reserved** and rejected inside `filters`.\nThey must be set at the top of the request (`/search` or `/get`),\nnot inside the DSL:\n\n```\nowner_id   owner_type   app_id   project_id\n```\n\n<a id=\"filter-operators\"></a>\n\n#### Operators\n\nA predicate value may be a **scalar shorthand** (equality) or an\n**operator map** (multiple operators in one dict are AND-joined):\n\n| Operator | SQL | Applies to |\n|---|---|---|\n| `eq` | `=` | string, timestamp; on `sender_id` becomes `array_has(...)` |\n| `ne` | `!=` | string, timestamp |\n| `gt` | `>` | timestamp |\n| `gte` | `>=` | timestamp |\n| `lt` | `<` | timestamp |\n| `lte` | `<=` | timestamp |\n| `in` | `IN (...)` | string (list of values); on `sender_id` becomes `OR` of `array_has` |\n\nExamples:\n\n```json\n// Equality shorthand\n{ \"session_id\": \"demo-001\" }\n\n// Operator map (operators inside one dict are AND-joined)\n{ \"timestamp\": { \"gte\": 1748390400000, \"lt\": 1748400000000 } }\n\n// IN list\n{ \"session_id\": { \"in\": [\"demo-001\", \"demo-002\"] } }\n\n// Array-membership on sender_ids\n{ \"sender_id\": \"alice\" }                       // array_has(sender_ids, 'alice')\n{ \"sender_id\": { \"in\": [\"alice\", \"bob\"] } }    // OR of two array_has\n```\n\n### SearchMethod\n\n| Value | Behaviour |\n|---|---|\n| `\"keyword\"` | BM25 only — pure lexical match, no embedding cost |\n| `\"vector\"` | Dense vector ANN only — semantic recall, no lexical |\n| `\"hybrid\"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter against the configured derived index backend |\n| `\"agentic\"` | Iterative cluster-path retrieval driven by a cross-encoder rerank loop; higher quality at higher latency / cost |\n\n`\"hybrid\"` is the default because it balances recall and precision\nwithout requiring the agentic loop. `\"agentic\"` calls the LLM in a loop\nand should be reserved for offline or background workflows.\n\n### GetMemoryType\n\n| Value | Track | Returned in `data.<plural>` |\n|---|---|---|\n| `\"episode\"` | user | `data.episodes` — [GetEpisodeItem](#getepisodeitem) |\n| `\"profile\"` | user | `data.profiles` — [GetProfileItem](#getprofileitem) |\n| `\"agent_case\"` | agent | `data.agent_cases` — [GetAgentCaseItem](#getagentcaseitem) |\n| `\"agent_skill\"` | agent | `data.agent_skills` — [GetAgentSkillItem](#getagentskillitem) |\n\n`memory_type` must match the requested owner kind: `\"episode\"` /\n`\"profile\"` require `user_id`; `\"agent_case\"` / `\"agent_skill\"`\nrequire `agent_id`. The mismatching combinations are rejected with\n`422`.\n\n## Endpoints\n\n### POST /api/v2/memory/add\n\nAppend a batch of messages to a session buffer. The server\naccumulates messages until the boundary detector decides the session\nis complete (or you force it with `/flush`); on boundary the LLM\nextracts a memory cell and the writers persist markdown + index rows.\n\nThe response distinguishes \"buffered\" from \"extracted this call\" via\nthe `status` field — see [Response body](#response-body) below.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `messages` | `array<MessageItem>` | yes | — | 1–500 items |\n| `defer_extraction` | `boolean` | no | `false` | buffer durably without boundary or extraction LLM calls |\n| `async_mode` | `boolean` | no | `false` | queue the batch and return `202` immediately; see [Asynchronous mode](#asynchronous-mode) |\n\n**`session_id`** — Identifies the conversation buffer on the server.\nMessages POSTed with the same `(session_id, app_id, project_id)`\naccumulate into one extraction batch. Different `session_id`s never\nshare a buffer, even within the same scope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n**`messages`** — Ordered list of [MessageItem](#messageitem) to\nappend. Order matters (it is preserved when the buffer is later\nextracted). The 1–500 cap is a per-request safety bound, not a\nsession lifetime cap — you can call `/add` many times for the same\n`session_id`.\n\n**`defer_extraction`** — When `true`, `/add` only merges the messages\ninto the durable SQLite `unprocessed_buffer`. It skips boundary\ndetection, memcell creation, and all extraction pipelines, so batching\nclients can capture every turn cheaply and call `/flush` after an idle\nwindow, a size threshold, or a session switch. A successful deferred\nadd always reports `status: \"accumulated\"`.\n\n**`async_mode`** — When `true`, `/add` persists the batch to the durable\nmemorize queue and returns `202 Accepted` without running boundary\ndetection or extraction; a background worker delivers queued batches in\narrival order per session, several sessions at a time. See\n[Asynchronous mode](#asynchronous-mode) for the `202` body, the ordering\nrule and how to poll progress.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `status` | `\"accumulated\" \\| \"extracted\"` | What the server did with the batch |\n\n**`message_count`** — Always equal to `len(messages)` on a `200` —\nthere is no partial accept. The field is present mainly for log\ncorrelation.\n\n**`status`** — Two-state outcome:\n- `\"accumulated\"` — Messages were added to the buffer; the boundary\n  detector did **not** trigger this call. The buffer remains open.\n- `\"extracted\"` — During this call the boundary detector tripped; the\n  LLM ran extraction and the writers persisted at least one memory\n  cell. The buffer is empty after the call returns.\n\n`202 Accepted` — only with `async_mode`, or when the session already has\nqueued batches (see [Asynchronous mode](#asynchronous-mode)) — returns a\nSuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"queued\"` | The batch is persisted; delivery happens in the background |\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `pending` | `integer` | Unfinished queued batches of this session, this one included |\n\nThe envelope's `request_id` is the handle for\n[`GET /tasks/{task_id}`](#get-apiv2taskstask_id).\n\n#### cURL example\n\n```bash\nTS=$(( $(date +%s) * 1000 ))\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/add \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"session_id\\\": \\\"demo-002\\\",\n    \\\"app_id\\\": \\\"default\\\",\n    \\\"project_id\\\": \\\"default\\\",\n    \\\"messages\\\": [\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $TS, \\\"content\\\": \\\"I love climbing in Yosemite every spring.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+10)), \\\"content\\\": \\\"My favorite coffee shop is Blue Bottle in SOMA.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+20)), \\\"content\\\": \\\"I bike to work most days.\\\"}\n    ]\n  }\"\n```\n\nResponse (real capture):\n\n```json\n{\n    \"request_id\": \"ae78d3f689c941eea135893e702fd171\",\n    \"data\": {\n        \"message_count\": 3,\n        \"status\": \"accumulated\"\n    }\n}\n```\n\n### POST /api/v2/memory/flush\n\nForce the boundary detector to decide **now** for the given session\nbuffer. The LLM runs extraction (one call) regardless of whether the\nheuristic would have tripped on its own. Useful at the end of a chat\nor agent run to make sure pending context becomes durable memory.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n\n**`session_id`** — Identifies which buffer to flush. Must match the\n`session_id` of prior `/add` calls in the same `(app_id, project_id)`\nscope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"extracted\" \\| \"no_extraction\"` | What happened |\n\n**`status`**:\n- `\"extracted\"` — The buffer had at least one message; extraction ran\n  and produced at least one memory cell.\n- `\"no_extraction\"` — The buffer was empty (no prior `/add` calls for\n  this `(session_id, app_id, project_id)`, or it was already flushed).\n\n`202 Accepted` with the same body as `/add` (`message_count` is `0`) only\nwhen the session already has queued batches, so the flush runs after them;\n`/flush` has no `async_mode` of its own. See\n[Asynchronous mode](#asynchronous-mode).\n\nA `200` from `/flush` is synchronous with respect to markdown persistence: by the\ntime the response returns, the new entry is on disk. Derived index\nsync is still asynchronous — see\n[Eventual consistency](#eventual-consistency).\n\n#### cURL example\n\n```bash\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/flush \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"session_id\":\"demo-002\",\"app_id\":\"default\",\"project_id\":\"default\"}'\n```\n\nResponse (real capture; this call took several seconds because of the\nextraction LLM call):\n\n```json\n{\n    \"request_id\": \"e65bcf4f56c042e39cdf50866810672c\",\n    \"data\": {\n        \"status\": \"extracted\"\n    }\n}\n```\n\n### Asynchronous mode\n\n`async_mode: true` on `/add` turns the call into \"accept and return\": the\nrequest body is written to the SQLite `memorize_inbox` table in one\ntransaction and the response is `202 Accepted`. `/flush` has no\n`async_mode` (the Cloud API has no asynchronous flush either): it runs\ninline, except that a flush on a session with queued batches joins the\nqueue behind them and answers `202` (see Ordering). An in-process\nworker then delivers queued batches to the same code path a synchronous\ncall uses: each session's batches in the order they were accepted, several\nsessions at a time. The field name, the `202` body\n(`status: \"queued\"` plus the envelope's `request_id`) and the status\nvocabulary (`queued` / `processing` / `success` / `failed`) match the\nEverOS Cloud gateway, so one client can talk to both.\n\n**Ordering.** Batches of one session are delivered strictly in arrival\norder, and a `/flush` runs after every `/add` accepted before it. To keep\nthat true, a synchronous call (`async_mode` omitted or `false`) on a\nsession that still has queued batches is queued too and answered with\n`202` — otherwise it would overtake work the client was already told is\naccepted. Clients that never use `async_mode` never see this: their\nsessions never have queued batches. Sessions are independent of each\nother: up to `max_concurrency` batches (default 4) run at once, never two\nfrom one session, so one slow session does not hold up the others, and\nthere is no ordering between sessions. Like the Cloud consumer, which runs\ngroups in parallel and each group in order.\n\n**Backpressure.** The queue is bounded. A batch is rejected before it is\nstored when:\n\n| Condition | HTTP | `error.code` | Default limit |\n|---|---|---|---|\n| Serialised body too large | `413` | `PAYLOAD_TOO_LARGE` | 4 MiB |\n| Session has too many unfinished batches | `429` | `QUEUE_SATURATED` | 50 |\n| Process has too many unfinished batches | `429` | `QUEUE_SATURATED` | 200 |\n\nThe caps are checked before the insert without a lock, so a burst of\nconcurrent requests can overshoot them by a few rows. A rejected batch was\n**not** accepted: retry later or keep it client-side.\n`pending` in every `202` body lets a client slow down before hitting `429`.\nLimits are configured under `[memorize.async_queue]`, see\n[configuration.md](configuration.md#memorizeasync_queue).\n\n**Failure.** A batch whose delivery raises is marked `failed` and skipped\nso its session keeps moving; `GET /tasks/{request_id}` shows `error`.\nThere is no automatic retry by default (`max_attempts = 1`), because the\nmemorize pipeline is not atomic: boundary detection writes the memcell and\nconsumes the session buffer before extraction runs. A failure before that\npoint (the common one: the boundary LLM call failed) leaves nothing behind,\nand re-sending the batch is safe. A failure after it leaves the memcell\nwithout its episode; re-sending then cuts a second memcell, and a `/flush`\nfinds an empty buffer. `last_error` does not say which step failed. The\nserver never re-drives a failed batch and deletes its row with the others\nafter seven days. Synchronous calls have the same property; they just hand\nthe decision to the client as a 5xx.\n\n**Delivery is at-least-once across a crash.** A row that was in flight\nwhen the process died is delivered again on the next start. If the first\ndelivery had already written its memcell, an `/add` replay duplicates that\nmemory; a `/flush` replay finds an empty buffer and does nothing, so a crash\nthat fell between the memcell write and the episode write leaves that\nmemcell without its episode. A\nnormal shutdown waits up to 30 seconds for the rows in flight (up to\n`max_concurrency` of them, one per session); a row that takes longer, or a\nprocess manager that kills earlier (`docker stop` sends SIGKILL after 10\nseconds by default), is replayed the same way, so one restart can replay\nup to `max_concurrency` batches. So is a\nbatch whose delivery had just failed when the process died before its\n`failed` mark was written, and a batch whose status write was lost (see\nStuck rows) if the server restarts before the row is marked. Replays are\nbounded: a batch that has been replayed `max_replays` times (default 3)\nwithout ever finishing is marked `failed` with `error_code`\n`replays_exhausted` instead of being requeued again, so a batch that kills\nthe process cannot crash-loop it.\n\n**Operations.** `GET /health` carries a `memorize_queue` block: `healthy`\n(the delivery loop is alive), `pending`, `oldest_pending_age_s`, `failed`,\n`loop_restarts` and `last_loop_error_at`. The probe only observes: a loop\nthat dies relaunches itself (with backoff) and is relaunched by the next\n`/add`, and `loop_restarts` counts those. Alert on `healthy` and on\n`loop_restarts` changing; a growing `oldest_pending_age_s` with a live loop\nmeans delivery is slower than arrival.\n\n**Retention.** Every queue row, whatever its status, is deleted seven days\n(`row_retention_hours`) after it was created. `GET /tasks/{task_id}` returns `404`\nafter that. A delivered row drops its request body at once. A `failed` row\nkeeps its body (in `memorize_inbox.payload_json`, found by `request_id`)\nuntil purged, and failed rows are not capped: an outage in which every batch\nfails keeps every body accepted during it for the retention period. SQLite\nreuses the freed pages but does not shrink `system.db`. A row that ages out\nwhile still `queued` was never delivered; the purge logs a warning when that\nhappens.\n\n**Stuck rows.** A row left `processing` because its status write was lost\n(SQLite busy past its timeout, disk full) is marked `failed` before the\nworker claims its next batch, or at its next scheduling pass if that write\nfails too, so a session's `pending` does not stay non-zero because of it;\nbatches the worker is still running are never swept. Its `last_error`\nreads `status write lost; outcome unknown`:\nthe batch may well have been delivered in full (the lost write is usually\nthe `success` one), so check the session's memories before re-sending it.\nIf the server restarts before that mark lands, the row is replayed like a\ncrash (see above).\n\nSetting `[memorize.async_queue] enabled = false` closes the door:\n`async_mode` is accepted but ignored and new calls run synchronously unless\ntheir session still has queued rows. Rows accepted while it was on are still\ndelivered by the worker, and a session that still has such rows still queues\nits synchronous calls behind them, so no drain is needed before switching.\nDowngrading to a release without `async_mode` is different: queued rows are\nneither delivered nor deleted there. Set `enabled = false` and restart first,\nso `async_mode` requests stop adding rows (a synchronous call still queues\nbehind its session's backlog until it drains); wait until\n`select count(*) from memorize_inbox where status in ('queued', 'processing')`\non `system.db` is 0; then stop and downgrade.\n\n### GET /api/v2/tasks/{task_id}\n\nThe outcome of one accepted request. The path and the body are the EverOS\nCloud gateway's (`GET /api/v2/tasks/{task_id}`), so a client polls both the\nsame way; `task_id` is the `request_id` a `202` carried.\n\n`200 OK`, or `404 NOT_FOUND` when the row never existed or has aged out of\n`row_retention_hours`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | The request id |\n| `status` | `\"queued\" \\| \"processing\" \\| \"success\" \\| \"failed\"` | Where the batch is. Cloud can also report `pending`; this build never does |\n| `task_type` | `\"memory_add\" \\| \"memory_flush\"` | — |\n| `error` | `string \\| null` | Summary of the failure, if any |\n| `error_code` | `string \\| null` | `status_write_lost` for a row the sweep failed, `replays_exhausted` for one replayed `max_replays` times without finishing, otherwise the exception type that failed the batch |\n| `created_at` | `string` (ISO 8601) | Accepted at |\n| `finished_at` | `string \\| null` | Set once `status` is `success` or `failed` |\n\n### GET /api/v2/tasks\n\nAccepted requests, newest first, one page at a time. Mirrors the Cloud\ngateway's listing.\n\n#### Query parameters\n\n| Parameter | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `session_id` | `string` | no | — | Only this session's requests |\n| `app_id` / `project_id` | `string` | no | `\"default\"` | Scope for `session_id` |\n| `status` | `string` | no | — | One of the four statuses; anything else is `422 INVALID_INPUT` |\n| `page` | `integer` | no | `1` | `≥ 1` |\n| `page_size` | `integer` | no | `20` | `1..100`, larger values are capped |\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `total` | `integer` | Rows matching the filters, all pages |\n| `page` / `page_size` | `integer` | As requested (`page_size` after capping) |\n| `items` | `array<TaskItem>` | The page, newest first; fields as in `GET /tasks/{task_id}` |\n\nDrain a session before exiting by polling `?session_id=X&status=queued` and\n`?session_id=X&status=processing` until both `total` are `0`. That means\nevery batch reached the memorize pipeline (unless rows aged out before\ndelivery, see Retention); it does not mean every message is an episode yet:\nmessages still in the session buffer wait for the boundary detector or the\nnext `/flush`.\n\n### POST /api/v2/memory/search\n\nHybrid retrieval over the memory store. Combines BM25, dense vector\nANN, optional scalar filtering, optional cross-encoder rerank, and\noptional final LLM rerank. Returns ranked items grouped by kind.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `user_id` | `string \\| null` | XOR with `agent_id` | `null` | `minLength=1` if set |\n| `agent_id` | `string \\| null` | XOR with `user_id` | `null` | `minLength=1` if set |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `query` | `string` | yes | — | `minLength=1` |\n| `method` | [SearchMethod](#searchmethod) | no | `\"hybrid\"` | — |\n| `top_k` | `integer` | no | `-1` | `-1` or `1..100` |\n| `radius` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `min_score` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `include_profile` | `boolean` | no | `false` | — |\n| `enable_llm_rerank` | `boolean` | no | `false` | — |\n| `filters` | [FilterNode](#filternode-filter-dsl) `\\| null` | no | `null` | — |\n\n**`user_id` / `agent_id`** — **Exactly one** must be set. Determines\nwhich track is searched: `user_id` → user-memory (episodes /\nprofiles); `agent_id` → agent-memory (cases / skills).\n\n**`app_id` / `project_id`** — Scope identifiers; results never cross\nscopes.\n\n**`query`** — The retrieval query. The same string is fed to BM25\ntokenization and to the embedding model, then the two recall lists\nare fused.\n\n**`method`** — Retrieval method; see [SearchMethod](#searchmethod).\nDefault `\"hybrid\"` is the recommended starting point.\n\n**`top_k`** — Maximum number of items to return per kind. Two modes:\n- `-1` (default) — \"Use server defaults.\" Recall falls back to a\n  fixed internal cap, and a server-side `radius` default kicks in if\n  the caller did not pass one (see `radius` below). Use for \"give me\n  whatever you find worth returning.\"\n- `1..100` — Explicit cap. Recall is sized as `top_k × multiplier`\n  and **no** default `radius` is applied unless you set it yourself.\n\nValues of `0` or outside `-1` / `1..100` are rejected with `422`.\n\n**`radius`** — Optional **cosine-similarity threshold** in `[0.0, 1.0]`.\nCandidates whose score is below this value are dropped — use it to cut\na long tail of weak matches. Three-level fallback for the effective\nradius:\n\n1. Caller-supplied `radius` (including the literal `0.0`) always wins.\n2. With `top_k=-1` and no caller-supplied `radius`, a server-side\n   default radius kicks in.\n3. With `top_k>0` and no caller-supplied `radius`, no threshold is\n   applied (`null`).\n\n**`min_score`** — Optional **post-fusion relevance floor** in\n`[0.0, 1.0]`. Results below this score are evicted after fusion,\nindependent of `radius` (which is a per-recall cosine threshold).\n\n**`include_profile`** — When `user_id` is set, also fetch the user's\nprofile and include it in `data.profiles`. The profile is not\nranked; `score` is `null`. Ignored when `agent_id` is set.\n\n**`enable_llm_rerank`** — Opt-in LLM rerank pass for\n`method: \"hybrid\"`. Applies to `agent_case` and `agent_skill` fusion\nonly; the episode hybrid path has built-in fact eviction and\nignores this flag. Adds one LLM call per request. Ignored by\n`keyword` / `vector` (no fusion to rerank) and `agentic` (uses its\nown cross-encoder loop).\n\n**`filters`** — Optional filter-DSL node; see\n[FilterNode](#filternode-filter-dsl). Applied **before** ranking, so\nit does not perturb the ranker.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All five\narrays are always present so client code can iterate without\nbranching on owner type; arrays that do not apply to the requested\nowner kind stay as `[]`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set |\n| `profiles` | `array<SearchProfileItem>` | Populated when `user_id` is set **and** `include_profile=true` |\n| `agent_cases` | `array<SearchAgentCaseItem>` | Populated when `agent_id` is set |\n| `agent_skills` | `array<SearchAgentSkillItem>` | Populated when `agent_id` is set |\n| `unprocessed_messages` | `array<UnprocessedMessageDTO>` | Populated **only** when `filters.session_id` is a top-level eq scalar; otherwise stays `[]`. Independent of `user_id` / `agent_id` (buffer rows have no owner attribution — boundary detection runs before owner inference) |\n\n#### SearchEpisodeItem\n\nUser-track conversation episode hit. `score` is the fused retrieval\nscore; `atomic_facts` lists single-sentence facts that matched the\nquery within this episode (already nested, no separate call needed).\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_ep_<YYYYMMDD>_<NNN>` |\n| `user_id` | `string \\| null` | Owner of this episode |\n| `app_id` | `string` | Scope where the episode lives |\n| `project_id` | `string` | Scope where the episode lives |\n| `session_id` | `string` | The `session_id` whose messages produced this episode |\n| `timestamp` | `string` | ISO-8601 with timezone offset; default `UTC` renders as `Z`, non-UTC defaults render as `±HH:MM` — see [Conventions](#conventions) |\n| `sender_ids` | `array<string>` | Distinct `sender_id`s in the underlying messages |\n| `summary` | `string` | Short summary (~200 chars), suitable for list-view rendering |\n| `subject` | `string` | One-line subject / title |\n| `episode` | `string` | Full extracted narrative |\n| `type` | `\"Conversation\"` | Reserved; today only conversation-derived episodes ship |\n| `score` | `number` | Fused retrieval score for this episode |\n| `atomic_facts` | `array<SearchAtomicFactItem>` | Sub-facts extracted from the same episode that matched the query |\n\n> **`id` is unique within `(app_id, project_id)`.** The same `id` can appear\n> under two projects (each project keeps its own daily files and sequence);\n> address a row globally by `(app_id, project_id, id)`. Within one response\n> ids never repeat, because a request is always scoped to one project.\n\n#### SearchAtomicFactItem\n\nA single-sentence fact pulled out of an episode during extraction.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_af_<YYYYMMDD>_<NNN>` |\n| `content` | `string` | The fact as a single sentence |\n| `score` | `number` | Same score scale as the parent episode |\n\n#### SearchProfileItem\n\nUser profile. Only populated when `include_profile=true` and\n`user_id` is set.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Profile id |\n| `user_id` | `string \\| null` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `profile_data` | `object` | Free-form structured profile fields produced by extraction (schema is profile-specific) |\n| `score` | `number \\| null` | `null` for direct fetches (no query-aware profile ranking yet) |\n\n#### SearchAgentCaseItem\n\nAgent-track case hit. Returned only when the request uses `agent_id`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_ac_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `session_id` | `string` | The agent run this case came from |\n| `task_intent` | `string` | What the agent was trying to do (one-sentence framing) |\n| `approach` | `string` | How the agent went about it |\n| `quality_score` | `number` | Quality assessed at extraction time, in `[0, 1]` |\n| `key_insight` | `string \\| null` | One-line takeaway; `null` if extraction did not produce one |\n| `timestamp` | `string` | ISO-8601 UTC of when the run happened |\n| `score` | `number` | Fused retrieval score for this case |\n\n#### SearchAgentSkillItem\n\nAgent-track skill hit — a named procedural memory produced by\nclustering related cases.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_sk_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `name` | `string` | Skill name (a stable, human-meaningful slug) |\n| `description` | `string` | One-line description |\n| `content` | `string` | Full skill body (procedure / playbook text) |\n| `confidence` | `number` | Extraction confidence in `[0, 1]` |\n| `maturity_score` | `number` | Maturity assessed at clustering time, in `[0, 1]` |\n| `source_case_ids` | `array<string>` | `agent_case` ids that contributed to this skill |\n| `score` | `number` | Fused retrieval score for this skill |\n\n#### UnprocessedMessageDTO\n\nA raw message still sitting in the boundary-detection buffer — sent to\n`/add` but not yet carved into an episode / case. Returned **only**\nwhen the request's `filters` contains `session_id` as a top-level eq\nscalar (`{\"session_id\": \"<sid>\"}`); compound shapes (`AND` / `OR`\ncombinators, operator maps such as `{\"eq\": ...}` / `{\"in\": ...}`) do\nnot trigger the lookup because there is no defensible buffer-scope\nmapping for them. Buffer rows have no `user_id` / `agent_id`\nattribution, so `session_id` is the only meaningful query dimension.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Original `message_id` from `/add` |\n| `app_id` | `string` | Scope where the buffered message lives |\n| `project_id` | `string` | Scope where the buffered message lives |\n| `session_id` | `string` | Matches the requested `filters.session_id` |\n| `sende\n\n<!-- [TRUNCATED AT 50KB] -->","docs/architecture.md":"# Architecture\n\n> Companion: [.claude/rules/architecture.md](../.claude/rules/architecture.md) (auto-loaded coding rules)\n\n## DDD layered architecture\n\n```\n┌──────────────────────────────────────────────────────┐\n│  entrypoints/  (Presentation)                         │\n│    cli + api                                          │\n├──────────────────────────────────────────────────────┤\n│  service/      (Application — Use Case orchestration) │\n│    memorize / search / get / knowledge                │\n├──────────────────────────────────────────────────────┤\n│  memory/       (Domain — Business core)               │\n│    models + extract + search + cascade + prompt_slots │\n│    + reflection + strategies + get + events            │\n├──────────────────────────────────────────────────────┤\n│  infra/persistence  (Storage adapters; infra/ may host other adapter types)    │\n│    markdown + sqlite + derived index                  │\n└──────────────────────────────────────────────────────┘\n\nCross-cutting (used by all layers, depends on none):\n  component/  ← Injectable providers (LLM / Embedding / parser / config / utils)\n  core/       ← Runtime base (observability / lifespan / context / errors / persistence / middleware)\n  config/     ← Configuration data (Settings schema + default.toml)\n```\n\n## Dependency direction (single-direction, enforced)\n\n```\nentrypoints → service → memory → infra\n```\n\n| from → to | Allowed? |\n|---|---|\n| entrypoints → service | ✅ |\n| entrypoints → memory / infra | ❌ (must go through service) |\n| service → memory | ✅ |\n| memory → infra | ✅ |\n| memory → service | ❌ |\n| infra → memory | ❌ |\n| infra cross-subpackage (e.g. lancedb → markdown within persistence/) | ❌ (use service to orchestrate); one carve-out: every index backend reads the shared record declarations in `persistence/records/` (`lancedb → records` is whitelisted in import-linter) |\n| any → component / core / config | ✅ (cross-cutting) |\n| `infra/persistence/index → component.embedding` | ✅ — the only network-capable provider `infra` reaches for. Startup measures the embedding model's real output width before the index is opened for use; nothing else can answer that question, and asking it from a higher layer would mean every index user had to remember to ask. Elsewhere `infra` uses `component` only for `utils.datetime`. |\n\nEnforced via `import-linter` in CI:\n\n```toml\n[tool.importlinter]\nroot_packages = [\"everos\"]\n\n[[tool.importlinter.contracts]]\nname = \"Layered architecture\"\ntype = \"layers\"\nlayers = [\n    \"everos.entrypoints\",\n    \"everos.service\",\n    \"everos.memory\",\n    \"everos.infra\",\n]\n```\n\n## Storage three-piece set\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│             md-first storage stack                              │\n└────────────────────────────────────────────────────────────────┘\n\n   ┌──────────────┐   ┌──────────────┐   ┌─────────────────┐\n   │   Markdown   │   │   SQLite     │   │ Derived index   │\n   │  (truth)     │   │  (state)     │   │ LanceDB/Milvus  │\n   ├──────────────┤   ├──────────────┤   ├─────────────────┤\n   │ entries +    │   │ change queue │   │ vector ANN      │\n   │ frontmatter  │   │ + state/LSN  │   │ BM25 (Tantivy)  │\n   │ Git friendly │   │ buffer /     │   │ scalar filter   │\n   │ Obsidian OK  │   │   audit      │   │ multi-modal     │\n   └──────────────┘   └──────────────┘   └─────────────────┘\n          │                  │                    │\n          ▼                  ▼                    ▼\n    memory-root/         .index/sqlite/      .index/<backend>/\n   (truth source)       (system data)       (rebuildable)\n```\n\n## Write path\n\n```\nExternal message\n       │\n       ▼\n1. service.memorize           (entrypoint of write path)\n       │\n       ▼\n2. memory.extract.pipeline    (calls everalgo)\n       │\n       ▼\n3. infra.persistence.markdown.write       (atomic: tmp + fsync + rename)\n       │  ✅ md write success → return immediately\n       │\n   ┌───┴────┐\n   │        │\n   ▼        ▼\n4a. SQLite   4b. memory.cascade  (async daemon)\n    audit        watches md → diff entries → index sync\n```\n\n**Key guarantee**: md write is strongly consistent (fsync). The derived\nindex is eventually consistent. Index backend unavailability does not block\nresponse — changes buffer in the SQLite `md_change_state` queue, replayed on\nrecovery.\n\n## Read path\n\n```\nUser query\n   │\n   ▼\n1. service.search\n   │\n   ▼\n2. memory.search (hybrid)     BM25 + vector ANN + scalar filter\n                              through the configured index backend\n   │\n   ▼\n3. (optional) read md         original markdown for context\n   │\n   ▼\n   Return\n```\n\n## Key components\n\n### `memory/extract/`\n\n```\nextract/\n├── ingest/      Standardized message intake + multi-modal parser dispatch\n├── parser/      Input parsing (format normalization, message preprocessing)\n├── pipeline/    Main extraction pipeline (calls everalgo + dual-track split + writes store)\n└── evolution/   Async memory evolution (event/counter/cron triggers)\n```\n\n### `memory/cascade/`\n\nDaemon that watches markdown changes and syncs to the configured derived\nindex backend:\n\n- inotify / FSEvents file watcher (cross-platform via `watchdog`)\n- 500ms debounce\n- Entry-level diff (added / changed / removed)\n- Per-entry index upsert / delete (text + vector columns update together)\n- LSN-based crash recovery via the SQLite `md_change_state` queue\n- Handlers for all eight business kinds: episode, atomic_fact, foresight,\n  user_profile, agent_case, agent_skill, knowledge_document, knowledge_topic\n\n### `memory/prompt_slots/`\n\nThree-layer prompt overlay:\n\n```\nconfig/prompt_slots/*.yaml          (Layer 1: defaults, ships with package)\n       ↓\n~/.everos/prompt_slots/*.yaml       (Layer 2: app-level override)\n       ↓\nruntime override                    (Layer 3: per-call override)\n```\n\nExtractors may accept a prompt-override parameter; EverOS supplies overrides for episode and boundary-detection prompts, and falls back to the algo-bundled default elsewhere — no hardcoded prompts in algorithm code.\n\n### `memory/reflection/`\n\nOffline memory self-improvement. The orchestrator (`orchestrator.py`)\nimplements the Select → Merge → Re-extract → Deprecate pipeline, merging\nfragmented episodes within a cluster into a single coherent narrative. Driven\nby the `reflect_episodes` OME strategy (cron, disabled by default).\n\n### `memory/strategies/`\n\nOME strategy implementations — one file per strategy:\n\n- `extract_atomic_facts` / `extract_foresight` / `extract_user_profile` — user pipeline\n- `extract_agent_case` / `extract_agent_skill` — agent pipeline\n- `reflect_episodes` — offline episode consolidation (cron)\n- `trigger_profile_clustering` / `trigger_skill_clustering` — clustering triggers\n\n### `core/observability/`\n\nThree-piece observability:\n\n- `metrics/` — Prometheus counter / gauge / histogram + global registry\n- `logging/` — structlog with context processor (trace_id propagation)\n- `tracing/` — OpenTelemetry tracer + span helpers\n\n## Markdown layout\n\n```\n~/.everos/                                  # memory root (default; EVEROS_ROOT)\n└── <app_id>/<project_id>/                  # scope (\"default\" → default_app/default_project)\n    ├── users/<user_id>/\n    │   ├── user.md                                     # profile (single-file rewrite)\n    │   ├── episodes/episode-<YYYY-MM-DD>.md            # daily-log append\n    │   ├── .atomic_facts/atomic_fact-<YYYY-MM-DD>.md   # hidden, framework-derived\n    │   └── .foresights/foresight-<YYYY-MM-DD>.md       # hidden, framework-derived\n    ├── agents/<agent_id>/\n    │   ├── .cases/agent_case-<YYYY-MM-DD>.md           # hidden, framework-derived\n    │   └── skills/skill_<name>/SKILL.md                # named-dir\n    └── knowledge/                                      # global shared knowledge\n```\n\nSystem-managed entries (`.index/`, `.tmp/`) and `ome.toml` live directly\nunder the memory root.\nFull tree + frontmatter chassis: [storage_layout.md](storage_layout.md) and\n[how-memory-works.md](how-memory-works.md). Frontmatter has 4-tier field\nprotection (L1 read-only / L2 system / L3 business / L4 user).\n\n## everalgo boundary\n\n`everalgo` is a set of PyPI-published packages (`everalgo-user-memory`,\n`everalgo-agent-memory`, `everalgo-rank`, `everalgo-knowledge`, plus the\noptional `everalgo-parser` extra), imported under the `everalgo` namespace,\nholding **only memory extraction algorithms**:\n\n- `everalgo.parser` — multi-modal parsing (optional `[multimodal]` extra)\n- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile extractors\n- `everalgo.agent_memory` — AgentMemCell / Case / Skill extractors\n- `everalgo.rank` — boundary detection / fusion + rerank\n- `everalgo.knowledge` — KnowledgeExtractor (document parse + topic extraction)\n\neveralgo is:\n\n- **Stateless** — pure functions, no class hierarchy\n- **No I/O** — does not touch md files, derived indexes, or SQLite\n- **No prompts inline** — extractors that accept a prompt-override parameter use the project-supplied value; others use their algo-bundled defaults\n\nThis boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).\n\n## Error handling architecture\n\n### Exception hierarchy\n\nAll application exceptions derive from `AppError` (`core/errors.py`),\nsplit into four branches by nature:\n\n```\nAppError\n├── DomainError                    (client-side / business-rule violations)\n│   ├── NotFoundError              → 404 NOT_FOUND\n│   │   ├── DocumentNotFoundError\n│   │   └── TopicNotFoundError\n│   ├── ConflictError              → 409 CONFLICT\n│   │   └── DuplicateDocumentError\n│   ├── InvalidInputError          → 422 INVALID_INPUT\n│   │   ├── ExtractionEmptyError   → 422 EXTRACTION_EMPTY\n│   │   └── FilterError\n│   ├── PathTraversalError         → 400 BAD_REQUEST\n│   └── UnsupportedModalityError   → 415 UNSUPPORTED_FORMAT\n├── InfrastructureError            (transient, retryable)     → 503\n│   ├── StorageError\n│   ├── VectorStoreError\n│   └── ExternalServiceError\n│       ├── LLMServiceError\n│       ├── EmbeddingServiceError\n│       └── RerankServiceError\n├── CapabilityError                (permanent, not retryable) → 503\n│   └── MultimodalNotEnabledError\n└── ConfigurationError             (misconfiguration)         → 500\n```\n\n### Error propagation strategy\n\nExceptions are raised at the layer where the error is detected and\npropagate naturally — **service and route layers do not catch-and-wrap**.\nThe entrypoints layer registers per-type exception handlers\n(`entrypoints/api/exception_handlers.py`) via Starlette's MRO dispatch.\nEach handler converts the exception into a canonical error envelope with\nan `ErrorCode` enum value and the appropriate HTTP status code.\n\n### Boundary translation\n\nThird-party exception types are translated at the component boundary to\nprevent external types from leaking into upper layers:\n\n- `everalgo.llm.LLMError` → `LLMServiceError` at `component/parser/_core.py`\n- Embedding / rerank provider errors → `EmbeddingServiceError` / `RerankServiceError` at their respective protocol modules\n\n## Further reading\n\n- [docs/overview.md](overview.md) — vision and scope\n- [docs/engineering.md](engineering.md) — engineering tooling and CI / CD\n- [.claude/rules/architecture.md](../.claude/rules/architecture.md) — short-form rules for Claude Code\n","use-cases/claude-code-plugin/plugin.json":"{\n  \"name\": \"evermem\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Automatically recalls relevant memories from past sessions and injects them into Claude's context\",\n  \"author\": {\n    \"name\": \"EverMem\"\n  },\n  \"keywords\": [\"memory\", \"context\", \"recall\", \"persistence\"]\n}\n","use-cases/claude-code-plugin/skills/memory-tools.md":"---\ndescription: Guidance for using EverMem memory tools to recall past session context\nalwaysInclude: true\n---\n\n# EverMem Memory Tools\n\nYou have access to memory tools that can recall context from the user's past coding sessions. Use these tools proactively when they would help provide better assistance.\n\n## Available Tools\n\n- **evermem_search**: Search past conversations using semantic + keyword matching. Params: `query` (required), `limit` (default 10, max 20)\n\n## When to Use Memory Search\n\n**DO search memories when:**\n- User asks about past work, decisions, or implementations (\"how did we handle X?\")\n- User references previous sessions (\"remember when\", \"last time\", \"we discussed\")\n- User is debugging something that may have been solved before\n- User asks about project patterns, conventions, or architecture decisions\n- Context from previous sessions would improve your response\n- User seems to expect you to know something from before\n\n**DON'T search memories when:**\n- The question is self-contained and doesn't need historical context\n- User explicitly provides all needed context in their message\n- It's a general knowledge question unrelated to their project history\n- You've already searched for this topic in the current session\n\n## Best Practices\n\n1. **Be selective**: Don't search for every query - only when past context adds value\n2. **Use specific queries**: Search for relevant terms, not the entire user message\n3. **Synthesize results**: When you find relevant memories, integrate them naturally into your response\n4. **Be transparent**: Mention when your response is informed by past session context\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# EverOS — md-first Memory Extraction Framework\n\nThis is a Python framework for md-first memory extraction (lightweight; single-user or small-team).\n\n## Quick commands\n\n```bash\nuv sync                    # install deps\nmake lint                  # ruff (check + format-check) + import-linter\nmake format                # auto-fix formatting\nmake test                  # pytest tests/unit\nmake integration           # pytest tests/integration\nmake ci                    # full CI: lint + test + integration\n```\n\n## Architecture\n\nDDD 5 layers + cross-cutting:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — cli + api (Presentation)\n- `service/` — use case orchestration (memorize / retrieve / evolve / manage)\n- `memory/` — domain (extract + search + cascade + prompt_slots + models)\n- `infra/` — storage adapters (markdown + sqlite + lancedb)\n- `component/` — injectable providers (llm / embedding / config / utils)\n- `core/` — runtime base (observability / lifespan / context)\n- `config/` — configuration data (Settings + default.toml)\n\n**Dependency rule**: `entrypoints → service → memory → infra`. Single-direction, enforced by `import-linter`.\n\nDetailed: [docs/architecture.md](docs/architecture.md).\n\n## Engineering practices\n\n- **Coding rules** auto-loaded from [.claude/rules/](.claude/rules/) (10 rules; the three always-loaded ones cover architecture / code-style / language-policy, the rest are path-scoped and load when Claude Code opens a matching file)\n- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`, `/release`\n- **Project-level decisions** in [docs/](docs/) (low-frequency, human-judgment-required)\n- **Language policy**: the project targets a global audience — docs and code are English; CJK appears only in test fixtures and locale-suffixed mirrors. Scanned by `make check-cjk`.\n- **Datetime discipline**: never call `datetime.now()` / `time.time()` directly — use `everos.component.utils.datetime`. Enforced by `make check-datetime`.\n\nContributor engineering reference — build, test, CI gates, branch & commit conventions: [docs/engineering.md](docs/engineering.md).\n\n## Branch strategy\n\n`main` is the default and protected branch. Create scoped branches from\n`main` (`feat/*`, `fix/*`, `docs/*`, `ci/*`, `chore/*`, `refactor/*`) and open\npull requests back to `main` after the required checks pass.\n\nSee [.claude/skills/new-branch/SKILL.md](.claude/skills/new-branch/SKILL.md)\nfor the full branch workflow.\n\n## GitHub sync guard\n\nWhen refreshing this repository from an internal source archive, preserve\nGitHub-only contributor and automation files. Do not overwrite `CLAUDE.md`,\n`.claude/skills/*`, `CONTRIBUTING.md`, or `.github/*` workflow/template files\nwithout checking [docs/github-sync.md](docs/github-sync.md).\n\n## Storage three-piece set\n\n```\nMarkdown (truth)  +  SQLite (state)  +  LanceDB (vector + BM25 + scalar)\n```\n\n- Memory root: `~/.everos/{agents,users,knowledge}/` (md files = single source of truth)\n- System DB: `~/.everos/.index/sqlite/system.db` (state + audit + queue + metadata)\n- Index: `~/.everos/.index/lancedb/` (rebuildable from md)\n\nSelection rationale: [docs/architecture.md](docs/architecture.md).\n\n## Source layout\n\n**src layout** (`src/everos/<...>`): standard PyPA project structure — code lives under `src/` so the working tree is not on the import path until installed, preventing accidental imports of in-development modules.\n\nAlgorithm assets (prompts, extractors) live in the separate [`everalgo`](https://github.com/EverMind-AI/EverAlgo) library, consumed here as the `everalgo-*` PyPI packages.\n\n## Where things go\n\n| Want to... | Look at |\n|---|---|\n| Understand architecture | [docs/architecture.md](docs/architecture.md) |\n| Understand storage choice | [docs/architecture.md](docs/architecture.md) (storage section) |\n| Build, test, CI & conventions | [docs/engineering.md](docs/engineering.md) |\n| Add a new module | [.claude/rules/init-py-and-reexport.md](.claude/rules/init-py-and-reexport.md) |\n| Make a commit | use `/commit` |\n| Open a branch / PR | use `/new-branch` / `/pr` |\n| Run checks before pushing | `make ci` |\n","isInternal":false,"tokens":1050,"sizeBytes":4297},{"name":"architecture.md","path":".claude/rules/architecture.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/architecture.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Architecture rule (always loaded)\n\nEverOS is a DDD-layered framework. The dependency direction is **single, downward only**:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — CLI + HTTP API (presentation). No business logic.\n- `service/` — use-case orchestration (memorize / retrieve / evolve / manage).\n- `memory/` — domain (extract / search / cascade / prompt_slots / models).\n- `infra/` — storage adapters (markdown + sqlite + lancedb) and the OME subsystem.\n- `component/` — injectable providers (llm / embedding / config / utils).\n- `core/` — runtime base (observability / lifespan / context / persistence primitives).\n- `config/` — configuration data (Settings + default TOML).\n\n## Hard constraints (enforced by `import-linter`, run in `make lint`)\n\n1. **Layering**: an outer layer may import an inner layer, never the reverse.\n   `entrypoints → service → memory → infra`.\n2. **Private internals**: `service`, `memory`, and `entrypoints` must not import\n   `infra.persistence.{markdown,lancedb,sqlite}.**` internals — go through the\n   package facade (`from everos.infra.persistence.markdown import ...`).\n3. **OME isolation**: `infra.ome` must not import `persistence`, `memory`,\n   `service`, or `entrypoints`. It is a low-level scheduler with no domain knowledge.\n\nIf a change needs to cross a boundary the wrong way, the design is wrong — refactor,\ndon't add an exception.\n\nFull rationale: [docs/architecture.md](../../docs/architecture.md).\n","isInternal":false,"tokens":378,"sizeBytes":1592},{"name":"async-programming.md","path":".claude/rules/async-programming.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/async-programming.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Async programming rule\n\nThe write/read paths are async end-to-end. Keep them non-blocking.\n\n- **No blocking calls in async functions** — no synchronous file I/O, no `time.sleep`,\n  no blocking DB/network calls inside `async def`. Ruff `ASYNC` flags the common cases.\n- **Offload CPU/blocking work** with `anyio.to_thread.run_sync` (or the established\n  helper) rather than blocking the event loop.\n- **Concurrency** via `asyncio.gather` / `asyncio.TaskGroup` for independent awaits;\n  don't `await` in a loop when the calls are independent.\n- **Tests**: `pytest-asyncio` is in `auto` mode — an `async def test_*` just works,\n  no `@pytest.mark.asyncio` needed.\n- **Don't fire-and-forget** without holding a reference (`asyncio.create_task` results\n  must be tracked, or you lose exceptions). The OME subsystem owns the long-running\n  background loops — application code shouldn't spawn its own.\n","frontmatter":{"paths":["src/**/*.py","tests/**/*.py"]},"isInternal":false,"tokens":241,"sizeBytes":957},{"name":"code-style.md","path":".claude/rules/code-style.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/code-style.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Code style rule (always loaded)\n\n- **Formatter & linter**: `ruff` is the single tool (replaces black / isort / flake8).\n  Line length 88, target `py312`. Run `make format` to auto-fix; `make lint` checks.\n- **Active ruff rule sets**: `E F I N UP B SIM ASYNC`. Don't disable a rule inline\n  unless there's a genuine reason — prefer fixing the code.\n- **Type hints**: annotate every public function signature (params + return). The\n  codebase is ~100% typed; keep it that way.\n- **`from __future__ import annotations`** at the top of every module — annotations\n  are strings, so forward refs and `X | None` unions are free.\n- **Prefer `collections.abc`** (`Sequence`, `Mapping`) over concrete `list`/`dict`\n  in signatures; use `Protocol` for structural interfaces.\n- **No dead code**: no commented-out blocks, no unused imports, no speculative\n  abstractions. Delete rather than comment out.\n- **Naming**: `*Manager` (orchestrators), `*Provider` (injectable services),\n  `*Reader`/`*Writer` (persistence), `*Recaller` (search routes). Follow the\n  established suffix when adding a sibling.\n","isInternal":false,"tokens":289,"sizeBytes":1095},{"name":"datetime-handling.md","path":".claude/rules/datetime-handling.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/datetime-handling.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Datetime handling rule (two-zone discipline)\n\n**Never** construct or read \"now\" directly. All datetime flows through\n`everos.component.utils.datetime`. This is a **hard CI gate**\n(`make check-datetime`, wired into `make lint`).\n\n## Banned (the checker fails the build on these)\n\n- `datetime.now()`, `datetime.utcnow()`, `datetime.today()`\n- `time.time()`, `time.time_ns()`\n- `datetime(YYYY, ...)` without `tzinfo=`\n- `.astimezone(...)` / `.replace(tzinfo=...)` outside the helper module\n\n## Use instead\n\n| Need | Helper |\n|---|---|\n| \"now\" for **storage** (UTC) | `get_utc_now()` |\n| \"now\" for **display** (configured TZ) | `get_now_with_timezone()` |\n| today's date, display TZ | `today_with_timezone()` |\n| normalize a value to UTC | `ensure_utc(d)` |\n| render to display TZ | `to_display_tz(d)` |\n| parse ISO / epoch / str | `from_iso_format(v)`, `from_timestamp(ts)` |\n| serialize | `to_iso_format(d)`, `to_date_str(d)`, `to_timestamp_ms(d)` |\n\n**Two zones**: persist in UTC, present in the configured display TZ. Crossing them\ngoes through the helpers — never ad-hoc. See [docs/datetime.md](../../docs/datetime.md).\n","frontmatter":{"paths":["src/**/*.py","tests/**/*.py"]},"isInternal":false,"tokens":327,"sizeBytes":1179},{"name":"imports.md","path":".claude/rules/imports.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Imports rule\n\n- **`from __future__ import annotations`** is the first import in every module.\n- **Import order** (ruff `I` enforces, `make format` fixes): stdlib → third-party\n  → first-party (`everalgo`, then `everos`). One group per blank-line-separated block.\n- **Absolute imports** for cross-package references (`from everos.memory import ...`).\n  Relative imports (`from .models import ...`) only **within** a package, typically\n  in its `__init__.py`.\n- **`TYPE_CHECKING` guard** for import cycles and type-only imports:\n  ```python\n  from typing import TYPE_CHECKING\n  if TYPE_CHECKING:\n      from everos.config import Settings\n  ```\n- Never import a private internal across a package boundary — respect the\n  `import-linter` contracts (see [architecture.md](architecture.md)).\n","frontmatter":{"paths":["src/**/*.py","tests/**/*.py"]},"isInternal":false,"tokens":209,"sizeBytes":847},{"name":"init-py-and-reexport.md","path":".claude/rules/init-py-and-reexport.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/init-py-and-reexport.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/__init__.py\"\n  - \"src/**/*.py\"\n---\n\n# `__init__.py` and re-export rule\n\nA package's `__init__.py` is its **public facade**. Consumers import from the\npackage, never from its internal modules.\n\n## Pattern\n\n```python\n\"\"\"One-paragraph module docstring: what this package is and how to use it.\"\"\"\n\nfrom .models import Episode as Episode\nfrom .models import MemCell as MemCell\n\n__all__ = [\n    \"Episode\",\n    \"MemCell\",\n]\n```\n\n- **Explicit `X as X` redundant-alias form** on each re-export. This is intentional:\n  it marks the name as a deliberate public re-export (ruff `F401` / `PLC0414` aware)\n  rather than an accidental unused import.\n- **`__all__`** lists every public name, alphabetically sorted, matching the\n  re-exports. It is the contract; keep it in sync.\n- **Internal modules stay private** — don't re-export helpers that aren't part of\n  the public API.\n- New subpackage? Add an `__init__.py` with a docstring + `__all__` even if it\n  starts small. Empty-but-documented beats missing.\n\nThis facade discipline is what lets `import-linter` forbid deep imports across\npackage boundaries (see [architecture.md](architecture.md)).\n","frontmatter":{"paths":["src/**/__init__.py","src/**/*.py"]},"isInternal":false,"tokens":302,"sizeBytes":1160},{"name":"language-policy.md","path":".claude/rules/language-policy.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/language-policy.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Language policy rule (always loaded)\n\nThe project targets a global audience and is **English-first**.\n\n- **Code, comments, docstrings, docs, commit messages, identifiers**: English only.\n- **CJK characters are allowed only in**:\n  - test fixtures under `tests/` and `data/` (multilingual input is the point), and\n  - locale-suffixed mirror files (e.g. `*_zh.json`).\n- Do **not** introduce CJK into `src/`, `docs/`, or config.\n\nEnforcement: `make check-cjk` scans for stray CJK outside the allowlist (advisory).\nKeep user-facing strings and error messages in English.\n","isInternal":false,"tokens":145,"sizeBytes":569},{"name":"logging-observability.md","path":".claude/rules/logging-observability.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/logging-observability.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/**/*.py\"\n---\n\n# Logging & observability rule\n\n- **Use the project logger**, never `print` or the stdlib `logging` directly:\n  ```python\n  from everos.core.observability.logging import get_logger\n  logger = get_logger(__name__)\n  ```\n- **Structured logging** (`structlog`): pass context as keyword fields, not f-strings.\n  ```python\n  logger.info(\"memory.search.completed\", owner_type=owner, n_results=len(items))\n  ```\n  Event name first (dotted, stable), structured kwargs after. This keeps logs\n  queryable and avoids leaking interpolated PII into the message string.\n- **Levels**: `debug` for developer detail, `info` for lifecycle milestones,\n  `warning` for recoverable anomalies, `error` for failures with a stack/context.\n- **Metrics** go through `core.observability.metrics` (Prometheus); don't invent\n  ad-hoc counters. Histograms/counters/gauges have registry helpers.\n- Don't log secrets, API keys, or full memory content at `info`/above.\n- **Tracing** (optional, `[otel]` extra, **off by default**): open spans with\n  `memory_span(...)` from `core.observability.tracing` — it stamps the Langfuse\n  `langfuse.*` attributes and is a no-op until `[observability] enabled`, so call\n  sites never branch on config. LLM / embedding token usage rides\n  `set_generation_usage` onto the active span (Langfuse computes cost).\n  Request/response content is emitted only when `capture_content` is on\n  (redaction hook + truncation). `request_id` is kept independent of the OTel\n  `trace_id`; an upstream `traceparent` header is continued when present.\n","frontmatter":{"paths":["src/**/*.py"]},"isInternal":false,"tokens":389,"sizeBytes":1574},{"name":"module-docstring.md","path":".claude/rules/module-docstring.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/module-docstring.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"src/everos/infra/**/*.py\"\n  - \"src/everos/memory/**/*.py\"\n  - \"src/everos/service/**/*.py\"\n  - \"src/everos/component/**/*.py\"\n  - \"src/everos/core/**/*.py\"\n---\n\n# Module docstring rule\n\nEvery non-trivial module in the domain/infra layers opens with a docstring that\nexplains **intent and contract**, not just a one-line label.\n\nA good module docstring states:\n\n- **What** the module is responsible for (one sentence).\n- **The load-bearing invariants** — the rules a reader must know to change it\n  safely (partition keys, what is/isn't written, defaults, ignored flags).\n- **External usage** when the module is a package facade (a short import example).\n\nExample (abbreviated, from `memory/search/manager.py`):\n\n```python\n\"\"\"SearchManager — top-level orchestrator for POST /api/v2/memory/search.\n\nHard partition by owner_type: user → episodes (+ profiles), agent →\nagent_cases + agent_skills. The manager never writes to storage; it only\nreads LanceDB + markdown.\n\"\"\"\n```\n\nPrefer prose that would save the next engineer a debugging session over\nboilerplate. If a module is genuinely trivial (a 3-line constant), a one-liner\nis fine — but most modules here are not.\n","frontmatter":{"paths":["src/everos/infra/**/*.py","src/everos/memory/**/*.py","src/everos/service/**/*.py","src/everos/component/**/*.py","src/everos/core/**/*.py"]},"isInternal":false,"tokens":293,"sizeBytes":1191},{"name":"testing.md","path":".claude/rules/testing.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/rules/testing.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"tests/**/*.py\"\n---\n\n# Testing rule\n\nTests mirror the source layout: `tests/unit/test_<layer>/...`,\n`tests/integration/...`, `tests/e2e/...`.\n\n- **Structure**: `tests/unit/` mirrors `src/everos/` package-for-package. Put a test\n  next to where its subject lives in the mirror.\n- **Async**: `pytest-asyncio` is in `auto` mode — write `async def test_*` directly,\n  no marker needed.\n- **Markers** (default run excludes both — `-m \"not slow and not live_llm\"`):\n  - `@pytest.mark.slow` — tests ≥ ~10s.\n  - `@pytest.mark.live_llm` — needs real LLM/embedder credentials.\n  Keep unit tests fast and credential-free; push anything needing real services\n  behind a marker or into `integration`/`e2e`.\n- **Fixtures**: shared fixtures live in the nearest `conftest.py`. The root conftest\n  resets module caches (settings/logging/datetime) per test — rely on that for\n  isolation rather than mutating globals.\n- **Module docstring** on each test file stating what contract it pins (see existing\n  tests for the style).\n- **Coverage gate**: `make cov` enforces 80% (`--cov-fail-under=80`). New code should\n  not drop coverage below the gate.\n- Run `make test` (unit) and `make integration` before pushing; both run in CI.\n","frontmatter":{"paths":["tests/**/*.py"]},"isInternal":false,"tokens":329,"sizeBytes":1238},{"name":"SKILL.md","path":".claude/skills/add-memory-kind/SKILL.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/skills/add-memory-kind/SKILL.md","title":"add-memory-kind","category":"anthropic-skill","format":"markdown","content":"---\nname: add-memory-kind\ndescription: 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).\n---\n\n# /add-memory-kind — Add a new business memory kind\n\n## When to invoke\n\nAdding a new persisted business entity (Episode, Case, Skill, AtomicFact,\nForesight, Profile, or something custom). Multiple storage layers may be\ninvolved; this skill walks the decision then the wiring.\n\n## 1. Decide the storage combination\n\nA memory kind **does not have to use all three** layers. Pick by what\nthe kind actually needs:\n\n| Need | Markdown | SQLite | LanceDB |\n|---|:-:|:-:|:-:|\n| Human-readable / agent-editable source-of-truth text | ✅ | | |\n| Structured state, ACID transactions, joins, predicates | | ✅ | |\n| Vector / BM25 / hybrid retrieval | | | ✅ |\n\nCommon combinations seen in EverOS:\n\n| Combo | Example | Rationale |\n|---|---|---|\n| **md only** | scratch notes / dump bins | text-of-truth, no index needed |\n| **md + lancedb** | episode / memcell / case | text-of-truth + semantic retrieval |\n| **md + sqlite** | profile / playbook / soul.md state | text-of-truth + structured state to query |\n| **md + sqlite + lancedb** | full-blown business records | when you need *both* transactional state AND retrieval |\n| **sqlite only** | audit log / task queue / LSN watermark | system state, never user-facing |\n| **lancedb only** | rare; usually you still want md | derived embeddings without text-of-truth |\n\nRule of thumb: **markdown is the truth**; sqlite and lancedb are derived\nindexes that can be rebuilt from md. Drop md only when the kind has no\nhuman-readable form (pure system state).\n\n## 2. Pick the markdown storage strategy (if md is in your combo)\n\nThree strategies — declared in the EverOS Markdown First spec:\n\n| Strategy | Filename | Mutation | Examples |\n|---|---|---|---|\n| **Daily-log append** | `<prefix>-YYYY-MM-DD.md` | append entries | memcell / episode / case / atomic_fact / foresight |\n| **Skill-named in-place** | `skill_<name>.md` | overwrite the file | skills (procedural memory) |\n| **Single-file rewrite** | `user.md` / `agent.md` / `soul.md` / `behaviors.md` / `tools.md` | overwrite the file | profiles / playbooks |\n\nThis skill currently has a **complete recipe for daily-log append**.\nSkill-named and single-file recipes are sketched at the bottom — their\nbase writers (`BaseSkillWriter` / `BaseProfileWriter`) land later in the\nproject; until then build a thin wrapper over `MarkdownWriter`\ndirectly.\n\n---\n\n## 3. Markdown daily-log: 4 steps\n\n### 3.1 Frontmatter schema — `infra/persistence/markdown/mds/<name>.py`\n\n```python\n\"\"\"Episode daily-log frontmatter.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime as _dt\nfrom typing import ClassVar, Literal\n\nfrom everos.core.persistence.markdown import UserScopedFrontmatter\n\n\nclass UserEpisodeDailyFrontmatter(UserScopedFrontmatter):\n    \"\"\"``users/<u>/episodes/episode-<YYYY-MM-DD>.md``.\"\"\"\n\n    ENTRY_ID_PREFIX: ClassVar[str] = \"ep\"\n    DIR_NAME: ClassVar[str] = \"episodes\"\n    FILE_PREFIX: ClassVar[str] = \"episode\"\n\n    type: Literal[\"user_episode_daily\"] = \"user_episode_daily\"\n    date: _dt.date\n    entry_count: int = 0\n    last_appended_at: _dt.datetime | None = None\n```\n\nFor agent-track kinds subclass `AgentScopedFrontmatter` instead. If\nuser-track and agent-track share a kind name (e.g. `memcell`), give\neach a **distinct** `ENTRY_ID_PREFIX` (e.g. `umc` vs `amc`) so reverse\nlookup is unambiguous.\n\n### 3.2 Re-export — `mds/__init__.py`\n\n```python\nfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatter\n```\n\n### 3.3 Business writer — `infra/persistence/markdown/writers/<name>.py`\n\n```python\n\"\"\"Episode appender.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom everos.core.persistence import MarkdownReader\n\nfrom ..mds import UserEpisodeDailyFrontmatter\nfrom .base import BaseDailyWriter\n\n\nclass UserEpisodeAppender(BaseDailyWriter):\n    schema = UserEpisodeDailyFrontmatter\n\n    # OPTIONAL: override the count strategy. Default is len(entries);\n    # override to trust the frontmatter field instead.\n    def _current_count(self, path: Path) -> int:\n        if not path.exists():\n            return 0\n        return MarkdownReader.read(path).frontmatter.get(\"entry_count\", 0)\n```\n\n### 3.4 Re-export — `writers/__init__.py`\n\n```python\nfrom .episode import UserEpisodeAppender as UserEpisodeAppender\n```\n\n### Done — usage\n\n```python\nfrom everos.infra.persistence.markdown.writers import UserEpisodeAppender\n\nappender = UserEpisodeAppender(memory_root)\neid = appender.append(\"u_jason\", \"I went to the doctor today.\")\n# → users/u_jason/episodes/episode-<today>.md\n# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)\n```\n\n---\n\n## 4. (Optional) SQLite table — 4 steps\n\nSkip this section if the kind doesn't need structured state beyond markdown.\n\n### 4.1 Schema — `infra/persistence/sqlite/tables/<name>.py`\n\n```python\nfrom everos.core.persistence.sqlite import BaseTable, Field\n\n\nclass EpisodeState(BaseTable, table=True):\n    __tablename__ = \"episode_state\"  # type: ignore[assignment]\n\n    id: int | None = Field(default=None, primary_key=True)\n    entry_id: str = Field(index=True, unique=True)\n    cluster_id: str | None = Field(default=None, index=True)\n    status: str = Field(default=\"active\")\n```\n\n`BaseTable` already provides `created_at` / `updated_at` (auto-bumped).\n\n### 4.2 Re-export — `tables/__init__.py`\n\n```python\nfrom .episode import EpisodeState as EpisodeState\n```\n\n### 4.3 Repo — `infra/persistence/sqlite/repos/<name>.py`\n\n```python\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\n\nfrom everos.core.persistence.sqlite import RepoBase\n\nfrom ..sqlite_manager import get_session_factory\nfrom ..tables import EpisodeState\n\n\nclass _EpisodeStateRepo(RepoBase[EpisodeState]):\n    model = EpisodeState\n\n    def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:\n        return get_session_factory()\n\n\nepisode_state_repo = _EpisodeStateRepo()\n```\n\n### 4.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_state_repo as episode_state_repo\n```\n\n---\n\n## 5. (Optional) Derived index — 4 steps\n\nSkip this section if the kind doesn't need vector / BM25 / hybrid retrieval.\n\n### 5.1 Record declaration — `infra/persistence/records/<name>.py`\n\n```python\nfrom typing import ClassVar\n\nfrom everos.core.persistence.lancedb import BaseLanceTable, Vector\n\n\nclass EpisodeIndex(BaseLanceTable):\n    TABLE_NAME: ClassVar[str] = \"episode_index\"\n    PRIMARY_KEY: ClassVar[tuple[str, ...]] = (\"app_id\", \"project_id\", \"id\")\n\n    id: str\n    app_id: str = \"default\"\n    project_id: str = \"default\"\n    entry_id: str\n    text: str\n    tags: list[str]\n    vector: Vector(1024) | None = None  # type: ignore[valid-type]\n```\n\n`TABLE_NAME` / `BM25_FIELDS` / `PRIMARY_KEY` are the backend-neutral\ndeclarations every index backend reads (`IndexRecordMeta`). `PRIMARY_KEY`\nhas **no default**: `check_primary_keys` refuses to start, and LanceDB\n`upsert` refuses to write, a table that does not declare one. `id` is unique\nonly within `(app_id, project_id)`, so every business table declares the\nthree-column key — LanceDB merges on it, Milvus keys the collection on a\ncomposed `__pk` and rejects key components containing `/`. `Vector(N)` must\nmatch your embedding dimension.\n\n### 5.2 Re-export — `records/__init__.py`\n\n```python\nfrom .episode import EpisodeIndex as EpisodeIndex\n```\n\n### 5.3 Repo — `infra/persistence/lancedb/repos/<name>.py`\n\n```python\nfrom lancedb import AsyncTable\n\nfrom everos.core.persistence.lancedb import LanceRepoBase\nfrom everos.infra.persistence.records import EpisodeIndex\n\nfrom ..lancedb_manager import get_table\n\n\nclass _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):\n    schema = EpisodeIndex\n\n    async def _table_lookup(self) -> AsyncTable:\n        return await get_table(self.schema.TABLE_NAME, self.schema)\n\n\nepisode_index_repo = _EpisodeIndexRepo()\n```\n\nBusiness code writes through the `everos.infra.persistence.index` facade:\n`upsert(records)` merges on the declared key and is the only write; look a\nrow up with `find_one_where(primary_key(app_id, project_id, row_id))`.\n\n### 5.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_index_repo as episode_index_repo\n```\n\n---\n\n## 6. (Future) Skill-named & single-file markdown strategies\n\nWhen the new memory kind needs:\n\n- **skill-named** files (one file per named skill, in-place rewrite) — wait\n  for `BaseSkillWriter`, or use `MarkdownWriter.write_markdown` directly\n  with a thin wrapper.\n- **single-file rewrite** (one fixed file like `user.md`) — wait for\n  `BaseProfileWriter`, same fallback.\n\nThese strategies do **not** use entry markers; their frontmatter schema\ndoes not need `ENTRY_ID_PREFIX` (only `id` / `type` / `schema_version` plus\nthe scope mixin fields).\n\n---\n\n## 7. Verification checklist\n\n- [ ] `make lint` — ruff + import-linter clean\n- [ ] `make test` — existing manager / lifespan / writer tests still pass\n- [ ] When the kind crosses **multiple** layers:\n  - [ ] markdown entry id is the join key for the sqlite / lancedb rows\n  - [ ] business code reads only via the repo singleton (no raw engine\n        access in service / memory)\n  - [ ] cascade daemon (when it lands) can rebuild sqlite / lancedb\n        from md alone — keep md as the truth\n\nTests by layer:\n\n| Tests for | Location |\n|---|---|\n| Markdown frontmatter schema | `tests/unit/test_infra/test_markdown/test_mds/` |\n| Markdown business appender | `tests/unit/test_infra/test_markdown/test_writers/` |\n| SQLite RepoBase logic | `tests/unit/test_core/test_persistence/test_sqlite/` |\n| SQLite manager / lifespan | `tests/unit/test_infra/test_sqlite/` |\n| LanceDB LanceRepoBase logic | `tests/unit/test_core/test_persistence/test_lancedb/` |\n| LanceDB manager / lifespan | `tests/unit/test_infra/test_lancedb/` |\n\n## 8. Common pitfalls\n\n| Mistake | Symptom | Fix |\n|---|---|---|\n| Forgot `ENTRY_ID_PREFIX` / `DIR_NAME` / `FILE_PREFIX` on a daily-log schema | `BaseDailyWriter.__init__` raises `TypeError` | Add all three ClassVars |\n| Same `ENTRY_ID_PREFIX` on user + agent variants | `MemoryLayout.locate_for_entry` collision error | Use distinct prefixes (e.g. `umc` vs `amc`) |\n| Imported `RepoBase` from `infra.persistence.sqlite` | `ImportError` | Lives in `core.persistence.sqlite` (moved earlier) |\n| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each `__init__.py` |\n| `Vector(N)` mismatched with embedding dim | LanceDB raises on insert | Make `N` exactly match the model output |\n| 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 |\n| Hand-rolling `datetime.now()` instead of `today_with_timezone()` | Day-boundary drift across timezones | Always go through `everos.component.utils.datetime` |\n\n## Background\n\n- Architecture: [../../rules/architecture.md](../../rules/architecture.md)\n- `__init__.py` re-export rules: [../../rules/init-py-and-reexport.md](../../rules/init-py-and-reexport.md)\n- Async programming: [../../rules/async-programming.md](../../rules/async-programming.md)\n- Datetime handling: [../../rules/datetime-handling.md](../../rules/datetime-handling.md)\n","frontmatter":{"name":"add-memory-kind","description":"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)."},"isInternal":false,"tokens":2908,"sizeBytes":11496},{"name":"SKILL.md","path":".claude/skills/commit/SKILL.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/skills/commit/SKILL.md","title":"commit","category":"anthropic-skill","format":"markdown","content":"---\nname: commit\ndescription: Stage and create a Conventional Commits message for the current change\n---\n\n# /commit\n\nCreate a well-formed commit following the [Conventional Commits](https://www.conventionalcommits.org)\nstandard. The format is enforced by `gitlint` in the `commit-msg` hook.\n\n## Steps\n\n1. Run `git status` and `git diff` (and `git diff --staged`) to see what changed.\n2. Review recent history for style: `git log --oneline -10`.\n3. Group the change into a single focused commit. If the working tree mixes\n   unrelated changes, stage selectively (`git add -p` / specific paths) rather\n   than committing everything at once.\n4. Write the message in **Conventional Commits** form:\n\n   ```\n   <type>[(scope)][!]: <imperative summary, ≤72 chars>\n\n   <optional body: what & why, wrapped at 72>\n\n   <optional footer: BREAKING CHANGE: …, Refs: #123>\n   ```\n\n5. Never use `--no-verify`. If pre-commit hooks fail, fix the cause and re-commit.\n6. Do not commit secrets, generated artifacts, or work-in-progress directly to\n   the protected `main` branch.\n\n## Types\n\n| Type | Use for |\n|---|---|\n| `feat` | new feature |\n| `fix` | bug fix |\n| `refactor` | behavior-preserving restructure |\n| `test` | add / update tests |\n| `docs` | documentation |\n| `style` | formatting only |\n| `perf` | performance |\n| `chore` | config / build / tooling |\n| `build` | build system or dependencies |\n| `ci` | CI configuration |\n| `revert` | revert a previous commit |\n\n## Notes\n\n- No emoji — the title must start with the `type`.\n- One logical change per commit; keep the history bisectable.\n- The summary is imperative mood: \"add\", not \"added\" / \"adds\".\n- `scope` is optional: `fix(search): …`. A `!` before the colon (or a\n  `BREAKING CHANGE:` footer) marks a breaking change.\n","frontmatter":{"name":"commit","description":"Stage and create a Conventional Commits message for the current change"},"isInternal":false,"tokens":473,"sizeBytes":1777},{"name":"SKILL.md","path":".claude/skills/new-branch/SKILL.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/skills/new-branch/SKILL.md","title":"new-branch","category":"anthropic-skill","format":"markdown","content":"---\nname: new-branch\ndescription: Create a GitHub branch from main with the project naming convention\n---\n\n# /new-branch\n\nCut a new branch for the GitHub repository.\n\n## Branch model\n\n```\nmain       = default and protected branch\nfeat/*     = feature work\nfix/*      = bug fixes\ndocs/*     = documentation-only changes\nci/*       = CI, build, and developer-experience changes\nchore/*    = repository maintenance\nrefactor/* = behavior-preserving code structure changes\n```\n\n## Steps\n\n1. Ask (or infer) the change type: `feat`, `fix`, `docs`, `ci`, `chore`, or\n   `refactor`.\n2. Update `main` first:\n   ```bash\n   git checkout main\n   git pull --ff-only\n   ```\n3. Create the branch with a kebab-case slug:\n   ```bash\n   git checkout -b <type>/<short-slug>\n   ```\n4. Keep the branch scoped to one purpose and open a pull request back to\n   `main`.\n\n## Naming\n\n- `feat/add-agentic-rerank`, `fix/empty-profile-crash`,\n  `docs/quickstart-config`, `ci/check-github-docs`.\n- Lowercase, hyphen-separated, no spaces, concise.\n\nNever commit directly to `main` — always use a branch and pull request.\n","frontmatter":{"name":"new-branch","description":"Create a GitHub branch from main with the project naming convention"},"isInternal":false,"tokens":288,"sizeBytes":1091},{"name":"SKILL.md","path":".claude/skills/pr/SKILL.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/skills/pr/SKILL.md","title":"pr","category":"anthropic-skill","format":"markdown","content":"---\nname: pr\ndescription: Open a GitHub PR targeting the correct branch with the project template\n---\n\n# /pr\n\nOpen a pull request on GitHub using the `gh` CLI and the repo's PR template.\n\n## Steps\n\n1. Confirm the branch and target:\n   - Base branch is **`main`**.\n   - Head branch should be a scoped branch such as `feat/*`, `fix/*`,\n     `docs/*`, `ci/*`, `chore/*`, or `refactor/*`.\n2. Ensure local checks pass first:\n   ```bash\n   make ci\n   ```\n   Do not open a PR with failing lint/tests.\n3. Push the branch:\n   ```bash\n   git push -u origin HEAD\n   ```\n4. Create the PR, filling the template\n   ([.github/PULL_REQUEST_TEMPLATE.md](../../../.github/PULL_REQUEST_TEMPLATE.md)):\n   ```bash\n   gh pr create --base main --fill-first\n   ```\n   Then edit the body to complete each section:\n   - **Summary** — what changed and why.\n   - **Area** — tick the relevant box (architecture / benchmark / use case /\n     docs / DX / CI-build-release).\n   - **Verification** — paste the commands you ran (`make ci`, manual checks).\n   - **Checklist** — tick honestly; don't tick boxes you didn't satisfy.\n   - **Notes for Reviewers** — anything subtle.\n\n## Notes\n\n- Keep the PR scoped to one area. Split unrelated changes.\n- If `make ci` was not fully run, say so in Verification rather than implying it passed.\n- Never retarget a community PR away from `main` unless a maintainer explicitly\n  asks for it.\n","frontmatter":{"name":"pr","description":"Open a GitHub PR targeting the correct branch with the project template"},"isInternal":false,"tokens":357,"sizeBytes":1407},{"name":"SKILL.md","path":".claude/skills/release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/.claude/skills/release/SKILL.md","title":"release","category":"anthropic-skill","format":"markdown","content":"---\nname: release\ndescription: Cut a versioned release and publish everos to PyPI via the tag-triggered workflow\n---\n\n# /release\n\nPublish a new version of `everos` to PyPI. Publishing is automated: pushing a\n`vX.Y.Z` tag triggers [.github/workflows/release.yml](../../../.github/workflows/release.yml),\nwhich builds, smoke-tests, and uploads via PyPI **Trusted Publishing** (OIDC —\nno stored token) behind the `release` environment's manual-approval gate, then\ndrafts the GitHub Release page from the CHANGELOG.\n\nA release is not finished when PyPI accepts the upload: the GitHub Release page\nis drafted, never auto-published, and someone has to write its lead summary and\nclick **Publish**.\n\n## Preconditions\n\n- On `main`, up to date, with green CI (the tag builds from `main`'s tree).\n- Decide the version per SemVer: patch = fixes, minor = back-compatible\n  features, major = breaking changes.\n\n## Steps\n\n```\n1. Bump the version    → pyproject.toml [project] version = \"X.Y.Z\"\n   (single source; everos.__version__ reads installed package metadata)\n2. Update CHANGELOG.md → move the Unreleased entries under a new\n   ## [X.Y.Z] - <date> heading, and write the release page's prose here\n   (lead paragraph + `### Upgrade` group — see \"The release page\")\n3. Commit              → git commit -m \"chore(release): vX.Y.Z\"\n4. Open a PR, merge to main after green CI\n5. Tag main + push     → git tag -a vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z\n6. Approve             → the release.yml run pauses on the `release`\n   environment; a reviewer approves in the Actions run\n7. Verify              → https://pypi.org/project/everos/X.Y.Z/\n8. Publish the page    → the run leaves a DRAFT GitHub Release, already\n   complete if step 2 was done properly. Read it once, click Publish.\n```\n\nThe tag must equal the `pyproject.toml` version — the workflow refuses to\npublish on a mismatch. A stable tag with no matching `## [X.Y.Z]` CHANGELOG\nsection fails the release job for the same reason.\n\n## The release page\n\n**The whole page is written in CHANGELOG.md, during the release PR.** Nothing\nis meant to be composed at publish time — by then the changes are weeks old and\nthe text gets no review. Write the version section like this:\n\n```markdown\n## [X.Y.Z] - 2026-09-01\n\n**What this release is for.** One paragraph, prose, no bullets — it becomes the\nlead of the release page. Say what changed for a user, not what was refactored.\n\n### Added\n### Changed\n### Fixed\n\n### Upgrade\n\nWhat a reader must know before upgrading: what happens on first startup, which\ncommand recovers a bad state, which pins moved. Omit the group entirely when a\nplain `pip install --upgrade` is all there is — 1.1.4 and 1.2.0 have nothing\nhere. Do not write filler.\n```\n\nCI turns that into the page: everything above `### Upgrade` is lifted verbatim\nwith the group headings demoted to `##`, and the Upgrade prose is wrapped in the\nboilerplate — pip line above it, compare link below — which is the shape every\nrelease since 1.1.3 has. See\n[1.2.1](https://github.com/EverMind-AI/EverOS/releases/tag/v1.2.1).\n\nSo publishing is a read-through and a click. If the draft looks wrong, the fix\nbelongs in CHANGELOG.md on `main`, not only in the draft — otherwise the two\ndrift apart and the next release inherits the habit.\n\n> While it is a draft, GitHub serves the release at\n> `releases/tag/untagged-<hash>`, and that URL keeps serving a stale page after\n> publication with no redirect. Never share it — a reader who opens it later\n> concludes the release never went out. Link `releases/tag/vX.Y.Z` instead; the\n> job prints both URLs in its step summary.\n\nPublish with the **Publish release** button in the web UI. Publishing through\nthe API by flipping `draft` alone drops the tag: GitHub rebinds the release to\nthe `untagged-<hash>` placeholder and creates a git tag by that name against the\ndefault branch (observed on 1.2.2). Pass `tag_name` if you must do it from the\nCLI:\n\n```bash\ngh api -X PATCH \"repos/EverMind-AI/EverOS/releases/<id>\" \\\n  -F draft=false -f tag_name=vX.Y.Z -f make_latest=true\n```\n\nRe-running the release job replaces its own draft and leaves an already-published\nrelease untouched, so a re-run is always safe.\n\n## Pre-releases\n\nPEP 440 pre-release tags publish too (PyPI accepts them; `pip install everos`\nignores them unless `--pre`): `vX.Y.ZrcN`, `vX.Y.ZaN`, `vX.Y.ZbN`. Set the same\nsuffix in `pyproject.toml` version before tagging.\n\nTheir release page is drafted as a **pre-release** and never becomes\n`/releases/latest`. A pre-release does not need its own CHANGELOG section — the\ndraft falls back to a one-line placeholder body when there is none.\n\n## One-time setup (project owner, not doable from CI)\n\n1. **PyPI trusted publisher** — PyPI → project `everos` → Settings →\n   Publishing → add: owner `EverMind-AI`, repo `EverOS`, workflow\n   `release.yml`, environment `release`.\n2. **GitHub environment** — repo Settings → Environments → create `release`\n   with required reviewers, so every publish needs a manual approval.\n\nNo PyPI API token is ever stored; the workflow mints a short-lived OIDC token\nat publish time.\n","frontmatter":{"name":"release","description":"Cut a versioned release and publish everos to PyPI via the tag-triggered workflow"},"isInternal":false,"tokens":1330,"sizeBytes":5166},{"name":"memory-tools.md","path":"use-cases/claude-code-plugin/skills/memory-tools.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/use-cases/claude-code-plugin/skills/memory-tools.md","title":"Skills Skill","category":"anthropic-skill","format":"markdown","content":"---\ndescription: Guidance for using EverMem memory tools to recall past session context\nalwaysInclude: true\n---\n\n# EverMem Memory Tools\n\nYou have access to memory tools that can recall context from the user's past coding sessions. Use these tools proactively when they would help provide better assistance.\n\n## Available Tools\n\n- **evermem_search**: Search past conversations using semantic + keyword matching. Params: `query` (required), `limit` (default 10, max 20)\n\n## When to Use Memory Search\n\n**DO search memories when:**\n- User asks about past work, decisions, or implementations (\"how did we handle X?\")\n- User references previous sessions (\"remember when\", \"last time\", \"we discussed\")\n- User is debugging something that may have been solved before\n- User asks about project patterns, conventions, or architecture decisions\n- Context from previous sessions would improve your response\n- User seems to expect you to know something from before\n\n**DON'T search memories when:**\n- The question is self-contained and doesn't need historical context\n- User explicitly provides all needed context in their message\n- It's a general knowledge question unrelated to their project history\n- You've already searched for this topic in the current session\n\n## Best Practices\n\n1. **Be selective**: Don't search for every query - only when past context adds value\n2. **Use specific queries**: Search for relevant terms, not the entire user message\n3. **Synthesize results**: When you find relevant memories, integrate them naturally into your response\n4. **Be transparent**: Mention when your response is informed by past session context\n","frontmatter":{"description":"Guidance for using EverMem memory tools to recall past session context","alwaysInclude":true},"isInternal":false,"tokens":331,"sizeBytes":1631},{"name":"api.md","path":"docs/api.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/docs/api.md","title":"docs - API Interface Contract & Specifications","category":"project-spec","format":"markdown","content":"# EverOS HTTP API (v2)\n\nHuman-readable reference for the EverOS HTTP API. Schema names, types\nand validation constraints mirror the OpenAPI spec served at\n`GET /openapi.json` (when the server runs with `ENV=DEV`; see\n[OpenAPI spec source](#openapi-spec-source)). This document adds the\nbusiness semantics the raw spec does not carry.\n\n## Table of contents\n\n- [Overview](#overview)\n  - [Base URL and versioning](#base-url-and-versioning)\n  - [Content type](#content-type)\n  - [Authentication](#authentication)\n  - [Response envelope](#response-envelope)\n  - [Eventual consistency](#eventual-consistency)\n  - [Conventions](#conventions)\n  - [ScopeId: `app_id` and `project_id`](#scopeid-app_id-and-project_id)\n- [Errors](#errors)\n- [Common types](#common-types)\n  - [MessageItem](#messageitem)\n  - [ContentItem](#contentitem)\n  - [ToolCall](#toolcall)\n  - [ToolFunction](#toolfunction)\n  - [FilterNode (filter DSL)](#filternode-filter-dsl)\n  - [SearchMethod](#searchmethod)\n  - [GetMemoryType](#getmemorytype)\n- [Endpoints](#endpoints)\n  - [POST /api/v2/memory/add](#post-apiv2memoryadd)\n  - [POST /api/v2/memory/flush](#post-apiv2memoryflush)\n  - [Asynchronous mode](#asynchronous-mode)\n  - [GET /api/v2/tasks/{task_id}](#get-apiv2taskstask_id)\n  - [GET /api/v2/tasks](#get-apiv2tasks)\n  - [POST /api/v2/memory/search](#post-apiv2memorysearch)\n  - [POST /api/v2/memory/get](#post-apiv2memoryget)\n  - [POST /api/v2/ome/trigger](#post-apiv2ometrigger)\n  - [Knowledge endpoints](#knowledge-endpoints)\n- [OpenAPI spec source](#openapi-spec-source)\n\n## Overview\n\n### Base URL and versioning\n\n| Setting | Default | Override |\n|---|---|---|\n| Host | `127.0.0.1` (loopback only) | `EVEROS_API__HOST` env var or `--host` flag |\n| Port | `8000` | `EVEROS_API__PORT` env var or `--port` flag |\n| Version prefix | `/api/v2` | — |\n\nBusiness endpoints live under `/api/v2/memory/`, `/api/v2/ome/`, and\n`/api/v2/knowledge/`. Knowledge endpoints have their own dedicated\nreference at [docs/knowledge.md](knowledge.md) and are cross-referenced\nbelow. The operational endpoints `GET /health`, `GET /metrics` and\n`POST /api/v2/cascade/quiesce` exist but are intentionally outside this\nreference — they are runtime probes and shutdown hooks for deployment, not\npart of the application contract. `quiesce` drains the md → index queue and\nstops the watcher; calling it twice is safe, and it returns a bare status body\n(`quiesced`, `drained`, `pending_before`, `pending_after`,\n`failed_permanent`), see `docs/openapi.json`. One note for anyone keying off\n`/health`: `capabilities.embed` reports whether the embedding service\nanswered a dimension probe **when the process started**, not whether it is\nreachable now -- a service that recovers later keeps reading `false` until\nthe next restart.\n\n`/api/v2` is the canonical prefix, aligned with the EverOS Cloud API. Every\nbusiness endpoint is **also** served under `/api/v1`, kept as a legacy\ncompatibility alias: the two prefixes resolve to the same handlers with\nidentical request/response contracts, so existing `/api/v1` integrations keep\nworking today. The alias carries no long-term guarantee — it may be removed in\na future major release, and it will be announced in the changelog with a\ndeprecation window before that happens. Write new integrations against\n`/api/v2`, and migrate existing ones when convenient.\n\n### Content type\n\nAll `POST` endpoints require `Content-Type: application/json`. Request\nand response bodies are UTF-8 JSON.\n\n### Authentication\n\nEverOS ships **no built-in authentication**. The server binds to\n`127.0.0.1` by default; place your own gateway or auth layer in front\nbefore exposing the API on any other interface. See\n[../SECURITY.md](../SECURITY.md) for the threat model.\n\n### Response envelope\n\nSuccessful responses from the business endpoints (`/memory/*`,\n`/knowledge/*`) wrap the payload in the envelope below (`200 OK`, or\n`201 Created` for a knowledge document upload). `POST /api/v2/ome/trigger`\nand the operational endpoints return their body bare — see each section.\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"data\": { /* endpoint-specific payload */ }\n}\n```\n\n`request_id` is generated server-side (32 lowercase hex chars) and is\nechoed by structured logs / tracing / metrics, so a single request can\nbe correlated end-to-end. Error responses carry their own envelope —\n`request_id` at the top level alongside a nested `error` object (not a\nbare FastAPI `detail`); see [Errors](#errors).\n\n### Eventual consistency\n\n`/add` and `/flush` write the markdown file (the source of truth)\n**synchronously** — when the call returns with `status: \"extracted\"`,\nthe new entry exists on disk. The configured vector / BM25 / scalar\nindex backend is rebuilt by the in-process **cascade coroutine\nasynchronously**.\n\nThat means `/search` and `/get` may not see a record immediately after\nthe `/flush` that produced it. Typical sync latency is sub-second, but\nunder load it can reach ~10–15 seconds. If you need read-your-write\nsemantics, retry with backoff. The markdown file is durable\nregardless; index lag never loses data.\n\nThe same applies one level up for everything an OME strategy produces\n**after** the extraction call returns: agent cases and skills (agent track),\nprofiles, atomic facts. `/flush` returns once the episode is on disk; the\n`extract_agent_case` strategy then runs its own LLM call, and skill\nclustering runs after that, so `/get` with `memory_type: \"agent_case\"` can\nlegitimately report `total_count: 0` for tens of seconds after a flush that\nreturned `\"extracted\"` (14 s for the case and 32 s for the skill on a\nlaptop with a hosted LLM). `GET /health`'s `cascade.pending` does **not**\ncover this window — it only tracks the md → index queue, which is empty\nwhile the strategy is still thinking. Poll `/get` with a budget instead.\nThe agent track also has extraction gates of its own, applied before any\nLLM call: a trajectory with fewer than three tool-call rounds is rejected as\ntoo simple (`AgentCaseExtractor(min_tool_call_rounds=3)`, the EverOS\ndefault — so a session with no tool use never yields a case), and so is one\nthat does not end on an assistant text message. Trajectories that pass\nthose gates can still be dropped by the extractor's own \"worth extracting\"\nLLM filter. EverOS logs the skip as `agent_case_skipped_by_algo` with the\nmemcell and session ids; the reason itself is on the adjacent everalgo log\nline, `skipping memcell (n_items=N): <reason> <detail>`. The same\n\"seconds later\" rule for every OME-produced kind is summarised in\n[how-memory-works.md](how-memory-works.md).\n\n### Conventions\n\n#### Timestamps\n\nEverOS runs a **two-zone discipline**: every stored byte is UTC; every\nrendered value carries the configured **display timezone**. The\ndisplay timezone is set by `EVEROS_MEMORY__TIMEZONE` (env var) or\n`[memory] timezone` (TOML); default `\"UTC\"`.\n\n| Direction | Format | Notes |\n|---|---|---|\n| **Request** — `messages[].timestamp` (`/add`) | Integer, **Unix epoch milliseconds (ms)** | Server auto-detects `>= 10^12` as ms for backward compat, but the contract is ms |\n| **Request** — `filters.timestamp.{gte,lt,…}` (`/search` / `/get`) | Integer, **Unix epoch ms** *or* ISO-8601 string | Same auto-detect; pick whichever you have |\n| **Response** — every `timestamp` field | ISO-8601 string with **explicit timezone offset** (`+HH:MM` or `Z`) | Always rendered in the configured display tz; never naive |\n\nWith the default `EVEROS_MEMORY__TIMEZONE=UTC` an episode produced\nat 11:30 UTC renders as `\"2026-05-28T11:30:36Z\"` (Pydantic\ncanonicalises `timezone.utc` to `Z`). Switch to\n`EVEROS_MEMORY__TIMEZONE=Asia/Shanghai` and the same UTC instant\nrenders as `\"2026-05-28T19:30:36+08:00\"` — bytes on disk are\nunchanged.\n\n**Fallback for naive input.** If you submit a `filters.timestamp` ISO\nstring without an offset (e.g. `\"2026-05-28T11:30:00\"`), the server\ntreats it as already display-tz-local before comparing against\nstorage. This is the same rule users see when reading rendered output:\n\"if you didn't say a zone, we assume your zone.\"\n\n**For internal architecture.** The storage / display split lives in\n`everos.component.utils.datetime`\n(`get_utc_now` / `ensure_utc` / `UtcDatetime` for storage;\n`get_now_with_timezone` / `to_display_tz` for display). See\n[datetime.md](datetime.md) for the design rationale.\n\n#### Other conventions\n\n- **Server-generated IDs** follow `<owner>_<kind>_<YYYYMMDD>_<NNN>`,\n  e.g. `alice_ep_20260528_00000001` for an episode, `alice_af_...`\n  for an atomic fact. See\n  [storage_layout.md §4](storage_layout.md) for the encoding.\n- **All endpoints are POST** for `/api/v2/memory/*` even when the\n  semantics look like a read (`/search`, `/get`) — the request bodies\n  are too rich (filters, methods, paging) to encode in a query string.\n\n### ScopeId: `app_id` and `project_id`\n\n`app_id` and `project_id` partition memory at the disk layer. Every\nwrite lands under\n`~/.everos/<app>/<project>/users/<user_id>/...` (or `.../agents/...`\nfor the agent track). The default scope materialises on disk as\n`default_app` / `default_project` (the `_app` / `_project` suffix is\nadded only for the literal id `\"default\"` so the default space stays\nvisually distinct from user-named scopes).\n\nA `/search` or `/get` query never crosses scopes — different\n`(app_id, project_id)` pairs are isolated.\n\nBoth fields share the same validation:\n\n| Setting | Value |\n|---|---|\n| Type | `string` |\n| Default | `\"default\"` |\n| Length | 1–128 chars |\n| Charset | `^[a-zA-Z0-9_.-]+$` |\n| Rejected literals | `\".\"` and `\"..\"` (path-traversal guard) |\n\n## Errors\n\nEvery non-2xx response uses a uniform error envelope — `request_id` at\nthe top level (mirroring the success envelope) alongside a nested\n`error` object. It is **not** a bare FastAPI `detail`:\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Document 'abc123' not found\",\n    \"timestamp\": \"2026-06-01T12:24:46+00:00\",\n    \"path\": \"/api/v2/knowledge/documents/abc123\"\n  }\n}\n```\n\n### error.code values\n\n`error.code` is a machine-readable `ErrorCode` enum. Clients can switch\non this value to decide retry / display / routing behaviour without\nparsing the human-readable `message` field.\n\n| `error.code` | HTTP | Retryable? | When |\n|---|---|---|---|\n| `NOT_FOUND` | `404` | No | Requested resource does not exist |\n| `CONFLICT` | `409` | No | Operation conflicts with existing state (e.g. duplicate document) |\n| `INVALID_INPUT` | `422` | No | Request-body validation failure. Also covers `/search` / `/get` filter-DSL compile errors — the compile reason rides in `message` — and, on `/add` and `/flush`, a session whose buffered messages the model could not segment on three consecutive calls: the buffer is moved to the `quarantine` track of `unprocessed_buffer` so the session can continue, and `message` names the session and the number of messages set aside |\n| `EXTRACTION_EMPTY` | `422` | No | Knowledge document parsed but the extractor produced no topics. An empty or whitespace-only upload is rejected earlier as `INVALID_INPUT` (`\"Uploaded file has no valid content.\"`) |\n| `BAD_REQUEST` | `400` | No | Path traversal attempt or other malformed input |\n| `UNSUPPORTED_FORMAT` | `415` | No | File format or modality not supported (e.g. unsupported `ContentItem` type, missing `ext` for `base64`) |\n| `PAYLOAD_TOO_LARGE` | `413` | No | An `async_mode` request body exceeds `memorize.async_queue.max_payload_bytes`; the batch was not accepted |\n| `QUEUE_SATURATED` | `429` | Yes | The memorize queue is at its per-session or global cap; the batch was not accepted — retry later or keep it client-side |\n| `EXTERNAL_SERVICE_UNAVAILABLE` | `503` | **Yes** | An external service (LLM, embedding, rerank) returned an error or timed out |\n| `CAPABILITY_UNAVAILABLE` | `503` | No | A required server-side capability is missing (e.g. `everos[multimodal]` extra not installed, LibreOffice absent) — requires admin action, not retry |\n| `CONFIGURATION_ERROR` | `500` | No | A required configuration is missing or invalid (e.g. embedding model not set) |\n| `INTERNAL_ERROR` | `500` | No | Unhandled exception (internal details are logged, never leaked) |\n\n### error object\n\n| Field | Type | Description |\n|---|---|---|\n| `code` | `string` | One of the `ErrorCode` values listed above |\n| `message` | `string` | Human-readable reason. For `INVALID_INPUT` from request validation, **only the first** validation error is surfaced, formatted `\"<msg>: <dotted-loc>\"` with the leading `body` segment stripped (e.g. `\"Field required: messages\"`); a model-level validator with no field location surfaces just `\"<msg>\"` (e.g. `\"Value error, exactly one of user_id / agent_id must be provided\"`) |\n| `timestamp` | `string` | ISO-8601 with timezone offset (display tz) |\n| `path` | `string` | Request path, e.g. `/api/v2/memory/add` |\n\n> Unlike FastAPI's default, the full per-field validation array is **not**\n> returned — only the first error's message. A client that needs the\n> offending field can read the `<loc>` suffix in `message`.\n\n## Common types\n\n### MessageItem\n\nOne turn in a `/add` batch. Shape mirrors the OpenAI Chat Completions\nmessage structure plus a stable `sender_id` for indexing.\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `sender_id` | `string` | yes | — | `minLength=1` |\n| `sender_name` | `string \\| null` | no | `null` | — |\n| `role` | `\"user\" \\| \"assistant\" \\| \"tool\"` | yes | — | — |\n| `timestamp` | `integer` | yes | — | `> 0` — **Unix epoch milliseconds (ms)** per v1 contract |\n| `content` | `string \\| array<ContentItem>` | yes | — | — |\n| `tool_calls` | `array<ToolCall> \\| null` | no | `null` | — |\n| `tool_call_id` | `string \\| null` | no | `null` | — |\n\n**`sender_id`** — Stable identifier of the entity producing this turn.\nFor `role: \"user\"`, this is the `user_id` the server will index the\nextracted memory under: markdown lands at\n`users/<sender_id>/episodes/...`, and `/search` / `/get` queries with\n`user_id: \"<sender_id>\"` reach it. For `role: \"assistant\"` or\n`\"tool\"`, the field is informational (the LLM sees it during\nextraction but it is not used as an indexing key).\n\n**`sender_name`** — Optional human-readable display name. Lets the LLM\nuse a real name during extraction without changing the indexing key.\nFor example, set `sender_id: \"u_42\"` (stable internal id) and\n`sender_name: \"Alice\"` (what the LLM sees).\n\n**`role`** — Speaker role; one of:\n- `\"user\"` — content originates from a human (or human-acting client).\n- `\"assistant\"` — content from the AI assistant.\n- `\"tool\"` — output of a tool call; pair with `tool_call_id`.\n\n**`timestamp`** — Wall-clock anchor in **Unix epoch milliseconds (ms)**\nper the v1 API contract. Used by extraction to anchor the resulting\nepisode and by the daily-log writer to bucket the entry into the right\nfile (`episode-<YYYY-MM-DD>.md` etc.).\n\n> Implementation note: the algo layer auto-detects seconds vs ms (values\n> `>= 10^12` are treated as ms, smaller as seconds) for backward compat,\n> but **clients should send ms** to honour the contract.\n\n**`content`** — The message body.\n- A bare **string** is shorthand for a single text content item.\n- An **array of `ContentItem`** is for mixed-modality input (text +\n  image / pdf / audio / ...); non-text items are parsed by the\n  multimodal LLM configured via `EVEROS_MULTIMODAL__*` env vars. See\n  [ContentItem](#contentitem).\n\n**`tool_calls`** — When `role: \"assistant\"`, the tool calls the\nassistant emitted in this turn (OpenAI Chat Completions shape).\n\n**`tool_call_id`** — When `role: \"tool\"`, the `id` of the call this\nmessage is the response to. Required for `role: \"tool\"`; a tool row\nwithout it is rejected with `422`.\n\n### ContentItem\n\nMixed-modality message-body element. Carry the payload in exactly one\nof `text` / `uri` / `base64`; the others must be `null`. For\n`type: \"text\"` use `text`; for every **non-text** type use `uri`\n(`http(s)://`) or `base64` (with `ext`). Non-text items are routed\nthrough the multimodal parser, which needs a fetchable or decodable\npayload — a non-text item carrying only `text` returns `415`.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `type` | `\"text\" \\| \"image\" \\| \"audio\" \\| \"doc\" \\| \"pdf\" \\| \"html\" \\| \"email\"` | yes | — | — |\n| `text` | `string \\| null` | no | `null` | Required when `type: \"text\"` |\n| `uri` | `string \\| null` | no | `null` | `http(s)://` (fetched server-side) or `file://` (read from the server's local fs, guardrailed) pointer |\n| `base64` | `string \\| null` | no | `null` | Inline payload, plain base64 (no `data:` prefix) |\n| `ext` | `string \\| null` | no | `null` | File-extension hint when `uri` lacks one |\n| `name` | `string \\| null` | no | `null` | Display filename, used in logs |\n| `extras` | `object \\| null` | no | `null` | Provider-specific metadata, opaque to EverOS |\n\n**`type`** — The content kind. Each non-text type is dispatched to the\nmultimodal LLM. If the multimodal endpoint cannot handle the supplied\npayload, `/add` returns `415 Unsupported Media Type`.\n\n**`text`** — The literal text payload; valid **only** for\n`type: \"text\"`. A non-text type (including `\"html\"`) is always routed\nto the parser and must carry `uri` or `base64`; passing only `text` on\na non-text item returns `415`. To inline HTML as plain text, send it\nas `type: \"text\"`.\n\n**`uri`** — `http(s)://` or `file://` pointer to the asset. An\n`http(s)` uri is fetched by the server and dispatched by the response\nContent-Type (use it for assets hosted elsewhere — S3 / OSS presigned\nURL, http server). A `file://` uri is read from the **server's** local\nfilesystem (the path must be reachable by the server process), subject\nto these guardrails:\n\n- the resolved path (symlinks followed) must be an existing regular file;\n- its size must be ≤ `EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES` (default 50 MiB);\n- when `EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS` is set, the path must lie\n  within one of the listed roots (unset = any readable file, the\n  local-first default).\n\nA guardrail violation surfaces as `415`. Mutually exclusive with `base64`.\n\n**`base64`** — Inline binary payload, base64-encoded (no `data:`\nprefix). Mutually exclusive with `uri`.\n\n> **Size caution.** base64 inflates the payload ~4/3× on the wire, and\n> the encoded blob is held **verbatim in the server's staging buffer\n> (SQLite) from `/add` until the session is flushed** — a multi-MB PDF\n> becomes multi-MB of SQLite text for the buffer's lifetime. It is *not*\n> persisted past extraction (the memory cell and episode store only the\n> parsed text, never the raw bytes), but the transient footprint is\n> real, and a large inline blob also slows request parsing. **Prefer\n> `uri` (`http(s)://`) for large assets**: the buffer then holds only\n> the URL plus the parsed text, and the bytes are fetched transiently at\n> parse time rather than stored. Reserve `base64` for small assets or\n> when no reachable URL exists.\n\n**`ext`** — File-extension hint (`\"pdf\"`, `\"png\"`, `\"html\"`, ...). For\n`base64` payloads this drives modality dispatch and is effectively\n**required** (without it the server falls back to `mime`, then `415`s\nif neither resolves). For `uri` payloads it is optional — the fetched\nContent-Type usually suffices.\n\n**`name`** — Filename / human label for logging and traceability.\nDoes not affect parsing.\n\n**`extras`** — Free-form bag of provider-specific metadata (e.g.\ncaption, page hints). Opaque to EverOS; passed through to the\nmultimodal LLM context.\n\n### ToolCall\n\nOpenAI-shaped tool invocation attached to an assistant turn.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `id` | `string` | yes | — | Stable id; echoed by the paired `role: \"tool\"` turn via `tool_call_id` |\n| `type` | `string` | no | `\"function\"` | Only `\"function\"` is meaningful today |\n| `function` | [ToolFunction](#toolfunction) | yes | — | The function being called |\n\n### ToolFunction\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `name` | `string` | yes | Function name as registered with the agent |\n| `arguments` | `string` | yes | **JSON-encoded string** (OpenAI convention — not an object) |\n\n### FilterNode (filter DSL)\n\nA recursive boolean tree of predicates. Used by `/search.filters` and\n`/get.filters`. The Pydantic envelope only checks the recursive\ncombinator shape; field-level validity (which scalar fields are\nfilterable, which operators apply, value coercion) runs when the\nnode is compiled to a backend-specific filter clause server-side. Compile\nerrors surface as `422` with the offending field / operator in\n`error.message`.\n\n#### Shape\n\nA node is a JSON object whose keys are one of:\n\n| Key | Value | Notes |\n|---|---|---|\n| `AND` | `array<FilterNode>` | All child nodes must match. Omit if not needed |\n| `OR` | `array<FilterNode>` | At least one child node must match. Omit if not needed |\n| *<allowed field>* | scalar or operator map | Predicate on that field — see [Allowed fields](#allowed-fields) and [Operators](#operators) |\n\n`AND`, `OR`, and scalar predicates **mix freely at the same level**;\nthey are implicitly joined with `AND`. A node with only scalar keys is\nfine — `AND` / `OR` arrays are not required.\n\nExample — `session_id == \"demo-001\"` AND (`sender_id` contains `\"alice\"` OR `\"bob\"`):\n\n```json\n{\n  \"session_id\": \"demo-001\",\n  \"OR\": [\n    { \"sender_id\": \"alice\" },\n    { \"sender_id\": \"bob\" }\n  ]\n}\n```\n\n<a id=\"filter-allowed-fields\"></a>\n\n#### Allowed fields\n\nThese are the only field names that may appear as a predicate key.\nAnything else returns `422`.\n\n| Field | Column kind | Value type | Notes |\n|---|---|---|---|\n| `session_id` | string | `string` | Session the row was extracted from |\n| `parent_type` | string | `string` | Parent kind, e.g. `\"memcell\"` |\n| `parent_id` | string | `string` | Parent id |\n| `timestamp` | timestamp | `integer` (Unix epoch **ms** per v1 contract) or `string` (ISO-8601) | Implementation auto-detects `>= 10^12` as ms / smaller as sec for backward compat, but the contract is ms |\n| `sender_id` | array of strings | `string` | Matches via `array_has(sender_ids, <value>)` — checks whether the row's `sender_ids` list contains the given id |\n\nThe following names are **reserved** and rejected inside `filters`.\nThey must be set at the top of the request (`/search` or `/get`),\nnot inside the DSL:\n\n```\nowner_id   owner_type   app_id   project_id\n```\n\n<a id=\"filter-operators\"></a>\n\n#### Operators\n\nA predicate value may be a **scalar shorthand** (equality) or an\n**operator map** (multiple operators in one dict are AND-joined):\n\n| Operator | SQL | Applies to |\n|---|---|---|\n| `eq` | `=` | string, timestamp; on `sender_id` becomes `array_has(...)` |\n| `ne` | `!=` | string, timestamp |\n| `gt` | `>` | timestamp |\n| `gte` | `>=` | timestamp |\n| `lt` | `<` | timestamp |\n| `lte` | `<=` | timestamp |\n| `in` | `IN (...)` | string (list of values); on `sender_id` becomes `OR` of `array_has` |\n\nExamples:\n\n```json\n// Equality shorthand\n{ \"session_id\": \"demo-001\" }\n\n// Operator map (operators inside one dict are AND-joined)\n{ \"timestamp\": { \"gte\": 1748390400000, \"lt\": 1748400000000 } }\n\n// IN list\n{ \"session_id\": { \"in\": [\"demo-001\", \"demo-002\"] } }\n\n// Array-membership on sender_ids\n{ \"sender_id\": \"alice\" }                       // array_has(sender_ids, 'alice')\n{ \"sender_id\": { \"in\": [\"alice\", \"bob\"] } }    // OR of two array_has\n```\n\n### SearchMethod\n\n| Value | Behaviour |\n|---|---|\n| `\"keyword\"` | BM25 only — pure lexical match, no embedding cost |\n| `\"vector\"` | Dense vector ANN only — semantic recall, no lexical |\n| `\"hybrid\"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter against the configured derived index backend |\n| `\"agentic\"` | Iterative cluster-path retrieval driven by a cross-encoder rerank loop; higher quality at higher latency / cost |\n\n`\"hybrid\"` is the default because it balances recall and precision\nwithout requiring the agentic loop. `\"agentic\"` calls the LLM in a loop\nand should be reserved for offline or background workflows.\n\n### GetMemoryType\n\n| Value | Track | Returned in `data.<plural>` |\n|---|---|---|\n| `\"episode\"` | user | `data.episodes` — [GetEpisodeItem](#getepisodeitem) |\n| `\"profile\"` | user | `data.profiles` — [GetProfileItem](#getprofileitem) |\n| `\"agent_case\"` | agent | `data.agent_cases` — [GetAgentCaseItem](#getagentcaseitem) |\n| `\"agent_skill\"` | agent | `data.agent_skills` — [GetAgentSkillItem](#getagentskillitem) |\n\n`memory_type` must match the requested owner kind: `\"episode\"` /\n`\"profile\"` require `user_id`; `\"agent_case\"` / `\"agent_skill\"`\nrequire `agent_id`. The mismatching combinations are rejected with\n`422`.\n\n## Endpoints\n\n### POST /api/v2/memory/add\n\nAppend a batch of messages to a session buffer. The server\naccumulates messages until the boundary detector decides the session\nis complete (or you force it with `/flush`); on boundary the LLM\nextracts a memory cell and the writers persist markdown + index rows.\n\nThe response distinguishes \"buffered\" from \"extracted this call\" via\nthe `status` field — see [Response body](#response-body) below.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `messages` | `array<MessageItem>` | yes | — | 1–500 items |\n| `defer_extraction` | `boolean` | no | `false` | buffer durably without boundary or extraction LLM calls |\n| `async_mode` | `boolean` | no | `false` | queue the batch and return `202` immediately; see [Asynchronous mode](#asynchronous-mode) |\n\n**`session_id`** — Identifies the conversation buffer on the server.\nMessages POSTed with the same `(session_id, app_id, project_id)`\naccumulate into one extraction batch. Different `session_id`s never\nshare a buffer, even within the same scope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n**`messages`** — Ordered list of [MessageItem](#messageitem) to\nappend. Order matters (it is preserved when the buffer is later\nextracted). The 1–500 cap is a per-request safety bound, not a\nsession lifetime cap — you can call `/add` many times for the same\n`session_id`.\n\n**`defer_extraction`** — When `true`, `/add` only merges the messages\ninto the durable SQLite `unprocessed_buffer`. It skips boundary\ndetection, memcell creation, and all extraction pipelines, so batching\nclients can capture every turn cheaply and call `/flush` after an idle\nwindow, a size threshold, or a session switch. A successful deferred\nadd always reports `status: \"accumulated\"`.\n\n**`async_mode`** — When `true`, `/add` persists the batch to the durable\nmemorize queue and returns `202 Accepted` without running boundary\ndetection or extraction; a background worker delivers queued batches in\narrival order per session, several sessions at a time. See\n[Asynchronous mode](#asynchronous-mode) for the `202` body, the ordering\nrule and how to poll progress.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `status` | `\"accumulated\" \\| \"extracted\"` | What the server did with the batch |\n\n**`message_count`** — Always equal to `len(messages)` on a `200` —\nthere is no partial accept. The field is present mainly for log\ncorrelation.\n\n**`status`** — Two-state outcome:\n- `\"accumulated\"` — Messages were added to the buffer; the boundary\n  detector did **not** trigger this call. The buffer remains open.\n- `\"extracted\"` — During this call the boundary detector tripped; the\n  LLM ran extraction and the writers persisted at least one memory\n  cell. The buffer is empty after the call returns.\n\n`202 Accepted` — only with `async_mode`, or when the session already has\nqueued batches (see [Asynchronous mode](#asynchronous-mode)) — returns a\nSuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"queued\"` | The batch is persisted; delivery happens in the background |\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `pending` | `integer` | Unfinished queued batches of this session, this one included |\n\nThe envelope's `request_id` is the handle for\n[`GET /tasks/{task_id}`](#get-apiv2taskstask_id).\n\n#### cURL example\n\n```bash\nTS=$(( $(date +%s) * 1000 ))\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/add \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"session_id\\\": \\\"demo-002\\\",\n    \\\"app_id\\\": \\\"default\\\",\n    \\\"project_id\\\": \\\"default\\\",\n    \\\"messages\\\": [\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $TS, \\\"content\\\": \\\"I love climbing in Yosemite every spring.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+10)), \\\"content\\\": \\\"My favorite coffee shop is Blue Bottle in SOMA.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+20)), \\\"content\\\": \\\"I bike to work most days.\\\"}\n    ]\n  }\"\n```\n\nResponse (real capture):\n\n```json\n{\n    \"request_id\": \"ae78d3f689c941eea135893e702fd171\",\n    \"data\": {\n        \"message_count\": 3,\n        \"status\": \"accumulated\"\n    }\n}\n```\n\n### POST /api/v2/memory/flush\n\nForce the boundary detector to decide **now** for the given session\nbuffer. The LLM runs extraction (one call) regardless of whether the\nheuristic would have tripped on its own. Useful at the end of a chat\nor agent run to make sure pending context becomes durable memory.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n\n**`session_id`** — Identifies which buffer to flush. Must match the\n`session_id` of prior `/add` calls in the same `(app_id, project_id)`\nscope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"extracted\" \\| \"no_extraction\"` | What happened |\n\n**`status`**:\n- `\"extracted\"` — The buffer had at least one message; extraction ran\n  and produced at least one memory cell.\n- `\"no_extraction\"` — The buffer was empty (no prior `/add` calls for\n  this `(session_id, app_id, project_id)`, or it was already flushed).\n\n`202 Accepted` with the same body as `/add` (`message_count` is `0`) only\nwhen the session already has queued batches, so the flush runs after them;\n`/flush` has no `async_mode` of its own. See\n[Asynchronous mode](#asynchronous-mode).\n\nA `200` from `/flush` is synchronous with respect to markdown persistence: by the\ntime the response returns, the new entry is on disk. Derived index\nsync is still asynchronous — see\n[Eventual consistency](#eventual-consistency).\n\n#### cURL example\n\n```bash\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/flush \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"session_id\":\"demo-002\",\"app_id\":\"default\",\"project_id\":\"default\"}'\n```\n\nResponse (real capture; this call took several seconds because of the\nextraction LLM call):\n\n```json\n{\n    \"request_id\": \"e65bcf4f56c042e39cdf50866810672c\",\n    \"data\": {\n        \"status\": \"extracted\"\n    }\n}\n```\n\n### Asynchronous mode\n\n`async_mode: true` on `/add` turns the call into \"accept and return\": the\nrequest body is written to the SQLite `memorize_inbox` table in one\ntransaction and the response is `202 Accepted`. `/flush` has no\n`async_mode` (the Cloud API has no asynchronous flush either): it runs\ninline, except that a flush on a session with queued batches joins the\nqueue behind them and answers `202` (see Ordering). An in-process\nworker then delivers queued batches to the same code path a synchronous\ncall uses: each session's batches in the order they were accepted, several\nsessions at a time. The field name, the `202` body\n(`status: \"queued\"` plus the envelope's `request_id`) and the status\nvocabulary (`queued` / `processing` / `success` / `failed`) match the\nEverOS Cloud gateway, so one client can talk to both.\n\n**Ordering.** Batches of one session are delivered strictly in arrival\norder, and a `/flush` runs after every `/add` accepted before it. To keep\nthat true, a synchronous call (`async_mode` omitted or `false`) on a\nsession that still has queued batches is queued too and answered with\n`202` — otherwise it would overtake work the client was already told is\naccepted. Clients that never use `async_mode` never see this: their\nsessions never have queued batches. Sessions are independent of each\nother: up to `max_concurrency` batches (default 4) run at once, never two\nfrom one session, so one slow session does not hold up the others, and\nthere is no ordering between sessions. Like the Cloud consumer, which runs\ngroups in parallel and each group in order.\n\n**Backpressure.** The queue is bounded. A batch is rejected before it is\nstored when:\n\n| Condition | HTTP | `error.code` | Default limit |\n|---|---|---|---|\n| Serialised body too large | `413` | `PAYLOAD_TOO_LARGE` | 4 MiB |\n| Session has too many unfinished batches | `429` | `QUEUE_SATURATED` | 50 |\n| Process has too many unfinished batches | `429` | `QUEUE_SATURATED` | 200 |\n\nThe caps are checked before the insert without a lock, so a burst of\nconcurrent requests can overshoot them by a few rows. A rejected batch was\n**not** accepted: retry later or keep it client-side.\n`pending` in every `202` body lets a client slow down before hitting `429`.\nLimits are configured under `[memorize.async_queue]`, see\n[configuration.md](configuration.md#memorizeasync_queue).\n\n**Failure.** A batch whose delivery raises is marked `failed` and skipped\nso its session keeps moving; `GET /tasks/{request_id}` shows `error`.\nThere is no automatic retry by default (`max_attempts = 1`), because the\nmemorize pipeline is not atomic: boundary detection writes the memcell and\nconsumes the session buffer before extraction runs. A failure before that\npoint (the common one: the boundary LLM call failed) leaves nothing behind,\nand re-sending the batch is safe. A failure after it leaves the memcell\nwithout its episode; re-sending then cuts a second memcell, and a `/flush`\nfinds an empty buffer. `last_error` does not say which step failed. The\nserver never re-drives a failed batch and deletes its row with the others\nafter seven days. Synchronous calls have the same property; they just hand\nthe decision to the client as a 5xx.\n\n**Delivery is at-least-once across a crash.** A row that was in flight\nwhen the process died is delivered again on the next start. If the first\ndelivery had already written its memcell, an `/add` replay duplicates that\nmemory; a `/flush` replay finds an empty buffer and does nothing, so a crash\nthat fell between the memcell write and the episode write leaves that\nmemcell without its episode. A\nnormal shutdown waits up to 30 seconds for the rows in flight (up to\n`max_concurrency` of them, one per session); a row that takes longer, or a\nprocess manager that kills earlier (`docker stop` sends SIGKILL after 10\nseconds by default), is replayed the same way, so one restart can replay\nup to `max_concurrency` batches. So is a\nbatch whose delivery had just failed when the process died before its\n`failed` mark was written, and a batch whose status write was lost (see\nStuck rows) if the server restarts before the row is marked. Replays are\nbounded: a batch that has been replayed `max_replays` times (default 3)\nwithout ever finishing is marked `failed` with `error_code`\n`replays_exhausted` instead of being requeued again, so a batch that kills\nthe process cannot crash-loop it.\n\n**Operations.** `GET /health` carries a `memorize_queue` block: `healthy`\n(the delivery loop is alive), `pending`, `oldest_pending_age_s`, `failed`,\n`loop_restarts` and `last_loop_error_at`. The probe only observes: a loop\nthat dies relaunches itself (with backoff) and is relaunched by the next\n`/add`, and `loop_restarts` counts those. Alert on `healthy` and on\n`loop_restarts` changing; a growing `oldest_pending_age_s` with a live loop\nmeans delivery is slower than arrival.\n\n**Retention.** Every queue row, whatever its status, is deleted seven days\n(`row_retention_hours`) after it was created. `GET /tasks/{task_id}` returns `404`\nafter that. A delivered row drops its request body at once. A `failed` row\nkeeps its body (in `memorize_inbox.payload_json`, found by `request_id`)\nuntil purged, and failed rows are not capped: an outage in which every batch\nfails keeps every body accepted during it for the retention period. SQLite\nreuses the freed pages but does not shrink `system.db`. A row that ages out\nwhile still `queued` was never delivered; the purge logs a warning when that\nhappens.\n\n**Stuck rows.** A row left `processing` because its status write was lost\n(SQLite busy past its timeout, disk full) is marked `failed` before the\nworker claims its next batch, or at its next scheduling pass if that write\nfails too, so a session's `pending` does not stay non-zero because of it;\nbatches the worker is still running are never swept. Its `last_error`\nreads `status write lost; outcome unknown`:\nthe batch may well have been delivered in full (the lost write is usually\nthe `success` one), so check the session's memories before re-sending it.\nIf the server restarts before that mark lands, the row is replayed like a\ncrash (see above).\n\nSetting `[memorize.async_queue] enabled = false` closes the door:\n`async_mode` is accepted but ignored and new calls run synchronously unless\ntheir session still has queued rows. Rows accepted while it was on are still\ndelivered by the worker, and a session that still has such rows still queues\nits synchronous calls behind them, so no drain is needed before switching.\nDowngrading to a release without `async_mode` is different: queued rows are\nneither delivered nor deleted there. Set `enabled = false` and restart first,\nso `async_mode` requests stop adding rows (a synchronous call still queues\nbehind its session's backlog until it drains); wait until\n`select count(*) from memorize_inbox where status in ('queued', 'processing')`\non `system.db` is 0; then stop and downgrade.\n\n### GET /api/v2/tasks/{task_id}\n\nThe outcome of one accepted request. The path and the body are the EverOS\nCloud gateway's (`GET /api/v2/tasks/{task_id}`), so a client polls both the\nsame way; `task_id` is the `request_id` a `202` carried.\n\n`200 OK`, or `404 NOT_FOUND` when the row never existed or has aged out of\n`row_retention_hours`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | The request id |\n| `status` | `\"queued\" \\| \"processing\" \\| \"success\" \\| \"failed\"` | Where the batch is. Cloud can also report `pending`; this build never does |\n| `task_type` | `\"memory_add\" \\| \"memory_flush\"` | — |\n| `error` | `string \\| null` | Summary of the failure, if any |\n| `error_code` | `string \\| null` | `status_write_lost` for a row the sweep failed, `replays_exhausted` for one replayed `max_replays` times without finishing, otherwise the exception type that failed the batch |\n| `created_at` | `string` (ISO 8601) | Accepted at |\n| `finished_at` | `string \\| null` | Set once `status` is `success` or `failed` |\n\n### GET /api/v2/tasks\n\nAccepted requests, newest first, one page at a time. Mirrors the Cloud\ngateway's listing.\n\n#### Query parameters\n\n| Parameter | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `session_id` | `string` | no | — | Only this session's requests |\n| `app_id` / `project_id` | `string` | no | `\"default\"` | Scope for `session_id` |\n| `status` | `string` | no | — | One of the four statuses; anything else is `422 INVALID_INPUT` |\n| `page` | `integer` | no | `1` | `≥ 1` |\n| `page_size` | `integer` | no | `20` | `1..100`, larger values are capped |\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `total` | `integer` | Rows matching the filters, all pages |\n| `page` / `page_size` | `integer` | As requested (`page_size` after capping) |\n| `items` | `array<TaskItem>` | The page, newest first; fields as in `GET /tasks/{task_id}` |\n\nDrain a session before exiting by polling `?session_id=X&status=queued` and\n`?session_id=X&status=processing` until both `total` are `0`. That means\nevery batch reached the memorize pipeline (unless rows aged out before\ndelivery, see Retention); it does not mean every message is an episode yet:\nmessages still in the session buffer wait for the boundary detector or the\nnext `/flush`.\n\n### POST /api/v2/memory/search\n\nHybrid retrieval over the memory store. Combines BM25, dense vector\nANN, optional scalar filtering, optional cross-encoder rerank, and\noptional final LLM rerank. Returns ranked items grouped by kind.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `user_id` | `string \\| null` | XOR with `agent_id` | `null` | `minLength=1` if set |\n| `agent_id` | `string \\| null` | XOR with `user_id` | `null` | `minLength=1` if set |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `query` | `string` | yes | — | `minLength=1` |\n| `method` | [SearchMethod](#searchmethod) | no | `\"hybrid\"` | — |\n| `top_k` | `integer` | no | `-1` | `-1` or `1..100` |\n| `radius` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `min_score` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `include_profile` | `boolean` | no | `false` | — |\n| `enable_llm_rerank` | `boolean` | no | `false` | — |\n| `filters` | [FilterNode](#filternode-filter-dsl) `\\| null` | no | `null` | — |\n\n**`user_id` / `agent_id`** — **Exactly one** must be set. Determines\nwhich track is searched: `user_id` → user-memory (episodes /\nprofiles); `agent_id` → agent-memory (cases / skills).\n\n**`app_id` / `project_id`** — Scope identifiers; results never cross\nscopes.\n\n**`query`** — The retrieval query. The same string is fed to BM25\ntokenization and to the embedding model, then the two recall lists\nare fused.\n\n**`method`** — Retrieval method; see [SearchMethod](#searchmethod).\nDefault `\"hybrid\"` is the recommended starting point.\n\n**`top_k`** — Maximum number of items to return per kind. Two modes:\n- `-1` (default) — \"Use server defaults.\" Recall falls back to a\n  fixed internal cap, and a server-side `radius` default kicks in if\n  the caller did not pass one (see `radius` below). Use for \"give me\n  whatever you find worth returning.\"\n- `1..100` — Explicit cap. Recall is sized as `top_k × multiplier`\n  and **no** default `radius` is applied unless you set it yourself.\n\nValues of `0` or outside `-1` / `1..100` are rejected with `422`.\n\n**`radius`** — Optional **cosine-similarity threshold** in `[0.0, 1.0]`.\nCandidates whose score is below this value are dropped — use it to cut\na long tail of weak matches. Three-level fallback for the effective\nradius:\n\n1. Caller-supplied `radius` (including the literal `0.0`) always wins.\n2. With `top_k=-1` and no caller-supplied `radius`, a server-side\n   default radius kicks in.\n3. With `top_k>0` and no caller-supplied `radius`, no threshold is\n   applied (`null`).\n\n**`min_score`** — Optional **post-fusion relevance floor** in\n`[0.0, 1.0]`. Results below this score are evicted after fusion,\nindependent of `radius` (which is a per-recall cosine threshold).\n\n**`include_profile`** — When `user_id` is set, also fetch the user's\nprofile and include it in `data.profiles`. The profile is not\nranked; `score` is `null`. Ignored when `agent_id` is set.\n\n**`enable_llm_rerank`** — Opt-in LLM rerank pass for\n`method: \"hybrid\"`. Applies to `agent_case` and `agent_skill` fusion\nonly; the episode hybrid path has built-in fact eviction and\nignores this flag. Adds one LLM call per request. Ignored by\n`keyword` / `vector` (no fusion to rerank) and `agentic` (uses its\nown cross-encoder loop).\n\n**`filters`** — Optional filter-DSL node; see\n[FilterNode](#filternode-filter-dsl). Applied **before** ranking, so\nit does not perturb the ranker.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All five\narrays are always present so client code can iterate without\nbranching on owner type; arrays that do not apply to the requested\nowner kind stay as `[]`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set |\n| `profiles` | `array<SearchProfileItem>` | Populated when `user_id` is set **and** `include_profile=true` |\n| `agent_cases` | `array<SearchAgentCaseItem>` | Populated when `agent_id` is set |\n| `agent_skills` | `array<SearchAgentSkillItem>` | Populated when `agent_id` is set |\n| `unprocessed_messages` | `array<UnprocessedMessageDTO>` | Populated **only** when `filters.session_id` is a top-level eq scalar; otherwise stays `[]`. Independent of `user_id` / `agent_id` (buffer rows have no owner attribution — boundary detection runs before owner inference) |\n\n#### SearchEpisodeItem\n\nUser-track conversation episode hit. `score` is the fused retrieval\nscore; `atomic_facts` lists single-sentence facts that matched the\nquery within this episode (already nested, no separate call needed).\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_ep_<YYYYMMDD>_<NNN>` |\n| `user_id` | `string \\| null` | Owner of this episode |\n| `app_id` | `string` | Scope where the episode lives |\n| `project_id` | `string` | Scope where the episode lives |\n| `session_id` | `string` | The `session_id` whose messages produced this episode |\n| `timestamp` | `string` | ISO-8601 with timezone offset; default `UTC` renders as `Z`, non-UTC defaults render as `±HH:MM` — see [Conventions](#conventions) |\n| `sender_ids` | `array<string>` | Distinct `sender_id`s in the underlying messages |\n| `summary` | `string` | Short summary (~200 chars), suitable for list-view rendering |\n| `subject` | `string` | One-line subject / title |\n| `episode` | `string` | Full extracted narrative |\n| `type` | `\"Conversation\"` | Reserved; today only conversation-derived episodes ship |\n| `score` | `number` | Fused retrieval score for this episode |\n| `atomic_facts` | `array<SearchAtomicFactItem>` | Sub-facts extracted from the same episode that matched the query |\n\n> **`id` is unique within `(app_id, project_id)`.** The same `id` can appear\n> under two projects (each project keeps its own daily files and sequence);\n> address a row globally by `(app_id, project_id, id)`. Within one response\n> ids never repeat, because a request is always scoped to one project.\n\n#### SearchAtomicFactItem\n\nA single-sentence fact pulled out of an episode during extraction.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_af_<YYYYMMDD>_<NNN>` |\n| `content` | `string` | The fact as a single sentence |\n| `score` | `number` | Same score scale as the parent episode |\n\n#### SearchProfileItem\n\nUser profile. Only populated when `include_profile=true` and\n`user_id` is set.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Profile id |\n| `user_id` | `string \\| null` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `profile_data` | `object` | Free-form structured profile fields produced by extraction (schema is profile-specific) |\n| `score` | `number \\| null` | `null` for direct fetches (no query-aware profile ranking yet) |\n\n#### SearchAgentCaseItem\n\nAgent-track case hit. Returned only when the request uses `agent_id`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_ac_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `session_id` | `string` | The agent run this case came from |\n| `task_intent` | `string` | What the agent was trying to do (one-sentence framing) |\n| `approach` | `string` | How the agent went about it |\n| `quality_score` | `number` | Quality assessed at extraction time, in `[0, 1]` |\n| `key_insight` | `string \\| null` | One-line takeaway; `null` if extraction did not produce one |\n| `timestamp` | `string` | ISO-8601 UTC of when the run happened |\n| `score` | `number` | Fused retrieval score for this case |\n\n#### SearchAgentSkillItem\n\nAgent-track skill hit — a named procedural memory produced by\nclustering related cases.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_sk_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `name` | `string` | Skill name (a stable, human-meaningful slug) |\n| `description` | `string` | One-line description |\n| `content` | `string` | Full skill body (procedure / playbook text) |\n| `confidence` | `number` | Extraction confidence in `[0, 1]` |\n| `maturity_score` | `number` | Maturity assessed at clustering time, in `[0, 1]` |\n| `source_case_ids` | `array<string>` | `agent_case` ids that contributed to this skill |\n| `score` | `number` | Fused retrieval score for this skill |\n\n#### UnprocessedMessageDTO\n\nA raw message still sitting in the boundary-detection buffer — sent to\n`/add` but not yet carved into an episode / case. Returned **only**\nwhen the request's `filters` contains `session_id` as a top-level eq\nscalar (`{\"session_id\": \"<sid>\"}`); compound shapes (`AND` / `OR`\ncombinators, operator maps such as `{\"eq\": ...}` / `{\"in\": ...}`) do\nnot trigger the lookup because there is no defensible buffer-scope\nmapping for them. Buffer rows have no `user_id` / `agent_id`\nattribution, so `session_id` is the only meaningful query dimension.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Original `message_id` from `/add` |\n| `app_id` | `string` | Scope where the buffered message lives |\n| `project_id` | `string` | Scope where the buffered message lives |\n| `session_id` | `string` | Matches the requested `filters.session_id` |\n| `sende\n\n<!-- [TRUNCATED AT 50KB] -->","isInternal":false,"tokens":13490,"sizeBytes":50304},{"name":"architecture.md","path":"docs/architecture.md","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/docs/architecture.md","title":"docs - System Architecture & Module Boundaries","category":"project-spec","format":"markdown","content":"# Architecture\n\n> Companion: [.claude/rules/architecture.md](../.claude/rules/architecture.md) (auto-loaded coding rules)\n\n## DDD layered architecture\n\n```\n┌──────────────────────────────────────────────────────┐\n│  entrypoints/  (Presentation)                         │\n│    cli + api                                          │\n├──────────────────────────────────────────────────────┤\n│  service/      (Application — Use Case orchestration) │\n│    memorize / search / get / knowledge                │\n├──────────────────────────────────────────────────────┤\n│  memory/       (Domain — Business core)               │\n│    models + extract + search + cascade + prompt_slots │\n│    + reflection + strategies + get + events            │\n├──────────────────────────────────────────────────────┤\n│  infra/persistence  (Storage adapters; infra/ may host other adapter types)    │\n│    markdown + sqlite + derived index                  │\n└──────────────────────────────────────────────────────┘\n\nCross-cutting (used by all layers, depends on none):\n  component/  ← Injectable providers (LLM / Embedding / parser / config / utils)\n  core/       ← Runtime base (observability / lifespan / context / errors / persistence / middleware)\n  config/     ← Configuration data (Settings schema + default.toml)\n```\n\n## Dependency direction (single-direction, enforced)\n\n```\nentrypoints → service → memory → infra\n```\n\n| from → to | Allowed? |\n|---|---|\n| entrypoints → service | ✅ |\n| entrypoints → memory / infra | ❌ (must go through service) |\n| service → memory | ✅ |\n| memory → infra | ✅ |\n| memory → service | ❌ |\n| infra → memory | ❌ |\n| infra cross-subpackage (e.g. lancedb → markdown within persistence/) | ❌ (use service to orchestrate); one carve-out: every index backend reads the shared record declarations in `persistence/records/` (`lancedb → records` is whitelisted in import-linter) |\n| any → component / core / config | ✅ (cross-cutting) |\n| `infra/persistence/index → component.embedding` | ✅ — the only network-capable provider `infra` reaches for. Startup measures the embedding model's real output width before the index is opened for use; nothing else can answer that question, and asking it from a higher layer would mean every index user had to remember to ask. Elsewhere `infra` uses `component` only for `utils.datetime`. |\n\nEnforced via `import-linter` in CI:\n\n```toml\n[tool.importlinter]\nroot_packages = [\"everos\"]\n\n[[tool.importlinter.contracts]]\nname = \"Layered architecture\"\ntype = \"layers\"\nlayers = [\n    \"everos.entrypoints\",\n    \"everos.service\",\n    \"everos.memory\",\n    \"everos.infra\",\n]\n```\n\n## Storage three-piece set\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│             md-first storage stack                              │\n└────────────────────────────────────────────────────────────────┘\n\n   ┌──────────────┐   ┌──────────────┐   ┌─────────────────┐\n   │   Markdown   │   │   SQLite     │   │ Derived index   │\n   │  (truth)     │   │  (state)     │   │ LanceDB/Milvus  │\n   ├──────────────┤   ├──────────────┤   ├─────────────────┤\n   │ entries +    │   │ change queue │   │ vector ANN      │\n   │ frontmatter  │   │ + state/LSN  │   │ BM25 (Tantivy)  │\n   │ Git friendly │   │ buffer /     │   │ scalar filter   │\n   │ Obsidian OK  │   │   audit      │   │ multi-modal     │\n   └──────────────┘   └──────────────┘   └─────────────────┘\n          │                  │                    │\n          ▼                  ▼                    ▼\n    memory-root/         .index/sqlite/      .index/<backend>/\n   (truth source)       (system data)       (rebuildable)\n```\n\n## Write path\n\n```\nExternal message\n       │\n       ▼\n1. service.memorize           (entrypoint of write path)\n       │\n       ▼\n2. memory.extract.pipeline    (calls everalgo)\n       │\n       ▼\n3. infra.persistence.markdown.write       (atomic: tmp + fsync + rename)\n       │  ✅ md write success → return immediately\n       │\n   ┌───┴────┐\n   │        │\n   ▼        ▼\n4a. SQLite   4b. memory.cascade  (async daemon)\n    audit        watches md → diff entries → index sync\n```\n\n**Key guarantee**: md write is strongly consistent (fsync). The derived\nindex is eventually consistent. Index backend unavailability does not block\nresponse — changes buffer in the SQLite `md_change_state` queue, replayed on\nrecovery.\n\n## Read path\n\n```\nUser query\n   │\n   ▼\n1. service.search\n   │\n   ▼\n2. memory.search (hybrid)     BM25 + vector ANN + scalar filter\n                              through the configured index backend\n   │\n   ▼\n3. (optional) read md         original markdown for context\n   │\n   ▼\n   Return\n```\n\n## Key components\n\n### `memory/extract/`\n\n```\nextract/\n├── ingest/      Standardized message intake + multi-modal parser dispatch\n├── parser/      Input parsing (format normalization, message preprocessing)\n├── pipeline/    Main extraction pipeline (calls everalgo + dual-track split + writes store)\n└── evolution/   Async memory evolution (event/counter/cron triggers)\n```\n\n### `memory/cascade/`\n\nDaemon that watches markdown changes and syncs to the configured derived\nindex backend:\n\n- inotify / FSEvents file watcher (cross-platform via `watchdog`)\n- 500ms debounce\n- Entry-level diff (added / changed / removed)\n- Per-entry index upsert / delete (text + vector columns update together)\n- LSN-based crash recovery via the SQLite `md_change_state` queue\n- Handlers for all eight business kinds: episode, atomic_fact, foresight,\n  user_profile, agent_case, agent_skill, knowledge_document, knowledge_topic\n\n### `memory/prompt_slots/`\n\nThree-layer prompt overlay:\n\n```\nconfig/prompt_slots/*.yaml          (Layer 1: defaults, ships with package)\n       ↓\n~/.everos/prompt_slots/*.yaml       (Layer 2: app-level override)\n       ↓\nruntime override                    (Layer 3: per-call override)\n```\n\nExtractors may accept a prompt-override parameter; EverOS supplies overrides for episode and boundary-detection prompts, and falls back to the algo-bundled default elsewhere — no hardcoded prompts in algorithm code.\n\n### `memory/reflection/`\n\nOffline memory self-improvement. The orchestrator (`orchestrator.py`)\nimplements the Select → Merge → Re-extract → Deprecate pipeline, merging\nfragmented episodes within a cluster into a single coherent narrative. Driven\nby the `reflect_episodes` OME strategy (cron, disabled by default).\n\n### `memory/strategies/`\n\nOME strategy implementations — one file per strategy:\n\n- `extract_atomic_facts` / `extract_foresight` / `extract_user_profile` — user pipeline\n- `extract_agent_case` / `extract_agent_skill` — agent pipeline\n- `reflect_episodes` — offline episode consolidation (cron)\n- `trigger_profile_clustering` / `trigger_skill_clustering` — clustering triggers\n\n### `core/observability/`\n\nThree-piece observability:\n\n- `metrics/` — Prometheus counter / gauge / histogram + global registry\n- `logging/` — structlog with context processor (trace_id propagation)\n- `tracing/` — OpenTelemetry tracer + span helpers\n\n## Markdown layout\n\n```\n~/.everos/                                  # memory root (default; EVEROS_ROOT)\n└── <app_id>/<project_id>/                  # scope (\"default\" → default_app/default_project)\n    ├── users/<user_id>/\n    │   ├── user.md                                     # profile (single-file rewrite)\n    │   ├── episodes/episode-<YYYY-MM-DD>.md            # daily-log append\n    │   ├── .atomic_facts/atomic_fact-<YYYY-MM-DD>.md   # hidden, framework-derived\n    │   └── .foresights/foresight-<YYYY-MM-DD>.md       # hidden, framework-derived\n    ├── agents/<agent_id>/\n    │   ├── .cases/agent_case-<YYYY-MM-DD>.md           # hidden, framework-derived\n    │   └── skills/skill_<name>/SKILL.md                # named-dir\n    └── knowledge/                                      # global shared knowledge\n```\n\nSystem-managed entries (`.index/`, `.tmp/`) and `ome.toml` live directly\nunder the memory root.\nFull tree + frontmatter chassis: [storage_layout.md](storage_layout.md) and\n[how-memory-works.md](how-memory-works.md). Frontmatter has 4-tier field\nprotection (L1 read-only / L2 system / L3 business / L4 user).\n\n## everalgo boundary\n\n`everalgo` is a set of PyPI-published packages (`everalgo-user-memory`,\n`everalgo-agent-memory`, `everalgo-rank`, `everalgo-knowledge`, plus the\noptional `everalgo-parser` extra), imported under the `everalgo` namespace,\nholding **only memory extraction algorithms**:\n\n- `everalgo.parser` — multi-modal parsing (optional `[multimodal]` extra)\n- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile extractors\n- `everalgo.agent_memory` — AgentMemCell / Case / Skill extractors\n- `everalgo.rank` — boundary detection / fusion + rerank\n- `everalgo.knowledge` — KnowledgeExtractor (document parse + topic extraction)\n\neveralgo is:\n\n- **Stateless** — pure functions, no class hierarchy\n- **No I/O** — does not touch md files, derived indexes, or SQLite\n- **No prompts inline** — extractors that accept a prompt-override parameter use the project-supplied value; others use their algo-bundled defaults\n\nThis boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).\n\n## Error handling architecture\n\n### Exception hierarchy\n\nAll application exceptions derive from `AppError` (`core/errors.py`),\nsplit into four branches by nature:\n\n```\nAppError\n├── DomainError                    (client-side / business-rule violations)\n│   ├── NotFoundError              → 404 NOT_FOUND\n│   │   ├── DocumentNotFoundError\n│   │   └── TopicNotFoundError\n│   ├── ConflictError              → 409 CONFLICT\n│   │   └── DuplicateDocumentError\n│   ├── InvalidInputError          → 422 INVALID_INPUT\n│   │   ├── ExtractionEmptyError   → 422 EXTRACTION_EMPTY\n│   │   └── FilterError\n│   ├── PathTraversalError         → 400 BAD_REQUEST\n│   └── UnsupportedModalityError   → 415 UNSUPPORTED_FORMAT\n├── InfrastructureError            (transient, retryable)     → 503\n│   ├── StorageError\n│   ├── VectorStoreError\n│   └── ExternalServiceError\n│       ├── LLMServiceError\n│       ├── EmbeddingServiceError\n│       └── RerankServiceError\n├── CapabilityError                (permanent, not retryable) → 503\n│   └── MultimodalNotEnabledError\n└── ConfigurationError             (misconfiguration)         → 500\n```\n\n### Error propagation strategy\n\nExceptions are raised at the layer where the error is detected and\npropagate naturally — **service and route layers do not catch-and-wrap**.\nThe entrypoints layer registers per-type exception handlers\n(`entrypoints/api/exception_handlers.py`) via Starlette's MRO dispatch.\nEach handler converts the exception into a canonical error envelope with\nan `ErrorCode` enum value and the appropriate HTTP status code.\n\n### Boundary translation\n\nThird-party exception types are translated at the component boundary to\nprevent external types from leaking into upper layers:\n\n- `everalgo.llm.LLMError` → `LLMServiceError` at `component/parser/_core.py`\n- Embedding / rerank provider errors → `EmbeddingServiceError` / `RerankServiceError` at their respective protocol modules\n\n## Further reading\n\n- [docs/overview.md](overview.md) — vision and scope\n- [docs/engineering.md](engineering.md) — engineering tooling and CI / CD\n- [.claude/rules/architecture.md](../.claude/rules/architecture.md) — short-form rules for Claude Code\n","isInternal":false,"tokens":2806,"sizeBytes":13080},{"name":"plugin.json","path":"use-cases/claude-code-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/EverMind-AI/EverOS/HEAD/use-cases/claude-code-plugin/plugin.json","title":"evermem","category":"plugin-manifest","format":"json","content":"{\n  \"name\": \"evermem\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Automatically recalls relevant memories from past sessions and injects them into Claude's context\",\n  \"author\": {\n    \"name\": \"EverMem\"\n  },\n  \"keywords\": [\"memory\", \"context\", \"recall\", \"persistence\"]\n}\n","frontmatter":{"name":"evermem","version":"0.2.0","description":"Automatically recalls relevant memories from past sessions and injects them into Claude's context","author":{"name":"EverMem"},"keywords":["memory","context","recall","persistence"]},"isInternal":false,"tokens":75,"sizeBytes":267}],"systemPromptSnippet":"<yakaai_skills repo=\"EverMind-AI/EverOS\">\n<!-- File: CLAUDE.md (Tokens: ~1050 | Category: claude-rule) -->\n# EverOS — md-first Memory Extraction Framework\n\nThis is a Python framework for md-first memory extraction (lightweight; single-user or small-team).\n\n## Quick commands\n\n```bash\nuv sync                    # install deps\nmake lint                  # ruff (check + format-check) + import-linter\nmake format                # auto-fix formatting\nmake test                  # pytest tests/unit\nmake integration           # pytest tests/integration\nmake ci                    # full CI: lint + test + integration\n```\n\n## Architecture\n\nDDD 5 layers + cross-cutting:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — cli + api (Presentation)\n- `service/` — use case orchestration (memorize / retrieve / evolve / manage)\n- `memory/` — domain (extract + search + cascade + prompt_slots + models)\n- `infra/` — storage adapters (markdown + sqlite + lancedb)\n- `component/` — injectable providers (llm / embedding / config / utils)\n- `core/` — runtime base (observability / lifespan / context)\n- `config/` — configuration data (Settings + default.toml)\n\n**Dependency rule**: `entrypoints → service → memory → infra`. Single-direction, enforced by `import-linter`.\n\nDetailed: [docs/architecture.md](docs/architecture.md).\n\n## Engineering practices\n\n- **Coding rules** auto-loaded from [.claude/rules/](.claude/rules/) (10 rules; the three always-loaded ones cover architecture / code-style / language-policy, the rest are path-scoped and load when Claude Code opens a matching file)\n- **Workflows** as slash commands in [.claude/skills/](.claude/skills/) — `/commit`, `/new-branch`, `/pr`, `/release`\n- **Project-level decisions** in [docs/](docs/) (low-frequency, human-judgment-required)\n- **Language policy**: the project targets a global audience — docs and code are English; CJK appears only in test fixtures and locale-suffixed mirrors. Scanned by `make check-cjk`.\n- **Datetime discipline**: never call `datetime.now()` / `time.time()` directly — use `everos.component.utils.datetime`. Enforced by `make check-datetime`.\n\nContributor engineering reference — build, test, CI gates, branch & commit conventions: [docs/engineering.md](docs/engineering.md).\n\n## Branch strategy\n\n`main` is the default and protected branch. Create scoped branches from\n`main` (`feat/*`, `fix/*`, `docs/*`, `ci/*`, `chore/*`, `refactor/*`) and open\npull requests back to `main` after the required checks pass.\n\nSee [.claude/skills/new-branch/SKILL.md](.claude/skills/new-branch/SKILL.md)\nfor the full branch workflow.\n\n## GitHub sync guard\n\nWhen refreshing this repository from an internal source archive, preserve\nGitHub-only contributor and automation files. Do not overwrite `CLAUDE.md`,\n`.claude/skills/*`, `CONTRIBUTING.md`, or `.github/*` workflow/template files\nwithout checking [docs/github-sync.md](docs/github-sync.md).\n\n## Storage three-piece set\n\n```\nMarkdown (truth)  +  SQLite (state)  +  LanceDB (vector + BM25 + scalar)\n```\n\n- Memory root: `~/.everos/{agents,users,knowledge}/` (md files = single source of truth)\n- System DB: `~/.everos/.index/sqlite/system.db` (state + audit + queue + metadata)\n- Index: `~/.everos/.index/lancedb/` (rebuildable from md)\n\nSelection rationale: [docs/architecture.md](docs/architecture.md).\n\n## Source layout\n\n**src layout** (`src/everos/<...>`): standard PyPA project structure — code lives under `src/` so the working tree is not on the import path until installed, preventing accidental imports of in-development modules.\n\nAlgorithm assets (prompts, extractors) live in the separate [`everalgo`](https://github.com/EverMind-AI/EverAlgo) library, consumed here as the `everalgo-*` PyPI packages.\n\n## Where things go\n\n| Want to... | Look at |\n|---|---|\n| Understand architecture | [docs/architecture.md](docs/architecture.md) |\n| Understand storage choice | [docs/architecture.md](docs/architecture.md) (storage section) |\n| Build, test, CI & conventions | [docs/engineering.md](docs/engineering.md) |\n| Add a new module | [.claude/rules/init-py-and-reexport.md](.claude/rules/init-py-and-reexport.md) |\n| Make a commit | use `/commit` |\n| Open a branch / PR | use `/new-branch` / `/pr` |\n| Run checks before pushing | `make ci` |\n\n<!-- File: .claude/rules/architecture.md (Tokens: ~378 | Category: anthropic-skill) -->\n# Architecture rule (always loaded)\n\nEverOS is a DDD-layered framework. The dependency direction is **single, downward only**:\n\n```\nentrypoints  →  service  →  memory  →  infra\n                              ↓\n                        component / core / config\n```\n\n- `entrypoints/` — CLI + HTTP API (presentation). No business logic.\n- `service/` — use-case orchestration (memorize / retrieve / evolve / manage).\n- `memory/` — domain (extract / search / cascade / prompt_slots / models).\n- `infra/` — storage adapters (markdown + sqlite + lancedb) and the OME subsystem.\n- `component/` — injectable providers (llm / embedding / config / utils).\n- `core/` — runtime base (observability / lifespan / context / persistence primitives).\n- `config/` — configuration data (Settings + default TOML).\n\n## Hard constraints (enforced by `import-linter`, run in `make lint`)\n\n1. **Layering**: an outer layer may import an inner layer, never the reverse.\n   `entrypoints → service → memory → infra`.\n2. **Private internals**: `service`, `memory`, and `entrypoints` must not import\n   `infra.persistence.{markdown,lancedb,sqlite}.**` internals — go through the\n   package facade (`from everos.infra.persistence.markdown import ...`).\n3. **OME isolation**: `infra.ome` must not import `persistence`, `memory`,\n   `service`, or `entrypoints`. It is a low-level scheduler with no domain knowledge.\n\nIf a change needs to cross a boundary the wrong way, the design is wrong — refactor,\ndon't add an exception.\n\nFull rationale: [docs/architecture.md](../../docs/architecture.md).\n\n<!-- File: .claude/rules/async-programming.md (Tokens: ~241 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Async programming rule\n\nThe write/read paths are async end-to-end. Keep them non-blocking.\n\n- **No blocking calls in async functions** — no synchronous file I/O, no `time.sleep`,\n  no blocking DB/network calls inside `async def`. Ruff `ASYNC` flags the common cases.\n- **Offload CPU/blocking work** with `anyio.to_thread.run_sync` (or the established\n  helper) rather than blocking the event loop.\n- **Concurrency** via `asyncio.gather` / `asyncio.TaskGroup` for independent awaits;\n  don't `await` in a loop when the calls are independent.\n- **Tests**: `pytest-asyncio` is in `auto` mode — an `async def test_*` just works,\n  no `@pytest.mark.asyncio` needed.\n- **Don't fire-and-forget** without holding a reference (`asyncio.create_task` results\n  must be tracked, or you lose exceptions). The OME subsystem owns the long-running\n  background loops — application code shouldn't spawn its own.\n\n<!-- File: .claude/rules/code-style.md (Tokens: ~289 | Category: anthropic-skill) -->\n# Code style rule (always loaded)\n\n- **Formatter & linter**: `ruff` is the single tool (replaces black / isort / flake8).\n  Line length 88, target `py312`. Run `make format` to auto-fix; `make lint` checks.\n- **Active ruff rule sets**: `E F I N UP B SIM ASYNC`. Don't disable a rule inline\n  unless there's a genuine reason — prefer fixing the code.\n- **Type hints**: annotate every public function signature (params + return). The\n  codebase is ~100% typed; keep it that way.\n- **`from __future__ import annotations`** at the top of every module — annotations\n  are strings, so forward refs and `X | None` unions are free.\n- **Prefer `collections.abc`** (`Sequence`, `Mapping`) over concrete `list`/`dict`\n  in signatures; use `Protocol` for structural interfaces.\n- **No dead code**: no commented-out blocks, no unused imports, no speculative\n  abstractions. Delete rather than comment out.\n- **Naming**: `*Manager` (orchestrators), `*Provider` (injectable services),\n  `*Reader`/`*Writer` (persistence), `*Recaller` (search routes). Follow the\n  established suffix when adding a sibling.\n\n<!-- File: .claude/rules/datetime-handling.md (Tokens: ~327 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Datetime handling rule (two-zone discipline)\n\n**Never** construct or read \"now\" directly. All datetime flows through\n`everos.component.utils.datetime`. This is a **hard CI gate**\n(`make check-datetime`, wired into `make lint`).\n\n## Banned (the checker fails the build on these)\n\n- `datetime.now()`, `datetime.utcnow()`, `datetime.today()`\n- `time.time()`, `time.time_ns()`\n- `datetime(YYYY, ...)` without `tzinfo=`\n- `.astimezone(...)` / `.replace(tzinfo=...)` outside the helper module\n\n## Use instead\n\n| Need | Helper |\n|---|---|\n| \"now\" for **storage** (UTC) | `get_utc_now()` |\n| \"now\" for **display** (configured TZ) | `get_now_with_timezone()` |\n| today's date, display TZ | `today_with_timezone()` |\n| normalize a value to UTC | `ensure_utc(d)` |\n| render to display TZ | `to_display_tz(d)` |\n| parse ISO / epoch / str | `from_iso_format(v)`, `from_timestamp(ts)` |\n| serialize | `to_iso_format(d)`, `to_date_str(d)`, `to_timestamp_ms(d)` |\n\n**Two zones**: persist in UTC, present in the configured display TZ. Crossing them\ngoes through the helpers — never ad-hoc. See [docs/datetime.md](../../docs/datetime.md).\n\n<!-- File: .claude/rules/imports.md (Tokens: ~209 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/*.py\"\n  - \"tests/**/*.py\"\n---\n\n# Imports rule\n\n- **`from __future__ import annotations`** is the first import in every module.\n- **Import order** (ruff `I` enforces, `make format` fixes): stdlib → third-party\n  → first-party (`everalgo`, then `everos`). One group per blank-line-separated block.\n- **Absolute imports** for cross-package references (`from everos.memory import ...`).\n  Relative imports (`from .models import ...`) only **within** a package, typically\n  in its `__init__.py`.\n- **`TYPE_CHECKING` guard** for import cycles and type-only imports:\n  ```python\n  from typing import TYPE_CHECKING\n  if TYPE_CHECKING:\n      from everos.config import Settings\n  ```\n- Never import a private internal across a package boundary — respect the\n  `import-linter` contracts (see [architecture.md](architecture.md)).\n\n<!-- File: .claude/rules/init-py-and-reexport.md (Tokens: ~302 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/__init__.py\"\n  - \"src/**/*.py\"\n---\n\n# `__init__.py` and re-export rule\n\nA package's `__init__.py` is its **public facade**. Consumers import from the\npackage, never from its internal modules.\n\n## Pattern\n\n```python\n\"\"\"One-paragraph module docstring: what this package is and how to use it.\"\"\"\n\nfrom .models import Episode as Episode\nfrom .models import MemCell as MemCell\n\n__all__ = [\n    \"Episode\",\n    \"MemCell\",\n]\n```\n\n- **Explicit `X as X` redundant-alias form** on each re-export. This is intentional:\n  it marks the name as a deliberate public re-export (ruff `F401` / `PLC0414` aware)\n  rather than an accidental unused import.\n- **`__all__`** lists every public name, alphabetically sorted, matching the\n  re-exports. It is the contract; keep it in sync.\n- **Internal modules stay private** — don't re-export helpers that aren't part of\n  the public API.\n- New subpackage? Add an `__init__.py` with a docstring + `__all__` even if it\n  starts small. Empty-but-documented beats missing.\n\nThis facade discipline is what lets `import-linter` forbid deep imports across\npackage boundaries (see [architecture.md](architecture.md)).\n\n<!-- File: .claude/rules/language-policy.md (Tokens: ~145 | Category: anthropic-skill) -->\n# Language policy rule (always loaded)\n\nThe project targets a global audience and is **English-first**.\n\n- **Code, comments, docstrings, docs, commit messages, identifiers**: English only.\n- **CJK characters are allowed only in**:\n  - test fixtures under `tests/` and `data/` (multilingual input is the point), and\n  - locale-suffixed mirror files (e.g. `*_zh.json`).\n- Do **not** introduce CJK into `src/`, `docs/`, or config.\n\nEnforcement: `make check-cjk` scans for stray CJK outside the allowlist (advisory).\nKeep user-facing strings and error messages in English.\n\n<!-- File: .claude/rules/logging-observability.md (Tokens: ~389 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/**/*.py\"\n---\n\n# Logging & observability rule\n\n- **Use the project logger**, never `print` or the stdlib `logging` directly:\n  ```python\n  from everos.core.observability.logging import get_logger\n  logger = get_logger(__name__)\n  ```\n- **Structured logging** (`structlog`): pass context as keyword fields, not f-strings.\n  ```python\n  logger.info(\"memory.search.completed\", owner_type=owner, n_results=len(items))\n  ```\n  Event name first (dotted, stable), structured kwargs after. This keeps logs\n  queryable and avoids leaking interpolated PII into the message string.\n- **Levels**: `debug` for developer detail, `info` for lifecycle milestones,\n  `warning` for recoverable anomalies, `error` for failures with a stack/context.\n- **Metrics** go through `core.observability.metrics` (Prometheus); don't invent\n  ad-hoc counters. Histograms/counters/gauges have registry helpers.\n- Don't log secrets, API keys, or full memory content at `info`/above.\n- **Tracing** (optional, `[otel]` extra, **off by default**): open spans with\n  `memory_span(...)` from `core.observability.tracing` — it stamps the Langfuse\n  `langfuse.*` attributes and is a no-op until `[observability] enabled`, so call\n  sites never branch on config. LLM / embedding token usage rides\n  `set_generation_usage` onto the active span (Langfuse computes cost).\n  Request/response content is emitted only when `capture_content` is on\n  (redaction hook + truncation). `request_id` is kept independent of the OTel\n  `trace_id`; an upstream `traceparent` header is continued when present.\n\n<!-- File: .claude/rules/module-docstring.md (Tokens: ~293 | Category: anthropic-skill) -->\n---\npaths:\n  - \"src/everos/infra/**/*.py\"\n  - \"src/everos/memory/**/*.py\"\n  - \"src/everos/service/**/*.py\"\n  - \"src/everos/component/**/*.py\"\n  - \"src/everos/core/**/*.py\"\n---\n\n# Module docstring rule\n\nEvery non-trivial module in the domain/infra layers opens with a docstring that\nexplains **intent and contract**, not just a one-line label.\n\nA good module docstring states:\n\n- **What** the module is responsible for (one sentence).\n- **The load-bearing invariants** — the rules a reader must know to change it\n  safely (partition keys, what is/isn't written, defaults, ignored flags).\n- **External usage** when the module is a package facade (a short import example).\n\nExample (abbreviated, from `memory/search/manager.py`):\n\n```python\n\"\"\"SearchManager — top-level orchestrator for POST /api/v2/memory/search.\n\nHard partition by owner_type: user → episodes (+ profiles), agent →\nagent_cases + agent_skills. The manager never writes to storage; it only\nreads LanceDB + markdown.\n\"\"\"\n```\n\nPrefer prose that would save the next engineer a debugging session over\nboilerplate. If a module is genuinely trivial (a 3-line constant), a one-liner\nis fine — but most modules here are not.\n\n<!-- File: .claude/rules/testing.md (Tokens: ~329 | Category: anthropic-skill) -->\n---\npaths:\n  - \"tests/**/*.py\"\n---\n\n# Testing rule\n\nTests mirror the source layout: `tests/unit/test_<layer>/...`,\n`tests/integration/...`, `tests/e2e/...`.\n\n- **Structure**: `tests/unit/` mirrors `src/everos/` package-for-package. Put a test\n  next to where its subject lives in the mirror.\n- **Async**: `pytest-asyncio` is in `auto` mode — write `async def test_*` directly,\n  no marker needed.\n- **Markers** (default run excludes both — `-m \"not slow and not live_llm\"`):\n  - `@pytest.mark.slow` — tests ≥ ~10s.\n  - `@pytest.mark.live_llm` — needs real LLM/embedder credentials.\n  Keep unit tests fast and credential-free; push anything needing real services\n  behind a marker or into `integration`/`e2e`.\n- **Fixtures**: shared fixtures live in the nearest `conftest.py`. The root conftest\n  resets module caches (settings/logging/datetime) per test — rely on that for\n  isolation rather than mutating globals.\n- **Module docstring** on each test file stating what contract it pins (see existing\n  tests for the style).\n- **Coverage gate**: `make cov` enforces 80% (`--cov-fail-under=80`). New code should\n  not drop coverage below the gate.\n- Run `make test` (unit) and `make integration` before pushing; both run in CI.\n\n<!-- File: .claude/skills/add-memory-kind/SKILL.md (Tokens: ~2908 | Category: anthropic-skill) -->\n---\nname: add-memory-kind\ndescription: 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).\n---\n\n# /add-memory-kind — Add a new business memory kind\n\n## When to invoke\n\nAdding a new persisted business entity (Episode, Case, Skill, AtomicFact,\nForesight, Profile, or something custom). Multiple storage layers may be\ninvolved; this skill walks the decision then the wiring.\n\n## 1. Decide the storage combination\n\nA memory kind **does not have to use all three** layers. Pick by what\nthe kind actually needs:\n\n| Need | Markdown | SQLite | LanceDB |\n|---|:-:|:-:|:-:|\n| Human-readable / agent-editable source-of-truth text | ✅ | | |\n| Structured state, ACID transactions, joins, predicates | | ✅ | |\n| Vector / BM25 / hybrid retrieval | | | ✅ |\n\nCommon combinations seen in EverOS:\n\n| Combo | Example | Rationale |\n|---|---|---|\n| **md only** | scratch notes / dump bins | text-of-truth, no index needed |\n| **md + lancedb** | episode / memcell / case | text-of-truth + semantic retrieval |\n| **md + sqlite** | profile / playbook / soul.md state | text-of-truth + structured state to query |\n| **md + sqlite + lancedb** | full-blown business records | when you need *both* transactional state AND retrieval |\n| **sqlite only** | audit log / task queue / LSN watermark | system state, never user-facing |\n| **lancedb only** | rare; usually you still want md | derived embeddings without text-of-truth |\n\nRule of thumb: **markdown is the truth**; sqlite and lancedb are derived\nindexes that can be rebuilt from md. Drop md only when the kind has no\nhuman-readable form (pure system state).\n\n## 2. Pick the markdown storage strategy (if md is in your combo)\n\nThree strategies — declared in the EverOS Markdown First spec:\n\n| Strategy | Filename | Mutation | Examples |\n|---|---|---|---|\n| **Daily-log append** | `<prefix>-YYYY-MM-DD.md` | append entries | memcell / episode / case / atomic_fact / foresight |\n| **Skill-named in-place** | `skill_<name>.md` | overwrite the file | skills (procedural memory) |\n| **Single-file rewrite** | `user.md` / `agent.md` / `soul.md` / `behaviors.md` / `tools.md` | overwrite the file | profiles / playbooks |\n\nThis skill currently has a **complete recipe for daily-log append**.\nSkill-named and single-file recipes are sketched at the bottom — their\nbase writers (`BaseSkillWriter` / `BaseProfileWriter`) land later in the\nproject; until then build a thin wrapper over `MarkdownWriter`\ndirectly.\n\n---\n\n## 3. Markdown daily-log: 4 steps\n\n### 3.1 Frontmatter schema — `infra/persistence/markdown/mds/<name>.py`\n\n```python\n\"\"\"Episode daily-log frontmatter.\"\"\"\n\nfrom __future__ import annotations\n\nimport datetime as _dt\nfrom typing import ClassVar, Literal\n\nfrom everos.core.persistence.markdown import UserScopedFrontmatter\n\n\nclass UserEpisodeDailyFrontmatter(UserScopedFrontmatter):\n    \"\"\"``users/<u>/episodes/episode-<YYYY-MM-DD>.md``.\"\"\"\n\n    ENTRY_ID_PREFIX: ClassVar[str] = \"ep\"\n    DIR_NAME: ClassVar[str] = \"episodes\"\n    FILE_PREFIX: ClassVar[str] = \"episode\"\n\n    type: Literal[\"user_episode_daily\"] = \"user_episode_daily\"\n    date: _dt.date\n    entry_count: int = 0\n    last_appended_at: _dt.datetime | None = None\n```\n\nFor agent-track kinds subclass `AgentScopedFrontmatter` instead. If\nuser-track and agent-track share a kind name (e.g. `memcell`), give\neach a **distinct** `ENTRY_ID_PREFIX` (e.g. `umc` vs `amc`) so reverse\nlookup is unambiguous.\n\n### 3.2 Re-export — `mds/__init__.py`\n\n```python\nfrom .episode import UserEpisodeDailyFrontmatter as UserEpisodeDailyFrontmatter\n```\n\n### 3.3 Business writer — `infra/persistence/markdown/writers/<name>.py`\n\n```python\n\"\"\"Episode appender.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pathlib import Path\n\nfrom everos.core.persistence import MarkdownReader\n\nfrom ..mds import UserEpisodeDailyFrontmatter\nfrom .base import BaseDailyWriter\n\n\nclass UserEpisodeAppender(BaseDailyWriter):\n    schema = UserEpisodeDailyFrontmatter\n\n    # OPTIONAL: override the count strategy. Default is len(entries);\n    # override to trust the frontmatter field instead.\n    def _current_count(self, path: Path) -> int:\n        if not path.exists():\n            return 0\n        return MarkdownReader.read(path).frontmatter.get(\"entry_count\", 0)\n```\n\n### 3.4 Re-export — `writers/__init__.py`\n\n```python\nfrom .episode import UserEpisodeAppender as UserEpisodeAppender\n```\n\n### Done — usage\n\n```python\nfrom everos.infra.persistence.markdown.writers import UserEpisodeAppender\n\nappender = UserEpisodeAppender(memory_root)\neid = appender.append(\"u_jason\", \"I went to the doctor today.\")\n# → users/u_jason/episodes/episode-<today>.md\n# → entry markers carry an auto-generated EntryId (e.g. ep_20260507_001)\n```\n\n---\n\n## 4. (Optional) SQLite table — 4 steps\n\nSkip this section if the kind doesn't need structured state beyond markdown.\n\n### 4.1 Schema — `infra/persistence/sqlite/tables/<name>.py`\n\n```python\nfrom everos.core.persistence.sqlite import BaseTable, Field\n\n\nclass EpisodeState(BaseTable, table=True):\n    __tablename__ = \"episode_state\"  # type: ignore[assignment]\n\n    id: int | None = Field(default=None, primary_key=True)\n    entry_id: str = Field(index=True, unique=True)\n    cluster_id: str | None = Field(default=None, index=True)\n    status: str = Field(default=\"active\")\n```\n\n`BaseTable` already provides `created_at` / `updated_at` (auto-bumped).\n\n### 4.2 Re-export — `tables/__init__.py`\n\n```python\nfrom .episode import EpisodeState as EpisodeState\n```\n\n### 4.3 Repo — `infra/persistence/sqlite/repos/<name>.py`\n\n```python\nfrom sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker\n\nfrom everos.core.persistence.sqlite import RepoBase\n\nfrom ..sqlite_manager import get_session_factory\nfrom ..tables import EpisodeState\n\n\nclass _EpisodeStateRepo(RepoBase[EpisodeState]):\n    model = EpisodeState\n\n    def _factory_lookup(self) -> async_sessionmaker[AsyncSession]:\n        return get_session_factory()\n\n\nepisode_state_repo = _EpisodeStateRepo()\n```\n\n### 4.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_state_repo as episode_state_repo\n```\n\n---\n\n## 5. (Optional) Derived index — 4 steps\n\nSkip this section if the kind doesn't need vector / BM25 / hybrid retrieval.\n\n### 5.1 Record declaration — `infra/persistence/records/<name>.py`\n\n```python\nfrom typing import ClassVar\n\nfrom everos.core.persistence.lancedb import BaseLanceTable, Vector\n\n\nclass EpisodeIndex(BaseLanceTable):\n    TABLE_NAME: ClassVar[str] = \"episode_index\"\n    PRIMARY_KEY: ClassVar[tuple[str, ...]] = (\"app_id\", \"project_id\", \"id\")\n\n    id: str\n    app_id: str = \"default\"\n    project_id: str = \"default\"\n    entry_id: str\n    text: str\n    tags: list[str]\n    vector: Vector(1024) | None = None  # type: ignore[valid-type]\n```\n\n`TABLE_NAME` / `BM25_FIELDS` / `PRIMARY_KEY` are the backend-neutral\ndeclarations every index backend reads (`IndexRecordMeta`). `PRIMARY_KEY`\nhas **no default**: `check_primary_keys` refuses to start, and LanceDB\n`upsert` refuses to write, a table that does not declare one. `id` is unique\nonly within `(app_id, project_id)`, so every business table declares the\nthree-column key — LanceDB merges on it, Milvus keys the collection on a\ncomposed `__pk` and rejects key components containing `/`. `Vector(N)` must\nmatch your embedding dimension.\n\n### 5.2 Re-export — `records/__init__.py`\n\n```python\nfrom .episode import EpisodeIndex as EpisodeIndex\n```\n\n### 5.3 Repo — `infra/persistence/lancedb/repos/<name>.py`\n\n```python\nfrom lancedb import AsyncTable\n\nfrom everos.core.persistence.lancedb import LanceRepoBase\nfrom everos.infra.persistence.records import EpisodeIndex\n\nfrom ..lancedb_manager import get_table\n\n\nclass _EpisodeIndexRepo(LanceRepoBase[EpisodeIndex]):\n    schema = EpisodeIndex\n\n    async def _table_lookup(self) -> AsyncTable:\n        return await get_table(self.schema.TABLE_NAME, self.schema)\n\n\nepisode_index_repo = _EpisodeIndexRepo()\n```\n\nBusiness code writes through the `everos.infra.persistence.index` facade:\n`upsert(records)` merges on the declared key and is the only write; look a\nrow up with `find_one_where(primary_key(app_id, project_id, row_id))`.\n\n### 5.4 Re-export — `repos/__init__.py`\n\n```python\nfrom .episode import episode_index_repo as episode_index_repo\n```\n\n---\n\n## 6. (Future) Skill-named & single-file markdown strategies\n\nWhen the new memory kind needs:\n\n- **skill-named** files (one file per named skill, in-place rewrite) — wait\n  for `BaseSkillWriter`, or use `MarkdownWriter.write_markdown` directly\n  with a thin wrapper.\n- **single-file rewrite** (one fixed file like `user.md`) — wait for\n  `BaseProfileWriter`, same fallback.\n\nThese strategies do **not** use entry markers; their frontmatter schema\ndoes not need `ENTRY_ID_PREFIX` (only `id` / `type` / `schema_version` plus\nthe scope mixin fields).\n\n---\n\n## 7. Verification checklist\n\n- [ ] `make lint` — ruff + import-linter clean\n- [ ] `make test` — existing manager / lifespan / writer tests still pass\n- [ ] When the kind crosses **multiple** layers:\n  - [ ] markdown entry id is the join key for the sqlite / lancedb rows\n  - [ ] business code reads only via the repo singleton (no raw engine\n        access in service / memory)\n  - [ ] cascade daemon (when it lands) can rebuild sqlite / lancedb\n        from md alone — keep md as the truth\n\nTests by layer:\n\n| Tests for | Location |\n|---|---|\n| Markdown frontmatter schema | `tests/unit/test_infra/test_markdown/test_mds/` |\n| Markdown business appender | `tests/unit/test_infra/test_markdown/test_writers/` |\n| SQLite RepoBase logic | `tests/unit/test_core/test_persistence/test_sqlite/` |\n| SQLite manager / lifespan | `tests/unit/test_infra/test_sqlite/` |\n| LanceDB LanceRepoBase logic | `tests/unit/test_core/test_persistence/test_lancedb/` |\n| LanceDB manager / lifespan | `tests/unit/test_infra/test_lancedb/` |\n\n## 8. Common pitfalls\n\n| Mistake | Symptom | Fix |\n|---|---|---|\n| Forgot `ENTRY_ID_PREFIX` / `DIR_NAME` / `FILE_PREFIX` on a daily-log schema | `BaseDailyWriter.__init__` raises `TypeError` | Add all three ClassVars |\n| Same `ENTRY_ID_PREFIX` on user + agent variants | `MemoryLayout.locate_for_entry` collision error | Use distinct prefixes (e.g. `umc` vs `amc`) |\n| Imported `RepoBase` from `infra.persistence.sqlite` | `ImportError` | Lives in `core.persistence.sqlite` (moved earlier) |\n| Skipped one of the four files (schema / writer / table / repo) | One side silently absent | Re-export both/all from each `__init__.py` |\n| `Vector(N)` mismatched with embedding dim | LanceDB raises on insert | Make `N` exactly match the model output |\n| 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 |\n| Hand-rolling `datetime.now()` instead of `today_with_timezone()` | Day-boundary drift across timezones | Always go through `everos.component.utils.datetime` |\n\n## Background\n\n- Architecture: [../../rules/architecture.md](../../rules/architecture.md)\n- `__init__.py` re-export rules: [../../rules/init-py-and-reexport.md](../../rules/init-py-and-reexport.md)\n- Async programming: [../../rules/async-programming.md](../../rules/async-programming.md)\n- Datetime handling: [../../rules/datetime-handling.md](../../rules/datetime-handling.md)\n\n<!-- File: .claude/skills/commit/SKILL.md (Tokens: ~473 | Category: anthropic-skill) -->\n---\nname: commit\ndescription: Stage and create a Conventional Commits message for the current change\n---\n\n# /commit\n\nCreate a well-formed commit following the [Conventional Commits](https://www.conventionalcommits.org)\nstandard. The format is enforced by `gitlint` in the `commit-msg` hook.\n\n## Steps\n\n1. Run `git status` and `git diff` (and `git diff --staged`) to see what changed.\n2. Review recent history for style: `git log --oneline -10`.\n3. Group the change into a single focused commit. If the working tree mixes\n   unrelated changes, stage selectively (`git add -p` / specific paths) rather\n   than committing everything at once.\n4. Write the message in **Conventional Commits** form:\n\n   ```\n   <type>[(scope)][!]: <imperative summary, ≤72 chars>\n\n   <optional body: what & why, wrapped at 72>\n\n   <optional footer: BREAKING CHANGE: …, Refs: #123>\n   ```\n\n5. Never use `--no-verify`. If pre-commit hooks fail, fix the cause and re-commit.\n6. Do not commit secrets, generated artifacts, or work-in-progress directly to\n   the protected `main` branch.\n\n## Types\n\n| Type | Use for |\n|---|---|\n| `feat` | new feature |\n| `fix` | bug fix |\n| `refactor` | behavior-preserving restructure |\n| `test` | add / update tests |\n| `docs` | documentation |\n| `style` | formatting only |\n| `perf` | performance |\n| `chore` | config / build / tooling |\n| `build` | build system or dependencies |\n| `ci` | CI configuration |\n| `revert` | revert a previous commit |\n\n## Notes\n\n- No emoji — the title must start with the `type`.\n- One logical change per commit; keep the history bisectable.\n- The summary is imperative mood: \"add\", not \"added\" / \"adds\".\n- `scope` is optional: `fix(search): …`. A `!` before the colon (or a\n  `BREAKING CHANGE:` footer) marks a breaking change.\n\n<!-- File: .claude/skills/new-branch/SKILL.md (Tokens: ~288 | Category: anthropic-skill) -->\n---\nname: new-branch\ndescription: Create a GitHub branch from main with the project naming convention\n---\n\n# /new-branch\n\nCut a new branch for the GitHub repository.\n\n## Branch model\n\n```\nmain       = default and protected branch\nfeat/*     = feature work\nfix/*      = bug fixes\ndocs/*     = documentation-only changes\nci/*       = CI, build, and developer-experience changes\nchore/*    = repository maintenance\nrefactor/* = behavior-preserving code structure changes\n```\n\n## Steps\n\n1. Ask (or infer) the change type: `feat`, `fix`, `docs`, `ci`, `chore`, or\n   `refactor`.\n2. Update `main` first:\n   ```bash\n   git checkout main\n   git pull --ff-only\n   ```\n3. Create the branch with a kebab-case slug:\n   ```bash\n   git checkout -b <type>/<short-slug>\n   ```\n4. Keep the branch scoped to one purpose and open a pull request back to\n   `main`.\n\n## Naming\n\n- `feat/add-agentic-rerank`, `fix/empty-profile-crash`,\n  `docs/quickstart-config`, `ci/check-github-docs`.\n- Lowercase, hyphen-separated, no spaces, concise.\n\nNever commit directly to `main` — always use a branch and pull request.\n\n<!-- File: .claude/skills/pr/SKILL.md (Tokens: ~357 | Category: anthropic-skill) -->\n---\nname: pr\ndescription: Open a GitHub PR targeting the correct branch with the project template\n---\n\n# /pr\n\nOpen a pull request on GitHub using the `gh` CLI and the repo's PR template.\n\n## Steps\n\n1. Confirm the branch and target:\n   - Base branch is **`main`**.\n   - Head branch should be a scoped branch such as `feat/*`, `fix/*`,\n     `docs/*`, `ci/*`, `chore/*`, or `refactor/*`.\n2. Ensure local checks pass first:\n   ```bash\n   make ci\n   ```\n   Do not open a PR with failing lint/tests.\n3. Push the branch:\n   ```bash\n   git push -u origin HEAD\n   ```\n4. Create the PR, filling the template\n   ([.github/PULL_REQUEST_TEMPLATE.md](../../../.github/PULL_REQUEST_TEMPLATE.md)):\n   ```bash\n   gh pr create --base main --fill-first\n   ```\n   Then edit the body to complete each section:\n   - **Summary** — what changed and why.\n   - **Area** — tick the relevant box (architecture / benchmark / use case /\n     docs / DX / CI-build-release).\n   - **Verification** — paste the commands you ran (`make ci`, manual checks).\n   - **Checklist** — tick honestly; don't tick boxes you didn't satisfy.\n   - **Notes for Reviewers** — anything subtle.\n\n## Notes\n\n- Keep the PR scoped to one area. Split unrelated changes.\n- If `make ci` was not fully run, say so in Verification rather than implying it passed.\n- Never retarget a community PR away from `main` unless a maintainer explicitly\n  asks for it.\n\n<!-- File: .claude/skills/release/SKILL.md (Tokens: ~1330 | Category: anthropic-skill) -->\n---\nname: release\ndescription: Cut a versioned release and publish everos to PyPI via the tag-triggered workflow\n---\n\n# /release\n\nPublish a new version of `everos` to PyPI. Publishing is automated: pushing a\n`vX.Y.Z` tag triggers [.github/workflows/release.yml](../../../.github/workflows/release.yml),\nwhich builds, smoke-tests, and uploads via PyPI **Trusted Publishing** (OIDC —\nno stored token) behind the `release` environment's manual-approval gate, then\ndrafts the GitHub Release page from the CHANGELOG.\n\nA release is not finished when PyPI accepts the upload: the GitHub Release page\nis drafted, never auto-published, and someone has to write its lead summary and\nclick **Publish**.\n\n## Preconditions\n\n- On `main`, up to date, with green CI (the tag builds from `main`'s tree).\n- Decide the version per SemVer: patch = fixes, minor = back-compatible\n  features, major = breaking changes.\n\n## Steps\n\n```\n1. Bump the version    → pyproject.toml [project] version = \"X.Y.Z\"\n   (single source; everos.__version__ reads installed package metadata)\n2. Update CHANGELOG.md → move the Unreleased entries under a new\n   ## [X.Y.Z] - <date> heading, and write the release page's prose here\n   (lead paragraph + `### Upgrade` group — see \"The release page\")\n3. Commit              → git commit -m \"chore(release): vX.Y.Z\"\n4. Open a PR, merge to main after green CI\n5. Tag main + push     → git tag -a vX.Y.Z -m \"vX.Y.Z\" && git push origin vX.Y.Z\n6. Approve             → the release.yml run pauses on the `release`\n   environment; a reviewer approves in the Actions run\n7. Verify              → https://pypi.org/project/everos/X.Y.Z/\n8. Publish the page    → the run leaves a DRAFT GitHub Release, already\n   complete if step 2 was done properly. Read it once, click Publish.\n```\n\nThe tag must equal the `pyproject.toml` version — the workflow refuses to\npublish on a mismatch. A stable tag with no matching `## [X.Y.Z]` CHANGELOG\nsection fails the release job for the same reason.\n\n## The release page\n\n**The whole page is written in CHANGELOG.md, during the release PR.** Nothing\nis meant to be composed at publish time — by then the changes are weeks old and\nthe text gets no review. Write the version section like this:\n\n```markdown\n## [X.Y.Z] - 2026-09-01\n\n**What this release is for.** One paragraph, prose, no bullets — it becomes the\nlead of the release page. Say what changed for a user, not what was refactored.\n\n### Added\n### Changed\n### Fixed\n\n### Upgrade\n\nWhat a reader must know before upgrading: what happens on first startup, which\ncommand recovers a bad state, which pins moved. Omit the group entirely when a\nplain `pip install --upgrade` is all there is — 1.1.4 and 1.2.0 have nothing\nhere. Do not write filler.\n```\n\nCI turns that into the page: everything above `### Upgrade` is lifted verbatim\nwith the group headings demoted to `##`, and the Upgrade prose is wrapped in the\nboilerplate — pip line above it, compare link below — which is the shape every\nrelease since 1.1.3 has. See\n[1.2.1](https://github.com/EverMind-AI/EverOS/releases/tag/v1.2.1).\n\nSo publishing is a read-through and a click. If the draft looks wrong, the fix\nbelongs in CHANGELOG.md on `main`, not only in the draft — otherwise the two\ndrift apart and the next release inherits the habit.\n\n> While it is a draft, GitHub serves the release at\n> `releases/tag/untagged-<hash>`, and that URL keeps serving a stale page after\n> publication with no redirect. Never share it — a reader who opens it later\n> concludes the release never went out. Link `releases/tag/vX.Y.Z` instead; the\n> job prints both URLs in its step summary.\n\nPublish with the **Publish release** button in the web UI. Publishing through\nthe API by flipping `draft` alone drops the tag: GitHub rebinds the release to\nthe `untagged-<hash>` placeholder and creates a git tag by that name against the\ndefault branch (observed on 1.2.2). Pass `tag_name` if you must do it from the\nCLI:\n\n```bash\ngh api -X PATCH \"repos/EverMind-AI/EverOS/releases/<id>\" \\\n  -F draft=false -f tag_name=vX.Y.Z -f make_latest=true\n```\n\nRe-running the release job replaces its own draft and leaves an already-published\nrelease untouched, so a re-run is always safe.\n\n## Pre-releases\n\nPEP 440 pre-release tags publish too (PyPI accepts them; `pip install everos`\nignores them unless `--pre`): `vX.Y.ZrcN`, `vX.Y.ZaN`, `vX.Y.ZbN`. Set the same\nsuffix in `pyproject.toml` version before tagging.\n\nTheir release page is drafted as a **pre-release** and never becomes\n`/releases/latest`. A pre-release does not need its own CHANGELOG section — the\ndraft falls back to a one-line placeholder body when there is none.\n\n## One-time setup (project owner, not doable from CI)\n\n1. **PyPI trusted publisher** — PyPI → project `everos` → Settings →\n   Publishing → add: owner `EverMind-AI`, repo `EverOS`, workflow\n   `release.yml`, environment `release`.\n2. **GitHub environment** — repo Settings → Environments → create `release`\n   with required reviewers, so every publish needs a manual approval.\n\nNo PyPI API token is ever stored; the workflow mints a short-lived OIDC token\nat publish time.\n\n<!-- File: use-cases/claude-code-plugin/skills/memory-tools.md (Tokens: ~331 | Category: anthropic-skill) -->\n---\ndescription: Guidance for using EverMem memory tools to recall past session context\nalwaysInclude: true\n---\n\n# EverMem Memory Tools\n\nYou have access to memory tools that can recall context from the user's past coding sessions. Use these tools proactively when they would help provide better assistance.\n\n## Available Tools\n\n- **evermem_search**: Search past conversations using semantic + keyword matching. Params: `query` (required), `limit` (default 10, max 20)\n\n## When to Use Memory Search\n\n**DO search memories when:**\n- User asks about past work, decisions, or implementations (\"how did we handle X?\")\n- User references previous sessions (\"remember when\", \"last time\", \"we discussed\")\n- User is debugging something that may have been solved before\n- User asks about project patterns, conventions, or architecture decisions\n- Context from previous sessions would improve your response\n- User seems to expect you to know something from before\n\n**DON'T search memories when:**\n- The question is self-contained and doesn't need historical context\n- User explicitly provides all needed context in their message\n- It's a general knowledge question unrelated to their project history\n- You've already searched for this topic in the current session\n\n## Best Practices\n\n1. **Be selective**: Don't search for every query - only when past context adds value\n2. **Use specific queries**: Search for relevant terms, not the entire user message\n3. **Synthesize results**: When you find relevant memories, integrate them naturally into your response\n4. **Be transparent**: Mention when your response is informed by past session context\n\n<!-- File: docs/api.md (Tokens: ~13490 | Category: project-spec) -->\n# EverOS HTTP API (v2)\n\nHuman-readable reference for the EverOS HTTP API. Schema names, types\nand validation constraints mirror the OpenAPI spec served at\n`GET /openapi.json` (when the server runs with `ENV=DEV`; see\n[OpenAPI spec source](#openapi-spec-source)). This document adds the\nbusiness semantics the raw spec does not carry.\n\n## Table of contents\n\n- [Overview](#overview)\n  - [Base URL and versioning](#base-url-and-versioning)\n  - [Content type](#content-type)\n  - [Authentication](#authentication)\n  - [Response envelope](#response-envelope)\n  - [Eventual consistency](#eventual-consistency)\n  - [Conventions](#conventions)\n  - [ScopeId: `app_id` and `project_id`](#scopeid-app_id-and-project_id)\n- [Errors](#errors)\n- [Common types](#common-types)\n  - [MessageItem](#messageitem)\n  - [ContentItem](#contentitem)\n  - [ToolCall](#toolcall)\n  - [ToolFunction](#toolfunction)\n  - [FilterNode (filter DSL)](#filternode-filter-dsl)\n  - [SearchMethod](#searchmethod)\n  - [GetMemoryType](#getmemorytype)\n- [Endpoints](#endpoints)\n  - [POST /api/v2/memory/add](#post-apiv2memoryadd)\n  - [POST /api/v2/memory/flush](#post-apiv2memoryflush)\n  - [Asynchronous mode](#asynchronous-mode)\n  - [GET /api/v2/tasks/{task_id}](#get-apiv2taskstask_id)\n  - [GET /api/v2/tasks](#get-apiv2tasks)\n  - [POST /api/v2/memory/search](#post-apiv2memorysearch)\n  - [POST /api/v2/memory/get](#post-apiv2memoryget)\n  - [POST /api/v2/ome/trigger](#post-apiv2ometrigger)\n  - [Knowledge endpoints](#knowledge-endpoints)\n- [OpenAPI spec source](#openapi-spec-source)\n\n## Overview\n\n### Base URL and versioning\n\n| Setting | Default | Override |\n|---|---|---|\n| Host | `127.0.0.1` (loopback only) | `EVEROS_API__HOST` env var or `--host` flag |\n| Port | `8000` | `EVEROS_API__PORT` env var or `--port` flag |\n| Version prefix | `/api/v2` | — |\n\nBusiness endpoints live under `/api/v2/memory/`, `/api/v2/ome/`, and\n`/api/v2/knowledge/`. Knowledge endpoints have their own dedicated\nreference at [docs/knowledge.md](knowledge.md) and are cross-referenced\nbelow. The operational endpoints `GET /health`, `GET /metrics` and\n`POST /api/v2/cascade/quiesce` exist but are intentionally outside this\nreference — they are runtime probes and shutdown hooks for deployment, not\npart of the application contract. `quiesce` drains the md → index queue and\nstops the watcher; calling it twice is safe, and it returns a bare status body\n(`quiesced`, `drained`, `pending_before`, `pending_after`,\n`failed_permanent`), see `docs/openapi.json`. One note for anyone keying off\n`/health`: `capabilities.embed` reports whether the embedding service\nanswered a dimension probe **when the process started**, not whether it is\nreachable now -- a service that recovers later keeps reading `false` until\nthe next restart.\n\n`/api/v2` is the canonical prefix, aligned with the EverOS Cloud API. Every\nbusiness endpoint is **also** served under `/api/v1`, kept as a legacy\ncompatibility alias: the two prefixes resolve to the same handlers with\nidentical request/response contracts, so existing `/api/v1` integrations keep\nworking today. The alias carries no long-term guarantee — it may be removed in\na future major release, and it will be announced in the changelog with a\ndeprecation window before that happens. Write new integrations against\n`/api/v2`, and migrate existing ones when convenient.\n\n### Content type\n\nAll `POST` endpoints require `Content-Type: application/json`. Request\nand response bodies are UTF-8 JSON.\n\n### Authentication\n\nEverOS ships **no built-in authentication**. The server binds to\n`127.0.0.1` by default; place your own gateway or auth layer in front\nbefore exposing the API on any other interface. See\n[../SECURITY.md](../SECURITY.md) for the threat model.\n\n### Response envelope\n\nSuccessful responses from the business endpoints (`/memory/*`,\n`/knowledge/*`) wrap the payload in the envelope below (`200 OK`, or\n`201 Created` for a knowledge document upload). `POST /api/v2/ome/trigger`\nand the operational endpoints return their body bare — see each section.\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"data\": { /* endpoint-specific payload */ }\n}\n```\n\n`request_id` is generated server-side (32 lowercase hex chars) and is\nechoed by structured logs / tracing / metrics, so a single request can\nbe correlated end-to-end. Error responses carry their own envelope —\n`request_id` at the top level alongside a nested `error` object (not a\nbare FastAPI `detail`); see [Errors](#errors).\n\n### Eventual consistency\n\n`/add` and `/flush` write the markdown file (the source of truth)\n**synchronously** — when the call returns with `status: \"extracted\"`,\nthe new entry exists on disk. The configured vector / BM25 / scalar\nindex backend is rebuilt by the in-process **cascade coroutine\nasynchronously**.\n\nThat means `/search` and `/get` may not see a record immediately after\nthe `/flush` that produced it. Typical sync latency is sub-second, but\nunder load it can reach ~10–15 seconds. If you need read-your-write\nsemantics, retry with backoff. The markdown file is durable\nregardless; index lag never loses data.\n\nThe same applies one level up for everything an OME strategy produces\n**after** the extraction call returns: agent cases and skills (agent track),\nprofiles, atomic facts. `/flush` returns once the episode is on disk; the\n`extract_agent_case` strategy then runs its own LLM call, and skill\nclustering runs after that, so `/get` with `memory_type: \"agent_case\"` can\nlegitimately report `total_count: 0` for tens of seconds after a flush that\nreturned `\"extracted\"` (14 s for the case and 32 s for the skill on a\nlaptop with a hosted LLM). `GET /health`'s `cascade.pending` does **not**\ncover this window — it only tracks the md → index queue, which is empty\nwhile the strategy is still thinking. Poll `/get` with a budget instead.\nThe agent track also has extraction gates of its own, applied before any\nLLM call: a trajectory with fewer than three tool-call rounds is rejected as\ntoo simple (`AgentCaseExtractor(min_tool_call_rounds=3)`, the EverOS\ndefault — so a session with no tool use never yields a case), and so is one\nthat does not end on an assistant text message. Trajectories that pass\nthose gates can still be dropped by the extractor's own \"worth extracting\"\nLLM filter. EverOS logs the skip as `agent_case_skipped_by_algo` with the\nmemcell and session ids; the reason itself is on the adjacent everalgo log\nline, `skipping memcell (n_items=N): <reason> <detail>`. The same\n\"seconds later\" rule for every OME-produced kind is summarised in\n[how-memory-works.md](how-memory-works.md).\n\n### Conventions\n\n#### Timestamps\n\nEverOS runs a **two-zone discipline**: every stored byte is UTC; every\nrendered value carries the configured **display timezone**. The\ndisplay timezone is set by `EVEROS_MEMORY__TIMEZONE` (env var) or\n`[memory] timezone` (TOML); default `\"UTC\"`.\n\n| Direction | Format | Notes |\n|---|---|---|\n| **Request** — `messages[].timestamp` (`/add`) | Integer, **Unix epoch milliseconds (ms)** | Server auto-detects `>= 10^12` as ms for backward compat, but the contract is ms |\n| **Request** — `filters.timestamp.{gte,lt,…}` (`/search` / `/get`) | Integer, **Unix epoch ms** *or* ISO-8601 string | Same auto-detect; pick whichever you have |\n| **Response** — every `timestamp` field | ISO-8601 string with **explicit timezone offset** (`+HH:MM` or `Z`) | Always rendered in the configured display tz; never naive |\n\nWith the default `EVEROS_MEMORY__TIMEZONE=UTC` an episode produced\nat 11:30 UTC renders as `\"2026-05-28T11:30:36Z\"` (Pydantic\ncanonicalises `timezone.utc` to `Z`). Switch to\n`EVEROS_MEMORY__TIMEZONE=Asia/Shanghai` and the same UTC instant\nrenders as `\"2026-05-28T19:30:36+08:00\"` — bytes on disk are\nunchanged.\n\n**Fallback for naive input.** If you submit a `filters.timestamp` ISO\nstring without an offset (e.g. `\"2026-05-28T11:30:00\"`), the server\ntreats it as already display-tz-local before comparing against\nstorage. This is the same rule users see when reading rendered output:\n\"if you didn't say a zone, we assume your zone.\"\n\n**For internal architecture.** The storage / display split lives in\n`everos.component.utils.datetime`\n(`get_utc_now` / `ensure_utc` / `UtcDatetime` for storage;\n`get_now_with_timezone` / `to_display_tz` for display). See\n[datetime.md](datetime.md) for the design rationale.\n\n#### Other conventions\n\n- **Server-generated IDs** follow `<owner>_<kind>_<YYYYMMDD>_<NNN>`,\n  e.g. `alice_ep_20260528_00000001` for an episode, `alice_af_...`\n  for an atomic fact. See\n  [storage_layout.md §4](storage_layout.md) for the encoding.\n- **All endpoints are POST** for `/api/v2/memory/*` even when the\n  semantics look like a read (`/search`, `/get`) — the request bodies\n  are too rich (filters, methods, paging) to encode in a query string.\n\n### ScopeId: `app_id` and `project_id`\n\n`app_id` and `project_id` partition memory at the disk layer. Every\nwrite lands under\n`~/.everos/<app>/<project>/users/<user_id>/...` (or `.../agents/...`\nfor the agent track). The default scope materialises on disk as\n`default_app` / `default_project` (the `_app` / `_project` suffix is\nadded only for the literal id `\"default\"` so the default space stays\nvisually distinct from user-named scopes).\n\nA `/search` or `/get` query never crosses scopes — different\n`(app_id, project_id)` pairs are isolated.\n\nBoth fields share the same validation:\n\n| Setting | Value |\n|---|---|\n| Type | `string` |\n| Default | `\"default\"` |\n| Length | 1–128 chars |\n| Charset | `^[a-zA-Z0-9_.-]+$` |\n| Rejected literals | `\".\"` and `\"..\"` (path-traversal guard) |\n\n## Errors\n\nEvery non-2xx response uses a uniform error envelope — `request_id` at\nthe top level (mirroring the success envelope) alongside a nested\n`error` object. It is **not** a bare FastAPI `detail`:\n\n```json\n{\n  \"request_id\": \"<32-char hex>\",\n  \"error\": {\n    \"code\": \"NOT_FOUND\",\n    \"message\": \"Document 'abc123' not found\",\n    \"timestamp\": \"2026-06-01T12:24:46+00:00\",\n    \"path\": \"/api/v2/knowledge/documents/abc123\"\n  }\n}\n```\n\n### error.code values\n\n`error.code` is a machine-readable `ErrorCode` enum. Clients can switch\non this value to decide retry / display / routing behaviour without\nparsing the human-readable `message` field.\n\n| `error.code` | HTTP | Retryable? | When |\n|---|---|---|---|\n| `NOT_FOUND` | `404` | No | Requested resource does not exist |\n| `CONFLICT` | `409` | No | Operation conflicts with existing state (e.g. duplicate document) |\n| `INVALID_INPUT` | `422` | No | Request-body validation failure. Also covers `/search` / `/get` filter-DSL compile errors — the compile reason rides in `message` — and, on `/add` and `/flush`, a session whose buffered messages the model could not segment on three consecutive calls: the buffer is moved to the `quarantine` track of `unprocessed_buffer` so the session can continue, and `message` names the session and the number of messages set aside |\n| `EXTRACTION_EMPTY` | `422` | No | Knowledge document parsed but the extractor produced no topics. An empty or whitespace-only upload is rejected earlier as `INVALID_INPUT` (`\"Uploaded file has no valid content.\"`) |\n| `BAD_REQUEST` | `400` | No | Path traversal attempt or other malformed input |\n| `UNSUPPORTED_FORMAT` | `415` | No | File format or modality not supported (e.g. unsupported `ContentItem` type, missing `ext` for `base64`) |\n| `PAYLOAD_TOO_LARGE` | `413` | No | An `async_mode` request body exceeds `memorize.async_queue.max_payload_bytes`; the batch was not accepted |\n| `QUEUE_SATURATED` | `429` | Yes | The memorize queue is at its per-session or global cap; the batch was not accepted — retry later or keep it client-side |\n| `EXTERNAL_SERVICE_UNAVAILABLE` | `503` | **Yes** | An external service (LLM, embedding, rerank) returned an error or timed out |\n| `CAPABILITY_UNAVAILABLE` | `503` | No | A required server-side capability is missing (e.g. `everos[multimodal]` extra not installed, LibreOffice absent) — requires admin action, not retry |\n| `CONFIGURATION_ERROR` | `500` | No | A required configuration is missing or invalid (e.g. embedding model not set) |\n| `INTERNAL_ERROR` | `500` | No | Unhandled exception (internal details are logged, never leaked) |\n\n### error object\n\n| Field | Type | Description |\n|---|---|---|\n| `code` | `string` | One of the `ErrorCode` values listed above |\n| `message` | `string` | Human-readable reason. For `INVALID_INPUT` from request validation, **only the first** validation error is surfaced, formatted `\"<msg>: <dotted-loc>\"` with the leading `body` segment stripped (e.g. `\"Field required: messages\"`); a model-level validator with no field location surfaces just `\"<msg>\"` (e.g. `\"Value error, exactly one of user_id / agent_id must be provided\"`) |\n| `timestamp` | `string` | ISO-8601 with timezone offset (display tz) |\n| `path` | `string` | Request path, e.g. `/api/v2/memory/add` |\n\n> Unlike FastAPI's default, the full per-field validation array is **not**\n> returned — only the first error's message. A client that needs the\n> offending field can read the `<loc>` suffix in `message`.\n\n## Common types\n\n### MessageItem\n\nOne turn in a `/add` batch. Shape mirrors the OpenAI Chat Completions\nmessage structure plus a stable `sender_id` for indexing.\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `sender_id` | `string` | yes | — | `minLength=1` |\n| `sender_name` | `string \\| null` | no | `null` | — |\n| `role` | `\"user\" \\| \"assistant\" \\| \"tool\"` | yes | — | — |\n| `timestamp` | `integer` | yes | — | `> 0` — **Unix epoch milliseconds (ms)** per v1 contract |\n| `content` | `string \\| array<ContentItem>` | yes | — | — |\n| `tool_calls` | `array<ToolCall> \\| null` | no | `null` | — |\n| `tool_call_id` | `string \\| null` | no | `null` | — |\n\n**`sender_id`** — Stable identifier of the entity producing this turn.\nFor `role: \"user\"`, this is the `user_id` the server will index the\nextracted memory under: markdown lands at\n`users/<sender_id>/episodes/...`, and `/search` / `/get` queries with\n`user_id: \"<sender_id>\"` reach it. For `role: \"assistant\"` or\n`\"tool\"`, the field is informational (the LLM sees it during\nextraction but it is not used as an indexing key).\n\n**`sender_name`** — Optional human-readable display name. Lets the LLM\nuse a real name during extraction without changing the indexing key.\nFor example, set `sender_id: \"u_42\"` (stable internal id) and\n`sender_name: \"Alice\"` (what the LLM sees).\n\n**`role`** — Speaker role; one of:\n- `\"user\"` — content originates from a human (or human-acting client).\n- `\"assistant\"` — content from the AI assistant.\n- `\"tool\"` — output of a tool call; pair with `tool_call_id`.\n\n**`timestamp`** — Wall-clock anchor in **Unix epoch milliseconds (ms)**\nper the v1 API contract. Used by extraction to anchor the resulting\nepisode and by the daily-log writer to bucket the entry into the right\nfile (`episode-<YYYY-MM-DD>.md` etc.).\n\n> Implementation note: the algo layer auto-detects seconds vs ms (values\n> `>= 10^12` are treated as ms, smaller as seconds) for backward compat,\n> but **clients should send ms** to honour the contract.\n\n**`content`** — The message body.\n- A bare **string** is shorthand for a single text content item.\n- An **array of `ContentItem`** is for mixed-modality input (text +\n  image / pdf / audio / ...); non-text items are parsed by the\n  multimodal LLM configured via `EVEROS_MULTIMODAL__*` env vars. See\n  [ContentItem](#contentitem).\n\n**`tool_calls`** — When `role: \"assistant\"`, the tool calls the\nassistant emitted in this turn (OpenAI Chat Completions shape).\n\n**`tool_call_id`** — When `role: \"tool\"`, the `id` of the call this\nmessage is the response to. Required for `role: \"tool\"`; a tool row\nwithout it is rejected with `422`.\n\n### ContentItem\n\nMixed-modality message-body element. Carry the payload in exactly one\nof `text` / `uri` / `base64`; the others must be `null`. For\n`type: \"text\"` use `text`; for every **non-text** type use `uri`\n(`http(s)://`) or `base64` (with `ext`). Non-text items are routed\nthrough the multimodal parser, which needs a fetchable or decodable\npayload — a non-text item carrying only `text` returns `415`.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `type` | `\"text\" \\| \"image\" \\| \"audio\" \\| \"doc\" \\| \"pdf\" \\| \"html\" \\| \"email\"` | yes | — | — |\n| `text` | `string \\| null` | no | `null` | Required when `type: \"text\"` |\n| `uri` | `string \\| null` | no | `null` | `http(s)://` (fetched server-side) or `file://` (read from the server's local fs, guardrailed) pointer |\n| `base64` | `string \\| null` | no | `null` | Inline payload, plain base64 (no `data:` prefix) |\n| `ext` | `string \\| null` | no | `null` | File-extension hint when `uri` lacks one |\n| `name` | `string \\| null` | no | `null` | Display filename, used in logs |\n| `extras` | `object \\| null` | no | `null` | Provider-specific metadata, opaque to EverOS |\n\n**`type`** — The content kind. Each non-text type is dispatched to the\nmultimodal LLM. If the multimodal endpoint cannot handle the supplied\npayload, `/add` returns `415 Unsupported Media Type`.\n\n**`text`** — The literal text payload; valid **only** for\n`type: \"text\"`. A non-text type (including `\"html\"`) is always routed\nto the parser and must carry `uri` or `base64`; passing only `text` on\na non-text item returns `415`. To inline HTML as plain text, send it\nas `type: \"text\"`.\n\n**`uri`** — `http(s)://` or `file://` pointer to the asset. An\n`http(s)` uri is fetched by the server and dispatched by the response\nContent-Type (use it for assets hosted elsewhere — S3 / OSS presigned\nURL, http server). A `file://` uri is read from the **server's** local\nfilesystem (the path must be reachable by the server process), subject\nto these guardrails:\n\n- the resolved path (symlinks followed) must be an existing regular file;\n- its size must be ≤ `EVEROS_MULTIMODAL__FILE_URI_MAX_BYTES` (default 50 MiB);\n- when `EVEROS_MULTIMODAL__FILE_URI_ALLOW_DIRS` is set, the path must lie\n  within one of the listed roots (unset = any readable file, the\n  local-first default).\n\nA guardrail violation surfaces as `415`. Mutually exclusive with `base64`.\n\n**`base64`** — Inline binary payload, base64-encoded (no `data:`\nprefix). Mutually exclusive with `uri`.\n\n> **Size caution.** base64 inflates the payload ~4/3× on the wire, and\n> the encoded blob is held **verbatim in the server's staging buffer\n> (SQLite) from `/add` until the session is flushed** — a multi-MB PDF\n> becomes multi-MB of SQLite text for the buffer's lifetime. It is *not*\n> persisted past extraction (the memory cell and episode store only the\n> parsed text, never the raw bytes), but the transient footprint is\n> real, and a large inline blob also slows request parsing. **Prefer\n> `uri` (`http(s)://`) for large assets**: the buffer then holds only\n> the URL plus the parsed text, and the bytes are fetched transiently at\n> parse time rather than stored. Reserve `base64` for small assets or\n> when no reachable URL exists.\n\n**`ext`** — File-extension hint (`\"pdf\"`, `\"png\"`, `\"html\"`, ...). For\n`base64` payloads this drives modality dispatch and is effectively\n**required** (without it the server falls back to `mime`, then `415`s\nif neither resolves). For `uri` payloads it is optional — the fetched\nContent-Type usually suffices.\n\n**`name`** — Filename / human label for logging and traceability.\nDoes not affect parsing.\n\n**`extras`** — Free-form bag of provider-specific metadata (e.g.\ncaption, page hints). Opaque to EverOS; passed through to the\nmultimodal LLM context.\n\n### ToolCall\n\nOpenAI-shaped tool invocation attached to an assistant turn.\n\n| Field | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `id` | `string` | yes | — | Stable id; echoed by the paired `role: \"tool\"` turn via `tool_call_id` |\n| `type` | `string` | no | `\"function\"` | Only `\"function\"` is meaningful today |\n| `function` | [ToolFunction](#toolfunction) | yes | — | The function being called |\n\n### ToolFunction\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n| `name` | `string` | yes | Function name as registered with the agent |\n| `arguments` | `string` | yes | **JSON-encoded string** (OpenAI convention — not an object) |\n\n### FilterNode (filter DSL)\n\nA recursive boolean tree of predicates. Used by `/search.filters` and\n`/get.filters`. The Pydantic envelope only checks the recursive\ncombinator shape; field-level validity (which scalar fields are\nfilterable, which operators apply, value coercion) runs when the\nnode is compiled to a backend-specific filter clause server-side. Compile\nerrors surface as `422` with the offending field / operator in\n`error.message`.\n\n#### Shape\n\nA node is a JSON object whose keys are one of:\n\n| Key | Value | Notes |\n|---|---|---|\n| `AND` | `array<FilterNode>` | All child nodes must match. Omit if not needed |\n| `OR` | `array<FilterNode>` | At least one child node must match. Omit if not needed |\n| *<allowed field>* | scalar or operator map | Predicate on that field — see [Allowed fields](#allowed-fields) and [Operators](#operators) |\n\n`AND`, `OR`, and scalar predicates **mix freely at the same level**;\nthey are implicitly joined with `AND`. A node with only scalar keys is\nfine — `AND` / `OR` arrays are not required.\n\nExample — `session_id == \"demo-001\"` AND (`sender_id` contains `\"alice\"` OR `\"bob\"`):\n\n```json\n{\n  \"session_id\": \"demo-001\",\n  \"OR\": [\n    { \"sender_id\": \"alice\" },\n    { \"sender_id\": \"bob\" }\n  ]\n}\n```\n\n<a id=\"filter-allowed-fields\"></a>\n\n#### Allowed fields\n\nThese are the only field names that may appear as a predicate key.\nAnything else returns `422`.\n\n| Field | Column kind | Value type | Notes |\n|---|---|---|---|\n| `session_id` | string | `string` | Session the row was extracted from |\n| `parent_type` | string | `string` | Parent kind, e.g. `\"memcell\"` |\n| `parent_id` | string | `string` | Parent id |\n| `timestamp` | timestamp | `integer` (Unix epoch **ms** per v1 contract) or `string` (ISO-8601) | Implementation auto-detects `>= 10^12` as ms / smaller as sec for backward compat, but the contract is ms |\n| `sender_id` | array of strings | `string` | Matches via `array_has(sender_ids, <value>)` — checks whether the row's `sender_ids` list contains the given id |\n\nThe following names are **reserved** and rejected inside `filters`.\nThey must be set at the top of the request (`/search` or `/get`),\nnot inside the DSL:\n\n```\nowner_id   owner_type   app_id   project_id\n```\n\n<a id=\"filter-operators\"></a>\n\n#### Operators\n\nA predicate value may be a **scalar shorthand** (equality) or an\n**operator map** (multiple operators in one dict are AND-joined):\n\n| Operator | SQL | Applies to |\n|---|---|---|\n| `eq` | `=` | string, timestamp; on `sender_id` becomes `array_has(...)` |\n| `ne` | `!=` | string, timestamp |\n| `gt` | `>` | timestamp |\n| `gte` | `>=` | timestamp |\n| `lt` | `<` | timestamp |\n| `lte` | `<=` | timestamp |\n| `in` | `IN (...)` | string (list of values); on `sender_id` becomes `OR` of `array_has` |\n\nExamples:\n\n```json\n// Equality shorthand\n{ \"session_id\": \"demo-001\" }\n\n// Operator map (operators inside one dict are AND-joined)\n{ \"timestamp\": { \"gte\": 1748390400000, \"lt\": 1748400000000 } }\n\n// IN list\n{ \"session_id\": { \"in\": [\"demo-001\", \"demo-002\"] } }\n\n// Array-membership on sender_ids\n{ \"sender_id\": \"alice\" }                       // array_has(sender_ids, 'alice')\n{ \"sender_id\": { \"in\": [\"alice\", \"bob\"] } }    // OR of two array_has\n```\n\n### SearchMethod\n\n| Value | Behaviour |\n|---|---|\n| `\"keyword\"` | BM25 only — pure lexical match, no embedding cost |\n| `\"vector\"` | Dense vector ANN only — semantic recall, no lexical |\n| `\"hybrid\"` *(default)* | Reciprocal-rank fuse of BM25 + vector + optional scalar filter against the configured derived index backend |\n| `\"agentic\"` | Iterative cluster-path retrieval driven by a cross-encoder rerank loop; higher quality at higher latency / cost |\n\n`\"hybrid\"` is the default because it balances recall and precision\nwithout requiring the agentic loop. `\"agentic\"` calls the LLM in a loop\nand should be reserved for offline or background workflows.\n\n### GetMemoryType\n\n| Value | Track | Returned in `data.<plural>` |\n|---|---|---|\n| `\"episode\"` | user | `data.episodes` — [GetEpisodeItem](#getepisodeitem) |\n| `\"profile\"` | user | `data.profiles` — [GetProfileItem](#getprofileitem) |\n| `\"agent_case\"` | agent | `data.agent_cases` — [GetAgentCaseItem](#getagentcaseitem) |\n| `\"agent_skill\"` | agent | `data.agent_skills` — [GetAgentSkillItem](#getagentskillitem) |\n\n`memory_type` must match the requested owner kind: `\"episode\"` /\n`\"profile\"` require `user_id`; `\"agent_case\"` / `\"agent_skill\"`\nrequire `agent_id`. The mismatching combinations are rejected with\n`422`.\n\n## Endpoints\n\n### POST /api/v2/memory/add\n\nAppend a batch of messages to a session buffer. The server\naccumulates messages until the boundary detector decides the session\nis complete (or you force it with `/flush`); on boundary the LLM\nextracts a memory cell and the writers persist markdown + index rows.\n\nThe response distinguishes \"buffered\" from \"extracted this call\" via\nthe `status` field — see [Response body](#response-body) below.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | see [ScopeId](#scopeid-app_id-and-project_id) |\n| `messages` | `array<MessageItem>` | yes | — | 1–500 items |\n| `defer_extraction` | `boolean` | no | `false` | buffer durably without boundary or extraction LLM calls |\n| `async_mode` | `boolean` | no | `false` | queue the batch and return `202` immediately; see [Asynchronous mode](#asynchronous-mode) |\n\n**`session_id`** — Identifies the conversation buffer on the server.\nMessages POSTed with the same `(session_id, app_id, project_id)`\naccumulate into one extraction batch. Different `session_id`s never\nshare a buffer, even within the same scope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n**`messages`** — Ordered list of [MessageItem](#messageitem) to\nappend. Order matters (it is preserved when the buffer is later\nextracted). The 1–500 cap is a per-request safety bound, not a\nsession lifetime cap — you can call `/add` many times for the same\n`session_id`.\n\n**`defer_extraction`** — When `true`, `/add` only merges the messages\ninto the durable SQLite `unprocessed_buffer`. It skips boundary\ndetection, memcell creation, and all extraction pipelines, so batching\nclients can capture every turn cheaply and call `/flush` after an idle\nwindow, a size threshold, or a session switch. A successful deferred\nadd always reports `status: \"accumulated\"`.\n\n**`async_mode`** — When `true`, `/add` persists the batch to the durable\nmemorize queue and returns `202 Accepted` without running boundary\ndetection or extraction; a background worker delivers queued batches in\narrival order per session, several sessions at a time. See\n[Asynchronous mode](#asynchronous-mode) for the `202` body, the ordering\nrule and how to poll progress.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `status` | `\"accumulated\" \\| \"extracted\"` | What the server did with the batch |\n\n**`message_count`** — Always equal to `len(messages)` on a `200` —\nthere is no partial accept. The field is present mainly for log\ncorrelation.\n\n**`status`** — Two-state outcome:\n- `\"accumulated\"` — Messages were added to the buffer; the boundary\n  detector did **not** trigger this call. The buffer remains open.\n- `\"extracted\"` — During this call the boundary detector tripped; the\n  LLM ran extraction and the writers persisted at least one memory\n  cell. The buffer is empty after the call returns.\n\n`202 Accepted` — only with `async_mode`, or when the session already has\nqueued batches (see [Asynchronous mode](#asynchronous-mode)) — returns a\nSuccessEnvelope wrapping:\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"queued\"` | The batch is persisted; delivery happens in the background |\n| `message_count` | `integer` | Number of messages accepted in this call |\n| `pending` | `integer` | Unfinished queued batches of this session, this one included |\n\nThe envelope's `request_id` is the handle for\n[`GET /tasks/{task_id}`](#get-apiv2taskstask_id).\n\n#### cURL example\n\n```bash\nTS=$(( $(date +%s) * 1000 ))\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/add \\\n  -H 'Content-Type: application/json' \\\n  -d \"{\n    \\\"session_id\\\": \\\"demo-002\\\",\n    \\\"app_id\\\": \\\"default\\\",\n    \\\"project_id\\\": \\\"default\\\",\n    \\\"messages\\\": [\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $TS, \\\"content\\\": \\\"I love climbing in Yosemite every spring.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+10)), \\\"content\\\": \\\"My favorite coffee shop is Blue Bottle in SOMA.\\\"},\n      {\\\"sender_id\\\": \\\"alice\\\", \\\"role\\\": \\\"user\\\", \\\"timestamp\\\": $((TS+20)), \\\"content\\\": \\\"I bike to work most days.\\\"}\n    ]\n  }\"\n```\n\nResponse (real capture):\n\n```json\n{\n    \"request_id\": \"ae78d3f689c941eea135893e702fd171\",\n    \"data\": {\n        \"message_count\": 3,\n        \"status\": \"accumulated\"\n    }\n}\n```\n\n### POST /api/v2/memory/flush\n\nForce the boundary detector to decide **now** for the given session\nbuffer. The LLM runs extraction (one call) regardless of whether the\nheuristic would have tripped on its own. Useful at the end of a chat\nor agent run to make sure pending context becomes durable memory.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `session_id` | `string` | yes | — | length 1–128 |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n\n**`session_id`** — Identifies which buffer to flush. Must match the\n`session_id` of prior `/add` calls in the same `(app_id, project_id)`\nscope.\n\n**`app_id` / `project_id`** — Scope identifiers; see\n[ScopeId](#scopeid-app_id-and-project_id).\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `status` | `\"extracted\" \\| \"no_extraction\"` | What happened |\n\n**`status`**:\n- `\"extracted\"` — The buffer had at least one message; extraction ran\n  and produced at least one memory cell.\n- `\"no_extraction\"` — The buffer was empty (no prior `/add` calls for\n  this `(session_id, app_id, project_id)`, or it was already flushed).\n\n`202 Accepted` with the same body as `/add` (`message_count` is `0`) only\nwhen the session already has queued batches, so the flush runs after them;\n`/flush` has no `async_mode` of its own. See\n[Asynchronous mode](#asynchronous-mode).\n\nA `200` from `/flush` is synchronous with respect to markdown persistence: by the\ntime the response returns, the new entry is on disk. Derived index\nsync is still asynchronous — see\n[Eventual consistency](#eventual-consistency).\n\n#### cURL example\n\n```bash\ncurl -X POST http://127.0.0.1:8000/api/v2/memory/flush \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"session_id\":\"demo-002\",\"app_id\":\"default\",\"project_id\":\"default\"}'\n```\n\nResponse (real capture; this call took several seconds because of the\nextraction LLM call):\n\n```json\n{\n    \"request_id\": \"e65bcf4f56c042e39cdf50866810672c\",\n    \"data\": {\n        \"status\": \"extracted\"\n    }\n}\n```\n\n### Asynchronous mode\n\n`async_mode: true` on `/add` turns the call into \"accept and return\": the\nrequest body is written to the SQLite `memorize_inbox` table in one\ntransaction and the response is `202 Accepted`. `/flush` has no\n`async_mode` (the Cloud API has no asynchronous flush either): it runs\ninline, except that a flush on a session with queued batches joins the\nqueue behind them and answers `202` (see Ordering). An in-process\nworker then delivers queued batches to the same code path a synchronous\ncall uses: each session's batches in the order they were accepted, several\nsessions at a time. The field name, the `202` body\n(`status: \"queued\"` plus the envelope's `request_id`) and the status\nvocabulary (`queued` / `processing` / `success` / `failed`) match the\nEverOS Cloud gateway, so one client can talk to both.\n\n**Ordering.** Batches of one session are delivered strictly in arrival\norder, and a `/flush` runs after every `/add` accepted before it. To keep\nthat true, a synchronous call (`async_mode` omitted or `false`) on a\nsession that still has queued batches is queued too and answered with\n`202` — otherwise it would overtake work the client was already told is\naccepted. Clients that never use `async_mode` never see this: their\nsessions never have queued batches. Sessions are independent of each\nother: up to `max_concurrency` batches (default 4) run at once, never two\nfrom one session, so one slow session does not hold up the others, and\nthere is no ordering between sessions. Like the Cloud consumer, which runs\ngroups in parallel and each group in order.\n\n**Backpressure.** The queue is bounded. A batch is rejected before it is\nstored when:\n\n| Condition | HTTP | `error.code` | Default limit |\n|---|---|---|---|\n| Serialised body too large | `413` | `PAYLOAD_TOO_LARGE` | 4 MiB |\n| Session has too many unfinished batches | `429` | `QUEUE_SATURATED` | 50 |\n| Process has too many unfinished batches | `429` | `QUEUE_SATURATED` | 200 |\n\nThe caps are checked before the insert without a lock, so a burst of\nconcurrent requests can overshoot them by a few rows. A rejected batch was\n**not** accepted: retry later or keep it client-side.\n`pending` in every `202` body lets a client slow down before hitting `429`.\nLimits are configured under `[memorize.async_queue]`, see\n[configuration.md](configuration.md#memorizeasync_queue).\n\n**Failure.** A batch whose delivery raises is marked `failed` and skipped\nso its session keeps moving; `GET /tasks/{request_id}` shows `error`.\nThere is no automatic retry by default (`max_attempts = 1`), because the\nmemorize pipeline is not atomic: boundary detection writes the memcell and\nconsumes the session buffer before extraction runs. A failure before that\npoint (the common one: the boundary LLM call failed) leaves nothing behind,\nand re-sending the batch is safe. A failure after it leaves the memcell\nwithout its episode; re-sending then cuts a second memcell, and a `/flush`\nfinds an empty buffer. `last_error` does not say which step failed. The\nserver never re-drives a failed batch and deletes its row with the others\nafter seven days. Synchronous calls have the same property; they just hand\nthe decision to the client as a 5xx.\n\n**Delivery is at-least-once across a crash.** A row that was in flight\nwhen the process died is delivered again on the next start. If the first\ndelivery had already written its memcell, an `/add` replay duplicates that\nmemory; a `/flush` replay finds an empty buffer and does nothing, so a crash\nthat fell between the memcell write and the episode write leaves that\nmemcell without its episode. A\nnormal shutdown waits up to 30 seconds for the rows in flight (up to\n`max_concurrency` of them, one per session); a row that takes longer, or a\nprocess manager that kills earlier (`docker stop` sends SIGKILL after 10\nseconds by default), is replayed the same way, so one restart can replay\nup to `max_concurrency` batches. So is a\nbatch whose delivery had just failed when the process died before its\n`failed` mark was written, and a batch whose status write was lost (see\nStuck rows) if the server restarts before the row is marked. Replays are\nbounded: a batch that has been replayed `max_replays` times (default 3)\nwithout ever finishing is marked `failed` with `error_code`\n`replays_exhausted` instead of being requeued again, so a batch that kills\nthe process cannot crash-loop it.\n\n**Operations.** `GET /health` carries a `memorize_queue` block: `healthy`\n(the delivery loop is alive), `pending`, `oldest_pending_age_s`, `failed`,\n`loop_restarts` and `last_loop_error_at`. The probe only observes: a loop\nthat dies relaunches itself (with backoff) and is relaunched by the next\n`/add`, and `loop_restarts` counts those. Alert on `healthy` and on\n`loop_restarts` changing; a growing `oldest_pending_age_s` with a live loop\nmeans delivery is slower than arrival.\n\n**Retention.** Every queue row, whatever its status, is deleted seven days\n(`row_retention_hours`) after it was created. `GET /tasks/{task_id}` returns `404`\nafter that. A delivered row drops its request body at once. A `failed` row\nkeeps its body (in `memorize_inbox.payload_json`, found by `request_id`)\nuntil purged, and failed rows are not capped: an outage in which every batch\nfails keeps every body accepted during it for the retention period. SQLite\nreuses the freed pages but does not shrink `system.db`. A row that ages out\nwhile still `queued` was never delivered; the purge logs a warning when that\nhappens.\n\n**Stuck rows.** A row left `processing` because its status write was lost\n(SQLite busy past its timeout, disk full) is marked `failed` before the\nworker claims its next batch, or at its next scheduling pass if that write\nfails too, so a session's `pending` does not stay non-zero because of it;\nbatches the worker is still running are never swept. Its `last_error`\nreads `status write lost; outcome unknown`:\nthe batch may well have been delivered in full (the lost write is usually\nthe `success` one), so check the session's memories before re-sending it.\nIf the server restarts before that mark lands, the row is replayed like a\ncrash (see above).\n\nSetting `[memorize.async_queue] enabled = false` closes the door:\n`async_mode` is accepted but ignored and new calls run synchronously unless\ntheir session still has queued rows. Rows accepted while it was on are still\ndelivered by the worker, and a session that still has such rows still queues\nits synchronous calls behind them, so no drain is needed before switching.\nDowngrading to a release without `async_mode` is different: queued rows are\nneither delivered nor deleted there. Set `enabled = false` and restart first,\nso `async_mode` requests stop adding rows (a synchronous call still queues\nbehind its session's backlog until it drains); wait until\n`select count(*) from memorize_inbox where status in ('queued', 'processing')`\non `system.db` is 0; then stop and downgrade.\n\n### GET /api/v2/tasks/{task_id}\n\nThe outcome of one accepted request. The path and the body are the EverOS\nCloud gateway's (`GET /api/v2/tasks/{task_id}`), so a client polls both the\nsame way; `task_id` is the `request_id` a `202` carried.\n\n`200 OK`, or `404 NOT_FOUND` when the row never existed or has aged out of\n`row_retention_hours`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | The request id |\n| `status` | `\"queued\" \\| \"processing\" \\| \"success\" \\| \"failed\"` | Where the batch is. Cloud can also report `pending`; this build never does |\n| `task_type` | `\"memory_add\" \\| \"memory_flush\"` | — |\n| `error` | `string \\| null` | Summary of the failure, if any |\n| `error_code` | `string \\| null` | `status_write_lost` for a row the sweep failed, `replays_exhausted` for one replayed `max_replays` times without finishing, otherwise the exception type that failed the batch |\n| `created_at` | `string` (ISO 8601) | Accepted at |\n| `finished_at` | `string \\| null` | Set once `status` is `success` or `failed` |\n\n### GET /api/v2/tasks\n\nAccepted requests, newest first, one page at a time. Mirrors the Cloud\ngateway's listing.\n\n#### Query parameters\n\n| Parameter | Type | Required | Default | Notes |\n|---|---|---|---|---|\n| `session_id` | `string` | no | — | Only this session's requests |\n| `app_id` / `project_id` | `string` | no | `\"default\"` | Scope for `session_id` |\n| `status` | `string` | no | — | One of the four statuses; anything else is `422 INVALID_INPUT` |\n| `page` | `integer` | no | `1` | `≥ 1` |\n| `page_size` | `integer` | no | `20` | `1..100`, larger values are capped |\n\n#### Response body\n\n| Field | Type | Notes |\n|---|---|---|\n| `total` | `integer` | Rows matching the filters, all pages |\n| `page` / `page_size` | `integer` | As requested (`page_size` after capping) |\n| `items` | `array<TaskItem>` | The page, newest first; fields as in `GET /tasks/{task_id}` |\n\nDrain a session before exiting by polling `?session_id=X&status=queued` and\n`?session_id=X&status=processing` until both `total` are `0`. That means\nevery batch reached the memorize pipeline (unless rows aged out before\ndelivery, see Retention); it does not mean every message is an episode yet:\nmessages still in the session buffer wait for the boundary detector or the\nnext `/flush`.\n\n### POST /api/v2/memory/search\n\nHybrid retrieval over the memory store. Combines BM25, dense vector\nANN, optional scalar filtering, optional cross-encoder rerank, and\noptional final LLM rerank. Returns ranked items grouped by kind.\n\n#### Request body\n\n| Field | Type | Required | Default | Constraints |\n|---|---|---|---|---|\n| `user_id` | `string \\| null` | XOR with `agent_id` | `null` | `minLength=1` if set |\n| `agent_id` | `string \\| null` | XOR with `user_id` | `null` | `minLength=1` if set |\n| `app_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `project_id` | `string` *(ScopeId)* | no | `\"default\"` | — |\n| `query` | `string` | yes | — | `minLength=1` |\n| `method` | [SearchMethod](#searchmethod) | no | `\"hybrid\"` | — |\n| `top_k` | `integer` | no | `-1` | `-1` or `1..100` |\n| `radius` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `min_score` | `number \\| null` | no | `null` | `0.0 ≤ x ≤ 1.0` if set |\n| `include_profile` | `boolean` | no | `false` | — |\n| `enable_llm_rerank` | `boolean` | no | `false` | — |\n| `filters` | [FilterNode](#filternode-filter-dsl) `\\| null` | no | `null` | — |\n\n**`user_id` / `agent_id`** — **Exactly one** must be set. Determines\nwhich track is searched: `user_id` → user-memory (episodes /\nprofiles); `agent_id` → agent-memory (cases / skills).\n\n**`app_id` / `project_id`** — Scope identifiers; results never cross\nscopes.\n\n**`query`** — The retrieval query. The same string is fed to BM25\ntokenization and to the embedding model, then the two recall lists\nare fused.\n\n**`method`** — Retrieval method; see [SearchMethod](#searchmethod).\nDefault `\"hybrid\"` is the recommended starting point.\n\n**`top_k`** — Maximum number of items to return per kind. Two modes:\n- `-1` (default) — \"Use server defaults.\" Recall falls back to a\n  fixed internal cap, and a server-side `radius` default kicks in if\n  the caller did not pass one (see `radius` below). Use for \"give me\n  whatever you find worth returning.\"\n- `1..100` — Explicit cap. Recall is sized as `top_k × multiplier`\n  and **no** default `radius` is applied unless you set it yourself.\n\nValues of `0` or outside `-1` / `1..100` are rejected with `422`.\n\n**`radius`** — Optional **cosine-similarity threshold** in `[0.0, 1.0]`.\nCandidates whose score is below this value are dropped — use it to cut\na long tail of weak matches. Three-level fallback for the effective\nradius:\n\n1. Caller-supplied `radius` (including the literal `0.0`) always wins.\n2. With `top_k=-1` and no caller-supplied `radius`, a server-side\n   default radius kicks in.\n3. With `top_k>0` and no caller-supplied `radius`, no threshold is\n   applied (`null`).\n\n**`min_score`** — Optional **post-fusion relevance floor** in\n`[0.0, 1.0]`. Results below this score are evicted after fusion,\nindependent of `radius` (which is a per-recall cosine threshold).\n\n**`include_profile`** — When `user_id` is set, also fetch the user's\nprofile and include it in `data.profiles`. The profile is not\nranked; `score` is `null`. Ignored when `agent_id` is set.\n\n**`enable_llm_rerank`** — Opt-in LLM rerank pass for\n`method: \"hybrid\"`. Applies to `agent_case` and `agent_skill` fusion\nonly; the episode hybrid path has built-in fact eviction and\nignores this flag. Adds one LLM call per request. Ignored by\n`keyword` / `vector` (no fusion to rerank) and `agentic` (uses its\nown cross-encoder loop).\n\n**`filters`** — Optional filter-DSL node; see\n[FilterNode](#filternode-filter-dsl). Applied **before** ranking, so\nit does not perturb the ranker.\n\n#### Response body\n\n`200 OK` returns a SuccessEnvelope wrapping `SearchData`. All five\narrays are always present so client code can iterate without\nbranching on owner type; arrays that do not apply to the requested\nowner kind stay as `[]`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `episodes` | `array<SearchEpisodeItem>` | Populated when `user_id` is set |\n| `profiles` | `array<SearchProfileItem>` | Populated when `user_id` is set **and** `include_profile=true` |\n| `agent_cases` | `array<SearchAgentCaseItem>` | Populated when `agent_id` is set |\n| `agent_skills` | `array<SearchAgentSkillItem>` | Populated when `agent_id` is set |\n| `unprocessed_messages` | `array<UnprocessedMessageDTO>` | Populated **only** when `filters.session_id` is a top-level eq scalar; otherwise stays `[]`. Independent of `user_id` / `agent_id` (buffer rows have no owner attribution — boundary detection runs before owner inference) |\n\n#### SearchEpisodeItem\n\nUser-track conversation episode hit. `score` is the fused retrieval\nscore; `atomic_facts` lists single-sentence facts that matched the\nquery within this episode (already nested, no separate call needed).\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_ep_<YYYYMMDD>_<NNN>` |\n| `user_id` | `string \\| null` | Owner of this episode |\n| `app_id` | `string` | Scope where the episode lives |\n| `project_id` | `string` | Scope where the episode lives |\n| `session_id` | `string` | The `session_id` whose messages produced this episode |\n| `timestamp` | `string` | ISO-8601 with timezone offset; default `UTC` renders as `Z`, non-UTC defaults render as `±HH:MM` — see [Conventions](#conventions) |\n| `sender_ids` | `array<string>` | Distinct `sender_id`s in the underlying messages |\n| `summary` | `string` | Short summary (~200 chars), suitable for list-view rendering |\n| `subject` | `string` | One-line subject / title |\n| `episode` | `string` | Full extracted narrative |\n| `type` | `\"Conversation\"` | Reserved; today only conversation-derived episodes ship |\n| `score` | `number` | Fused retrieval score for this episode |\n| `atomic_facts` | `array<SearchAtomicFactItem>` | Sub-facts extracted from the same episode that matched the query |\n\n> **`id` is unique within `(app_id, project_id)`.** The same `id` can appear\n> under two projects (each project keeps its own daily files and sequence);\n> address a row globally by `(app_id, project_id, id)`. Within one response\n> ids never repeat, because a request is always scoped to one project.\n\n#### SearchAtomicFactItem\n\nA single-sentence fact pulled out of an episode during extraction.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<user_id>_af_<YYYYMMDD>_<NNN>` |\n| `content` | `string` | The fact as a single sentence |\n| `score` | `number` | Same score scale as the parent episode |\n\n#### SearchProfileItem\n\nUser profile. Only populated when `include_profile=true` and\n`user_id` is set.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Profile id |\n| `user_id` | `string \\| null` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `profile_data` | `object` | Free-form structured profile fields produced by extraction (schema is profile-specific) |\n| `score` | `number \\| null` | `null` for direct fetches (no query-aware profile ranking yet) |\n\n#### SearchAgentCaseItem\n\nAgent-track case hit. Returned only when the request uses `agent_id`.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_ac_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `session_id` | `string` | The agent run this case came from |\n| `task_intent` | `string` | What the agent was trying to do (one-sentence framing) |\n| `approach` | `string` | How the agent went about it |\n| `quality_score` | `number` | Quality assessed at extraction time, in `[0, 1]` |\n| `key_insight` | `string \\| null` | One-line takeaway; `null` if extraction did not produce one |\n| `timestamp` | `string` | ISO-8601 UTC of when the run happened |\n| `score` | `number` | Fused retrieval score for this case |\n\n#### SearchAgentSkillItem\n\nAgent-track skill hit — a named procedural memory produced by\nclustering related cases.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | `<agent_id>_sk_<YYYYMMDD>_<NNN>` |\n| `agent_id` | `string` | Owner |\n| `app_id` | `string` | Scope |\n| `project_id` | `string` | Scope |\n| `name` | `string` | Skill name (a stable, human-meaningful slug) |\n| `description` | `string` | One-line description |\n| `content` | `string` | Full skill body (procedure / playbook text) |\n| `confidence` | `number` | Extraction confidence in `[0, 1]` |\n| `maturity_score` | `number` | Maturity assessed at clustering time, in `[0, 1]` |\n| `source_case_ids` | `array<string>` | `agent_case` ids that contributed to this skill |\n| `score` | `number` | Fused retrieval score for this skill |\n\n#### UnprocessedMessageDTO\n\nA raw message still sitting in the boundary-detection buffer — sent to\n`/add` but not yet carved into an episode / case. Returned **only**\nwhen the request's `filters` contains `session_id` as a top-level eq\nscalar (`{\"session_id\": \"<sid>\"}`); compound shapes (`AND` / `OR`\ncombinators, operator maps such as `{\"eq\": ...}` / `{\"in\": ...}`) do\nnot trigger the lookup because there is no defensible buffer-scope\nmapping for them. Buffer rows have no `user_id` / `agent_id`\nattribution, so `session_id` is the only meaningful query dimension.\n\n| Field | Type | Notes |\n|---|---|---|\n| `id` | `string` | Original `message_id` from `/add` |\n| `app_id` | `string` | Scope where the buffered message lives |\n| `project_id` | `string` | Scope where the buffered message lives |\n| `session_id` | `string` | Matches the requested `filters.session_id` |\n| `sende\n\n<!-- [TRUNCATED AT 50KB] -->\n\n<!-- File: docs/architecture.md (Tokens: ~2806 | Category: project-spec) -->\n# Architecture\n\n> Companion: [.claude/rules/architecture.md](../.claude/rules/architecture.md) (auto-loaded coding rules)\n\n## DDD layered architecture\n\n```\n┌──────────────────────────────────────────────────────┐\n│  entrypoints/  (Presentation)                         │\n│    cli + api                                          │\n├──────────────────────────────────────────────────────┤\n│  service/      (Application — Use Case orchestration) │\n│    memorize / search / get / knowledge                │\n├──────────────────────────────────────────────────────┤\n│  memory/       (Domain — Business core)               │\n│    models + extract + search + cascade + prompt_slots │\n│    + reflection + strategies + get + events            │\n├──────────────────────────────────────────────────────┤\n│  infra/persistence  (Storage adapters; infra/ may host other adapter types)    │\n│    markdown + sqlite + derived index                  │\n└──────────────────────────────────────────────────────┘\n\nCross-cutting (used by all layers, depends on none):\n  component/  ← Injectable providers (LLM / Embedding / parser / config / utils)\n  core/       ← Runtime base (observability / lifespan / context / errors / persistence / middleware)\n  config/     ← Configuration data (Settings schema + default.toml)\n```\n\n## Dependency direction (single-direction, enforced)\n\n```\nentrypoints → service → memory → infra\n```\n\n| from → to | Allowed? |\n|---|---|\n| entrypoints → service | ✅ |\n| entrypoints → memory / infra | ❌ (must go through service) |\n| service → memory | ✅ |\n| memory → infra | ✅ |\n| memory → service | ❌ |\n| infra → memory | ❌ |\n| infra cross-subpackage (e.g. lancedb → markdown within persistence/) | ❌ (use service to orchestrate); one carve-out: every index backend reads the shared record declarations in `persistence/records/` (`lancedb → records` is whitelisted in import-linter) |\n| any → component / core / config | ✅ (cross-cutting) |\n| `infra/persistence/index → component.embedding` | ✅ — the only network-capable provider `infra` reaches for. Startup measures the embedding model's real output width before the index is opened for use; nothing else can answer that question, and asking it from a higher layer would mean every index user had to remember to ask. Elsewhere `infra` uses `component` only for `utils.datetime`. |\n\nEnforced via `import-linter` in CI:\n\n```toml\n[tool.importlinter]\nroot_packages = [\"everos\"]\n\n[[tool.importlinter.contracts]]\nname = \"Layered architecture\"\ntype = \"layers\"\nlayers = [\n    \"everos.entrypoints\",\n    \"everos.service\",\n    \"everos.memory\",\n    \"everos.infra\",\n]\n```\n\n## Storage three-piece set\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│             md-first storage stack                              │\n└────────────────────────────────────────────────────────────────┘\n\n   ┌──────────────┐   ┌──────────────┐   ┌─────────────────┐\n   │   Markdown   │   │   SQLite     │   │ Derived index   │\n   │  (truth)     │   │  (state)     │   │ LanceDB/Milvus  │\n   ├──────────────┤   ├──────────────┤   ├─────────────────┤\n   │ entries +    │   │ change queue │   │ vector ANN      │\n   │ frontmatter  │   │ + state/LSN  │   │ BM25 (Tantivy)  │\n   │ Git friendly │   │ buffer /     │   │ scalar filter   │\n   │ Obsidian OK  │   │   audit      │   │ multi-modal     │\n   └──────────────┘   └──────────────┘   └─────────────────┘\n          │                  │                    │\n          ▼                  ▼                    ▼\n    memory-root/         .index/sqlite/      .index/<backend>/\n   (truth source)       (system data)       (rebuildable)\n```\n\n## Write path\n\n```\nExternal message\n       │\n       ▼\n1. service.memorize           (entrypoint of write path)\n       │\n       ▼\n2. memory.extract.pipeline    (calls everalgo)\n       │\n       ▼\n3. infra.persistence.markdown.write       (atomic: tmp + fsync + rename)\n       │  ✅ md write success → return immediately\n       │\n   ┌───┴────┐\n   │        │\n   ▼        ▼\n4a. SQLite   4b. memory.cascade  (async daemon)\n    audit        watches md → diff entries → index sync\n```\n\n**Key guarantee**: md write is strongly consistent (fsync). The derived\nindex is eventually consistent. Index backend unavailability does not block\nresponse — changes buffer in the SQLite `md_change_state` queue, replayed on\nrecovery.\n\n## Read path\n\n```\nUser query\n   │\n   ▼\n1. service.search\n   │\n   ▼\n2. memory.search (hybrid)     BM25 + vector ANN + scalar filter\n                              through the configured index backend\n   │\n   ▼\n3. (optional) read md         original markdown for context\n   │\n   ▼\n   Return\n```\n\n## Key components\n\n### `memory/extract/`\n\n```\nextract/\n├── ingest/      Standardized message intake + multi-modal parser dispatch\n├── parser/      Input parsing (format normalization, message preprocessing)\n├── pipeline/    Main extraction pipeline (calls everalgo + dual-track split + writes store)\n└── evolution/   Async memory evolution (event/counter/cron triggers)\n```\n\n### `memory/cascade/`\n\nDaemon that watches markdown changes and syncs to the configured derived\nindex backend:\n\n- inotify / FSEvents file watcher (cross-platform via `watchdog`)\n- 500ms debounce\n- Entry-level diff (added / changed / removed)\n- Per-entry index upsert / delete (text + vector columns update together)\n- LSN-based crash recovery via the SQLite `md_change_state` queue\n- Handlers for all eight business kinds: episode, atomic_fact, foresight,\n  user_profile, agent_case, agent_skill, knowledge_document, knowledge_topic\n\n### `memory/prompt_slots/`\n\nThree-layer prompt overlay:\n\n```\nconfig/prompt_slots/*.yaml          (Layer 1: defaults, ships with package)\n       ↓\n~/.everos/prompt_slots/*.yaml       (Layer 2: app-level override)\n       ↓\nruntime override                    (Layer 3: per-call override)\n```\n\nExtractors may accept a prompt-override parameter; EverOS supplies overrides for episode and boundary-detection prompts, and falls back to the algo-bundled default elsewhere — no hardcoded prompts in algorithm code.\n\n### `memory/reflection/`\n\nOffline memory self-improvement. The orchestrator (`orchestrator.py`)\nimplements the Select → Merge → Re-extract → Deprecate pipeline, merging\nfragmented episodes within a cluster into a single coherent narrative. Driven\nby the `reflect_episodes` OME strategy (cron, disabled by default).\n\n### `memory/strategies/`\n\nOME strategy implementations — one file per strategy:\n\n- `extract_atomic_facts` / `extract_foresight` / `extract_user_profile` — user pipeline\n- `extract_agent_case` / `extract_agent_skill` — agent pipeline\n- `reflect_episodes` — offline episode consolidation (cron)\n- `trigger_profile_clustering` / `trigger_skill_clustering` — clustering triggers\n\n### `core/observability/`\n\nThree-piece observability:\n\n- `metrics/` — Prometheus counter / gauge / histogram + global registry\n- `logging/` — structlog with context processor (trace_id propagation)\n- `tracing/` — OpenTelemetry tracer + span helpers\n\n## Markdown layout\n\n```\n~/.everos/                                  # memory root (default; EVEROS_ROOT)\n└── <app_id>/<project_id>/                  # scope (\"default\" → default_app/default_project)\n    ├── users/<user_id>/\n    │   ├── user.md                                     # profile (single-file rewrite)\n    │   ├── episodes/episode-<YYYY-MM-DD>.md            # daily-log append\n    │   ├── .atomic_facts/atomic_fact-<YYYY-MM-DD>.md   # hidden, framework-derived\n    │   └── .foresights/foresight-<YYYY-MM-DD>.md       # hidden, framework-derived\n    ├── agents/<agent_id>/\n    │   ├── .cases/agent_case-<YYYY-MM-DD>.md           # hidden, framework-derived\n    │   └── skills/skill_<name>/SKILL.md                # named-dir\n    └── knowledge/                                      # global shared knowledge\n```\n\nSystem-managed entries (`.index/`, `.tmp/`) and `ome.toml` live directly\nunder the memory root.\nFull tree + frontmatter chassis: [storage_layout.md](storage_layout.md) and\n[how-memory-works.md](how-memory-works.md). Frontmatter has 4-tier field\nprotection (L1 read-only / L2 system / L3 business / L4 user).\n\n## everalgo boundary\n\n`everalgo` is a set of PyPI-published packages (`everalgo-user-memory`,\n`everalgo-agent-memory`, `everalgo-rank`, `everalgo-knowledge`, plus the\noptional `everalgo-parser` extra), imported under the `everalgo` namespace,\nholding **only memory extraction algorithms**:\n\n- `everalgo.parser` — multi-modal parsing (optional `[multimodal]` extra)\n- `everalgo.user_memory` — ConvMemCell / Episode / Foresight / AtomicFact / Profile extractors\n- `everalgo.agent_memory` — AgentMemCell / Case / Skill extractors\n- `everalgo.rank` — boundary detection / fusion + rerank\n- `everalgo.knowledge` — KnowledgeExtractor (document parse + topic extraction)\n\neveralgo is:\n\n- **Stateless** — pure functions, no class hierarchy\n- **No I/O** — does not touch md files, derived indexes, or SQLite\n- **No prompts inline** — extractors that accept a prompt-override parameter use the project-supplied value; others use their algo-bundled defaults\n\nThis boundary lets everalgo be reused across product forms (this open-source build, EverOS Cloud, OpenClaw plugins, etc.).\n\n## Error handling architecture\n\n### Exception hierarchy\n\nAll application exceptions derive from `AppError` (`core/errors.py`),\nsplit into four branches by nature:\n\n```\nAppError\n├── DomainError                    (client-side / business-rule violations)\n│   ├── NotFoundError              → 404 NOT_FOUND\n│   │   ├── DocumentNotFoundError\n│   │   └── TopicNotFoundError\n│   ├── ConflictError              → 409 CONFLICT\n│   │   └── DuplicateDocumentError\n│   ├── InvalidInputError          → 422 INVALID_INPUT\n│   │   ├── ExtractionEmptyError   → 422 EXTRACTION_EMPTY\n│   │   └── FilterError\n│   ├── PathTraversalError         → 400 BAD_REQUEST\n│   └── UnsupportedModalityError   → 415 UNSUPPORTED_FORMAT\n├── InfrastructureError            (transient, retryable)     → 503\n│   ├── StorageError\n│   ├── VectorStoreError\n│   └── ExternalServiceError\n│       ├── LLMServiceError\n│       ├── EmbeddingServiceError\n│       └── RerankServiceError\n├── CapabilityError                (permanent, not retryable) → 503\n│   └── MultimodalNotEnabledError\n└── ConfigurationError             (misconfiguration)         → 500\n```\n\n### Error propagation strategy\n\nExceptions are raised at the layer where the error is detected and\npropagate naturally — **service and route layers do not catch-and-wrap**.\nThe entrypoints layer registers per-type exception handlers\n(`entrypoints/api/exception_handlers.py`) via Starlette's MRO dispatch.\nEach handler converts the exception into a canonical error envelope with\nan `ErrorCode` enum value and the appropriate HTTP status code.\n\n### Boundary translation\n\nThird-party exception types are translated at the component boundary to\nprevent external types from leaking into upper layers:\n\n- `everalgo.llm.LLMError` → `LLMServiceError` at `component/parser/_core.py`\n- Embedding / rerank provider errors → `EmbeddingServiceError` / `RerankServiceError` at their respective protocol modules\n\n## Further reading\n\n- [docs/overview.md](overview.md) — vision and scope\n- [docs/engineering.md](engineering.md) — engineering tooling and CI / CD\n- [.claude/rules/architecture.md](../.claude/rules/architecture.md) — short-form rules for Claude Code\n\n<!-- File: use-cases/claude-code-plugin/plugin.json (Tokens: ~75 | Category: plugin-manifest) -->\n{\n  \"name\": \"evermem\",\n  \"version\": \"0.2.0\",\n  \"description\": \"Automatically recalls relevant memories from past sessions and injects them into Claude's context\",\n  \"author\": {\n    \"name\": \"EverMem\"\n  },\n  \"keywords\": [\"memory\", \"context\", \"recall\", \"persistence\"]\n}\n</yakaai_skills>"}