Metadata-Version: 2.5
Name: mem8-fs
Version: 0.3.7
Summary: Agent memory framework: md-based, sqlite-indexed, RRF-fused retrieval
Author: mem8 contributors
License: MIT License
        
        Copyright (c) 2026 mem8 contributors
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: agent,ai,llm,memory,semantic-search,sqlite
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: anthropic-tokenizer>=0.1
Requires-Dist: anthropic>=0.40
Requires-Dist: click>=8.1
Requires-Dist: numpy>=1.26
Requires-Dist: pydantic-settings>=2.0
Requires-Dist: pydantic>=2.6
Requires-Dist: sqlite-vec>=0.1.6
Provides-Extra: all-databases
Requires-Dist: chromadb<2.0,>=1.0; extra == 'all-databases'
Requires-Dist: lancedb<1.0,>=0.20; extra == 'all-databases'
Provides-Extra: chroma
Requires-Dist: chromadb<2.0,>=1.0; extra == 'chroma'
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: evals
Requires-Dist: pytest>=8.0; extra == 'evals'
Requires-Dist: pyyaml>=6; extra == 'evals'
Requires-Dist: rich>=13; extra == 'evals'
Requires-Dist: types-pyyaml>=6.0; extra == 'evals'
Provides-Extra: lancedb
Requires-Dist: lancedb<1.0,>=0.20; extra == 'lancedb'
Provides-Extra: local
Requires-Dist: onnxruntime>=1.17; extra == 'local'
Requires-Dist: sentence-transformers>=3.0; extra == 'local'
Provides-Extra: openai
Requires-Dist: openai>=1.40; extra == 'openai'
Provides-Extra: voyage
Requires-Dist: voyageai>=0.2; extra == 'voyage'
Description-Content-Type: text/markdown

# mem8

## 项目介绍

mem8 是面向 AI Agent 的本地、持久化长期记忆 Python 库。Markdown 是记忆内容的真实源，数据库负责元数据、全文检索和向量索引。它支持从对话中提取记忆，也支持接收外部 LLM 已总结好的知识，并通过 BM25、向量检索和 RRF 融合返回相关记忆。

mem8 对外提供稳定、通用的消息与知识模型，核心能力覆盖记忆提取、决策、持久化、检索和维护，各模块通过明确接口协作。它不依赖特定业务仓库、Agent SDK、MCP、NAS 或 OSS；任务调度和远程持久化由接入方负责。

## 主要能力

- Markdown 文件持久化，数据目录可复制、挂载和备份。
- SQLite、LanceDB、Chroma 三种本地数据库 Backend。
- BM25 + Vector + RRF 混合检索。
- 按 ID 或相对路径查询记忆卡片。
- 支持 `text`、`thinking`、`tool_call`、`tool_result`、`attachment_text` 消息部件。
- 支持“从对话提取知识”和“直接写入已总结知识”两种更新模式。
- 支持幂等操作、乐观 generation 检查、只读打开和健康检查。
- 检查并修复 Markdown 记忆之间的链接。

## 可选项与配置

三个维度可独立组合，上层 API 保持一致：

| 维度 | 用途 | 当前选项与默认值 |
| --- | --- | --- |
| LLM | 提取知识、更新决策、生成卡片 | 内置 Anthropic；默认 `claude-sonnet-4-6`，输出预算 8192 token；可注入自定义实现 |
| Embedding | 生成语义检索向量 | Local（默认）、OpenAI-compatible、Voyage |
| 数据库 | 保存元数据、全文和向量索引 | SQLite（默认）、LanceDB、Chroma |

`openai` extra 只提供 Embedding，不提供 OpenAI LLM。修改 Embedding 时需同步选择匹配的模型和维度。

| Backend | 安装 | 向量存储 | 文本检索 | 适用场景 |
| --- | --- | --- | --- | --- |
| SQLite | `mem8-fs` | sqlite-vec | FTS5 | 默认、依赖最少、单机通用 |
| LanceDB | `mem8-fs[lancedb]` | LanceDB | SQLite FTS5 | 本地列式向量存储、数据量增长场景 |
| Chroma | `mem8-fs[chroma]` | Chroma | SQLite FTS5 | 已采用 Chroma 或偏好 Collection 模型的应用 |

三个 Backend 保持相同的 `Memory` API，并提供 `hybrid_rrf` 检索：

- SQLite 使用 `mem8.db` 保存元数据、FTS5 与 sqlite-vec 向量索引。
- LanceDB/Chroma 保存向量索引，并复用 `mem8-meta.db` 保存元数据、generation、操作回执和 FTS5 投影。
- Markdown 目录、写入约束、RRF 逻辑和上层服务不随 Backend 改变。
- Backend 不能通过修改配置原地切换；已有工作区切换前应显式迁移或重建索引。


LanceDB/Chroma 不使用 sqlite-vec 向量索引，但仍使用 SQLite 元数据；基础安装依赖仍包含 `sqlite-vec`。

### 配置方式

`<memory_root>/mem8.toml`：

```toml
[database]
provider = "lancedb"       # sqlite | lancedb | chroma
path = "lancedb"           # memory_root 内的相对路径
distance = "l2"            # l2 | cosine
require_hybrid = true
collection = "mem8_cards"
```

SQLite 默认配置不需要写 `[database]`。`database.path` 不能是绝对路径，也不能通过 `..` 越过 `memory_root`。

环境变量配置：

```bash
MEM8_DATABASE__PROVIDER=lancedb
MEM8_DATABASE__PATH=lancedb
MEM8_DATABASE__DISTANCE=l2
MEM8_DATABASE__REQUIRE_HYBRID=true
MEM8_DATABASE__COLLECTION=mem8_cards
```

其他常用变量：

- `MEM8_LLM__MODEL`
- `MEM8_LLM__MAX_TOKENS`
- `MEM8_EMBEDDING__PROVIDER`
- `MEM8_EMBEDDING__MODEL`
- `MEM8_EMBEDDING__DIM`
- `MEM8_STORAGE__DB_FILENAME`

环境变量优先于 `mem8.toml`。LanceDB/Chroma 工作区的 Backend 标识文件会校验 provider、Embedding 维度和距离度量，避免用不兼容配置误开已有工作区。默认 SQLite 和历史工作区继续保持兼容，不强制要求该标识文件。

## 环境要求

- Python 3.11 或更高版本。
- 默认 LLM 为 Anthropic，需要设置 `ANTHROPIC_API_KEY`。
- 使用检索和更新时，需要相应 Embedding Provider：Local、OpenAI 或 Voyage。
- Local Embedding 首次运行可能需要下载模型。

默认 SQLite 向量索引要求运行环境允许加载 sqlite-vec 扩展。仅使用 `MemoryCatalog` 浏览已有目录无需模型凭证。

## 安装

PyPI 分发名称为 `mem8-fs`，Python 包名仍为 `mem8`：

```bash
pip install mem8-fs
```

按需安装 Embedding Provider：

```bash
pip install "mem8-fs[local]"
pip install "mem8-fs[openai]"
pip install "mem8-fs[voyage]"
```

SQLite 是默认 Backend，不需要额外数据库依赖。其他 Backend 需安装对应 extra：

```bash
pip install "mem8-fs[lancedb]"
pip install "mem8-fs[chroma]"
pip install "mem8-fs[all-databases]"
```

源码开发环境：

```bash
uv sync --extra dev --extra evals --extra local
```

## CLI

### 初始化

```bash
mem8 init
```

默认记忆目录是当前工作目录下的 `.mem8`。也可以通过 CLI 的 `--memory-root PATH` 或 Python API 的 `Memory.open(memory_root=...)` 指定目录。

### 检索和查询

```bash
mem8 core inject ./seed.md
mem8 retrieve "context overflow" --topk 5 --similarity 0.55
mem8 query --card-id 1
mem8 query --path notes/context.md
```

### 更新记忆

从 JSONL 历史对话提取知识，再执行决策和写回：

```bash
mem8 update --messages messages.jsonl
```

```json
{"role":"user","content":"I prefer concise answers."}
{"role":"assistant","content":"Understood."}
```

外部 LLM 已生成结构化知识时，可跳过提取阶段，直接复用去重、决策和写回链路：

```bash
mem8 update-knowledge --knowledge-file knowledges.json
```

```json
[
  {
    "claim": "The user prefers concise answers.",
    "scope": "communication preferences",
    "evidence": ["Please keep answers concise."],
    "confidence": 1.0,
    "suggested_tags": ["preference", "style"]
  }
]
```

检查和修复记忆链接：

```bash
mem8 check
```

## Python API

### 检索与对话更新

```python
from mem8 import Memory
from mem8.domain.message import Message

with Memory.open(memory_root="/path/to/memory") as memory:
    results = memory.retrieve("user preferences", topk=5, similarity=0.55)
    card = memory.query(card_id=results[0].card_id) if results else None

    report = memory.update([
        Message(role="user", content="I prefer concise answers."),
        Message(role="assistant", content="Understood."),
    ])

    check_report = memory.check()
```

### 结构化消息

`Message.content` 继续兼容字符串，也支持厂商无关的结构化消息部件：

```python
from mem8 import (
    AttachmentTextPart,
    Memory,
    Message,
    TextPart,
    ThinkingPart,
    ToolCallPart,
    ToolResultPart,
)

messages = [Message(role="assistant", content=[
    ThinkingPart(text="private reasoning"),
    ToolCallPart(call_id="call-1", name="python_version"),
    ToolResultPart(call_id="call-1", content="Python 3.12"),
    AttachmentTextPart(
        text="project requires Python 3.12",
        filename="README.txt",
    ),
    TextPart(text="The project uses Python 3.12."),
])]

with Memory.open() as memory:
    memory.update(messages)
```

默认提取策略排除 `thinking`、`tool_call` 和失败的 `tool_result`；普通文本、成功的文本工具结果和附件提取文本可以作为证据。mem8 不接受二进制附件。

### 直接写入已总结知识

```python
from mem8 import Knowledge, Memory

knowledges = [Knowledge(
    claim="The user prefers concise answers.",
    scope="communication preferences",
    evidence=["Please keep answers concise."],
    confidence=1.0,
    suggested_tags=["preference", "style"],
)]

with Memory.open() as memory:
    report = memory.update_from_knowledge(
        knowledges,
        operation_key="example:knowledge:1",
        expected_generation=memory.generation,
    )
```

- `Memory.update(messages)`：由 mem8 调用 LLM 提取知识，适合历史对话和后续 dream 流程。
- `Memory.update_from_knowledge(knowledges)`：接收已经总结的 1～10 条知识，跳过提取阶段；后续决策和卡片构建仍会调用 LLM。
- 两种更新共享决策、写回、文件锁、操作回执和 generation 冲突检查。
- 只读工作区允许检索和查询，但拒绝所有更新操作。

### 更新结果完整性

新更新返回 `report_version=2` 的 `UpdateReport`。`outcome=complete` 表示去重后的知识项全部已写入或明确跳过；`partial` 表示仍有拒绝或未完成项。旧回执的完整性为 `unknown`，不能根据 `violations` 是否为空推断成功。

`items` 为每项提供稳定的 `knowledge_id`、`disposition`、`card_ids` 和 `issue_codes`。拒绝或未完成项还保存原始 `knowledge`。成功降级与可选链接提示保留在 `violations` 中，不会把已经写入的知识判为失败；确认已覆盖的 `no_op` 计为 `skipped`。

部分回执同样可以持久化并推进工作区 generation，但不证明输入全部处理成功。相同 key 重放返回原回执；调用方可以显式取出未完成项，通过 `update_from_knowledge` 发起独立、幂等的补偿更新，保留父操作和原回执不变。来源进度与补偿编排由调用方负责。

卡片展示标题与存储文件名分离。标题中的路径分隔符、非法字符、保留名称和超长名称会规范化为稳定的安全文件名；重名使用知识摘要区分，已有文件不会被覆盖。目录仍必须位于 `notes/`，不能使用绝对路径或父目录穿越。

### Backend 状态

```python
with Memory.open(memory_root="/path/to/memory") as memory:
    print(memory.database_provider)
    print(memory.database_capabilities)
    print(memory.database_health())
```

这些状态接口不修改记忆内容。

### API 一览

```python
Memory.open(...)
Memory.retrieve(prompt, topk, similarity) -> list[RetrievalResult]
Memory.query(card_id=..., path=...) -> RetrievalResult
Memory.update(messages) -> UpdateReport
Memory.update_from_knowledge(knowledges, operation_key=..., expected_generation=...) -> UpdateReport
Memory.check(options=CheckOptions(...), operation_key=...) -> CheckReport
Memory.publish_read_view() -> Path
Memory.open_read_view(memory_root, embedding=...) -> MemoryReadView
Memory.inject_core_card(source_md_path) -> int
Memory.record_eval_run(run) -> int
Memory.list_eval_runs(eval_name) -> list[EvalRun]
Memory.get_baseline_eval_run(eval_name) -> EvalRun | None
Memory.database_provider -> str
Memory.database_capabilities -> DatabaseCapabilities
Memory.database_health() -> DatabaseHealth
```

`Memory.open` 支持注入自定义 `llm`、`embedding`、`tokenizer` 和 `settings`。

## 数据目录、写入约束与恢复

### `.mem8` 目录结构

`.mem8` 是默认目录名，也可以指定其他 `memory_root`：

```text
<memory_root>/
├── core/                  # 显式注入的核心记忆 Markdown
├── notes/                 # 自动更新的普通记忆 Markdown
├── mem8.toml              # 配置；API 可在缺省时使用默认配置
├── mem8.db                # SQLite：元数据、FTS5 与向量索引
├── mem8-meta.db           # LanceDB/Chroma：元数据、FTS5、版本与回执
├── lancedb/               # 仅 LanceDB：向量数据，路径可配置
├── chroma/                # 仅 Chroma：向量数据，路径可配置
├── .mem8-backend.json      # LanceDB/Chroma：兼容性标识
├── .mem8.lock             # 目录访问锁
└── .mem8-update.json       # 更新进行中或中断待恢复时的日志
```

数据库相关项按所选后端出现，不是同时必有；SQLite 还可能产生 WAL/SHM 等运行文件。Markdown 保存记忆正文，数据库还保存元数据、关系、版本与回执，应备份整个目录，而不是只复制 Markdown 或数据库。

### 写入约束

- Agent 更新只写入 `notes/`。
- 核心记忆只能通过 `inject_core_card` 或 `core inject` 注入。
- Markdown 路径必须位于 `memory_root` 内；绝对路径和目录穿越会被拒绝。
- 单张卡片最多 4096 token、最多 8 个 Markdown 链接，链接按 1 token 计数。
- 卡片大小限制与 LLM 请求的输出预算是两项独立设置。
- 修改卡片前会重新计算 token 和链接限制。
- Embedding 维度必须与现有向量索引一致；LanceDB/Chroma 会通过 Backend 标识提前校验。
- 生产环境应持久化整个 `memory_root`，而不是只持久化数据库子目录。


### 可恢复更新

两种更新 API 复用同一套 redo journal：先在隔离的元数据/文件视图完成一条知识的决策、卡片构建和 embedding，保存完整计划到 `<memory_root>/.mem8-update.json`，再写入 Markdown、metadata、关系、vector 和 FTS。计划包含稳定的卡片 ID、关系 ID、内容和向量，恢复不重复生成已准备好的内容。

批次按知识顺序推进，后续知识能检索此前已完成的结果；整个批次执行完成才提交 generation 和持久化回执，知识完整性以 `outcome` 和 `items` 为准。它是可恢复的顺序提交，不是跨数据库的原子回滚事务。中断期间，公共检索、读取及维护接口拒绝访问，其他更新不能越过待恢复操作。

```python
from mem8 import Memory

with Memory.open(memory_root="/path/to/memory") as memory:
    report = memory.recover(operation_key="application-operation-123")
    # None：无待恢复操作，也无该 key 成功回执，不能据此宣称修复成功。
    # UpdateReport：已恢复执行，或读到此前的持久化回执；仍需检查 outcome。
```

- 相同 `operation_key`、相同输入重试会恢复/返回原持久化回执，包括部分回执，不重复创建卡片；不同输入复用同一 key 会被拒绝。
- `recover()` 不指定 key 时恢复目录中唯一的待完成操作。未准备的剩余知识可能调用 LLM；已保存的提取结果不重复提取，已准备计划不重新调用 LLM/embedding。
- journal 损坏、存储配置不匹配、generation 冲突会保持隔离并抛出 `RecoveryRequiredError`，不自动删除数据、切换 backend 或初始化空库。
- 旧调用可以不传 key，但现在也会生成操作 key、记录回执并推进 generation；异常后应调用 `recover()`，不要用新 key 重做整个批次。
- 三种 backend 共用协议和既有索引适配器。扩展写入动作必须补充显式计划命令和故障注入测试，不得绕过计划修改真实存储。
- journal 含输入消息/知识和卡片正文，需与整个记忆目录一起做访问控制、备份及持久化。只读 API 仍需可用的目录锁文件；不要在更新中复制单个数据库文件。
- 协议依赖文件系统的原子替换、锁和持久化语义，不替代部署方的远程同步/多主协调。不自动修复升级前损坏的数据；`check` 和核心卡片注入只参与互斥准入，不属于 redo 更新协议。


### `MemoryCatalog`：轻量只读目录

`MemoryCatalog` 读取元数据和完整 Markdown，不构造 LLM、Embedding、Tokenizer 或向量客户端，不初始化或迁移工作区。支持三种数据库的现有元数据目录，无需模型凭证。

```python
from mem8 import MemoryCatalog, CardFilter, CardOrder

with MemoryCatalog.open(memory_root="./my-memory") as catalog:
    cards = catalog.list_cards(
        filter=CardFilter(include_superseded=False, is_index_card=False),
        order=[CardOrder(field="updated_at", direction="desc")],
        limit=3,
    )
    if cards:
        document = catalog.read_card(card_id=cards[0].id)
        print(document.content)
```

- `list_cards` 列出元数据；`get_cards(card_ids=...)` 批量读取元数据；`read_card(card_id=...)` 读取完整文档。
- 默认排除已删除卡片，包含已被替代卡片，不限制核心或索引卡片。需要一致筛选时，各接口传入同一 `CardFilter`。
- 列表默认按 ID 升序；自定义排序未指定 ID 时，以 ID 降序处理同值。列表和批量读取上限为 1000。
- 批量读取去重、保留输入顺序并略过缺失或被过滤的卡片；单条缺失抛出 `CardNotFound`。
- 可传 `CatalogSettings` 或 `Settings`；默认只加载数据库类型与 SQLite 文件名相关配置和环境变量。
- 不存在或完全空的目录抛出 `WorkspaceUninitialized`；非空但未初始化或损坏的目录报错，不伪装成空列表。路径不能越过工作区。
- 上下文管理器持有 SQLite 元数据读快照，但不是跨 Markdown 文件的原子快照。调用方应协调写入或使用完整静态快照，不能用它绕过中断更新的恢复要求。
- `Memory.open(read_only=True)` 仍构造检索客户端，并使用目录锁；与轻量目录浏览不同。

## 记忆使用时机与读写编排

以下是接入方可选择的读写时机，不要求每个节点都调用。Turn 指一次输入到响应完成的处理过程；Loop 指 Turn 内的一轮模型推理与后续工具执行（如有），一个 Turn 可以包含多个 Loop。

| 操作 | 时机 | 使用目的与建议 |
| --- | --- | --- |
| 读 | Begin Turn（开始处理输入） | 根据当前请求检索相关历史事实、偏好和经验，为本轮处理提供上下文。 |
| 读 | Begin Loop（开始新一轮推理） | 当目标变化、出现疑问或需要新的背景信息时，按当前问题补充检索，避免每轮重复读取相同内容。 |
| 读 | Post Compact（上下文压缩后） | 按当前目标重新检索仍需使用的长期记忆，补充压缩后缺失的背景；不恢复全部历史对话。 |
| 写 | End Turn（本轮处理结束） | 将本轮已确认、值得跨轮复用的事实、偏好、结论或经验更新到记忆中。 |
| 写 | End Loop（一轮推理与工具执行结束） | 当获得可靠的新事实、纠正已有认知或形成阶段性结论时增量更新，不保存每一步临时推测。 |
| 写 | Pre Compact（上下文压缩前） | 在详细上下文被压缩前，筛选并保存尚未持久化且具有长期价值的信息。 |
| 写 | Dream（离线或周期性整理） | 由接入方组织历史对话或已总结知识，提取、整合并更新可复用记忆；不是重放全部工具操作。 |

- 读取复用 `Memory.retrieve`，需要展开相关卡片时使用 `Memory.query`；检索结果由接入方放入模型上下文，mem8 不自动注入。
- 写入历史对话使用 `Memory.update`；已有结构化知识时使用 `Memory.update_from_knowledge`，两者复用同一套决策与写回能力。
- 这些名称是通用生命周期时机，不表示特定 Agent SDK 一定提供对应 Hook。mem8 不内置 Agent Loop、压缩 Hook 或 Dream 调度器，触发和编排由接入方负责。
- 多个时机可能覆盖同一批信息，接入方应控制调用频率，并为同一更新的重试复用 `operation_key`；新的更新使用新的操作标识。压缩不等于任务结束，记忆写入也不等于完整工作状态备份。

### 异步串行写与不阻塞的并发读

mem8 当前提供同步 API；同一目录的 `Memory` 检索、查询和更新使用互斥锁，不能直接保证读写并行。需要非阻塞交互时，建议由接入方实现：

1. **异步接收、串行写入**：持久化请求后返回已受理，由每个记忆库的单一写执行器依次调用更新 API。已受理不等于写入成功。
2. **发布完整版本**：通过 publish_read_view 建立基线，后续更新、维护或恢复成功后发布逻辑快照并原子切换指针；完成接入方的持久化确认后才报告成功。
3. **同步响应、并发读取**：读请求固定一个已发布版本，在独立安全读副本上使用各自实例并返回结果，不等待写队列、不恢复写目录；不要跨线程共享实例。
4. **隔离失败**：写入中断时继续读取上一完整版本，由写执行器恢复；保留仍被读请求使用的旧版本。

读取可能稍旧，但不应看到半写入数据。mem8 提供可选稳定读视图；异步队列和远程同步仍由接入方负责。`read_only=True` 或 `MemoryCatalog` 本身不解除同目录读写阻塞，也不保证所有向量后端完全不修改运行文件。

### 稳定读视图与使用注意事项

同一目录的普通 Memory API 仍使用工作区锁；read_only=True 和直接打开
MemoryCatalog 不会自动隔离活跃写入。需要维护期间并发读取时，先发布稳定视图，
再让每个读请求独立打开视图：

~~~python
from mem8 import CheckOptions, Memory

with Memory.open(memory_root="./my-memory") as memory:
    memory.publish_read_view()  # 首次建立基线；后续 update/check/recover 更新已启用的视图
    report = memory.check(
        options=CheckOptions(seed=20261007),
        operation_key="check:example:20261007",
    )

with Memory.open_read_view("./my-memory") as view:
    with view.catalog() as catalog:
        cards = catalog.list_cards(limit=8)
        if cards:
            document = view.query(card_id=cards[0].id)
~~~

- 检索需给 open_read_view 注入相同模型和维度的 embedding；查询卡片和目录不需要模型。
- 读请求固定上一成功发布的 generation，不等待模型、check 或恢复执行；
  写入、check 和恢复仍串行，视图不是异步任务队列。读实例及其 catalog 使用后及时关闭。
- 视图是元数据、Markdown 和向量的逻辑快照，不复制活跃数据库引擎文件；
  快照向量检索使用精确距离计算，适合有界本地记忆库，不承诺大型索引的性能。
- 每个视图最多 100000 张卡片、8000000 个向量分量及 1 GiB；
  最多保留 8 个尚未回收的历史视图，活跃读者持有的版本不清理。
  达到保留上限时拒绝新发布，不覆盖正在使用的旧版本。
- .mem8-views/ 保存快照和原子指针；只读视图不等于原工作区的备份，
  不能通过删除原工作区 journal 绕过恢复。首次基线建立失败时明确报错，不冒充空库。
- 原始结构化消息用 update，外部已经总结的知识用 update_from_knowledge；
  中断恢复沿用原 operation_key，不要换新标识重复执行同一更新。

### 有界随机检查

check 是可能修改卡片的独立维护，不是只读审计。建议在离线更新完成后、
空闲维护窗口调用，不放入读请求或每轮对话的必经路径：

| 参数 | 默认值 | 含义 |
| --- | --- | --- |
| candidate_limit | 200 | 已标记内部断链的有界候选窗口 |
| max_links / max_cards | 8 / 8 | 最多检查链接数、实际修改卡片数；源卡去重 |
| max_stubs | 1 | 最多新建占位卡；包括在修改卡片额度中 |
| max_model_calls | 16 | 最多调用 LLM 接口次数 |
| seconds | 60 | 维护阶段的协作式时间预算 |
| seed | 随机 | 固定种子可复现同批选择；不同种子提供跨批次随机性 |

候选通过索引随机窗口查询，不先扫描全库。全库仅指候选范围，不表示全库已检查；
不同批次可能抽到同一张卡片，不维护复杂的跨日轮转台账。随机选择只决定检查对象，
不会强制改动卡片；修复复用已有核心卡保护、token/链接约束和镜像关系逻辑。

CheckReport 保留原修复列表，并提供 status、checked、selected_link_ids、
operation_key、generation 和 replayed。completed 表示所选批次完成，
partial 表示存在未完成或无法修复项，no_change 表示没有持久改动；
无改动回执不增加 generation。批次选择和修复命令写入现有 redo journal，
恢复重放已规划命令而非重新随机抽样或重复调用模型。

时间预算在规划及模型调用边界检查，不强行中断任意自定义同步适配器。
LLM 和 embedding 适配器必须设置请求超时；外部编排如需硬截止，应在隔离进程中执行，
沿用原操作键恢复，不能取消线程后继续共享可写实例。候选检索、模型和 embedding
仍有成本，应设置检索配置和适配器请求预算；异步 accepted 不代表维护成功。

## 评测与开发

```bash
mem8 eval --suite retrieval
mem8 eval --suite update --record
mem8 eval --suite check --baseline
mem8 eval --suite all --record
```

源码验证：

```bash
uv run pytest
uv run ruff check mem8 evals tests
uv run mypy mem8 evals
```

可选 Backend 的真实集成测试仅在对应依赖已安装时运行。

## 发布到 PyPI

仓库使用 GitHub Actions 和 PyPI Trusted Publishing。首次配置需建立 GitHub 的 `pypi` Environment，并在 PyPI 注册与仓库、`publish-pypi.yml` 和该 Environment 匹配的 Trusted Publisher。

发布前同时更新 `pyproject.toml` 与 `mem8/__init__.py` 中的版本号，合并到 `master` 后推送匹配的 Tag：

```bash
git tag vX.Y.Z
git push origin vX.Y.Z
```

工作流会校验 Tag 与版本声明、运行测试和静态检查、构建 wheel/sdist，并执行 `twine check`。发布 Job 使用 `pypi` GitHub Environment，仓库不保存 PyPI API Token。
