Skip to content
Merged
5 changes: 5 additions & 0 deletions docs/en/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ Without an override, the default is:
The default SQLite database is `powercontext.db` in this directory. Built-in background processors persist
intents and scheduling checkpoints in the same database. Existing installations require [offline migration](artifact-processing-migration.md).

The built-in SQLite backend requires Python's linked SQLite library (`sqlite3.sqlite_version`) to be **3.38.0 or newer**,
with FTS5, JSON functions, and loadable sqlite-vec for vector search. This retains the backend minimum specified in
[the Memory design](../../rfcs/0014_memory_layer_design.md#sqlite-adapter), including native vector `IN` filtering.
Older libraries are rejected before database initialization or accepting writes.

## Server

Server settings use the `POWERCONTEXT_SERVER_` prefix.
Expand Down
9 changes: 7 additions & 2 deletions docs/en/docs/workflows/atomic-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,13 @@ Submit to `POST /v1/atomic-memory/search`:
`mode` accepts `text`, `vector` or `hybrid` and defaults to `text`. Vector and hybrid modes require an available,
matching Embedding profile. Search returns active memories only. Each `hits[].memory` contains the content, exact
Artifact reference and state version; the hit also has `score` and `matched_by`. Preserve `memory.artifact` for citations.
Ordinary vector search applies eligibility first, computes exact L2, then ranks and limits results. This path does not
use ANN and does not establish production backend performance or acceptance.
Ordinary vector search on seekdb and OceanBase uses native approximate nearest-neighbor retrieval, with Scope, kind
and tag eligibility inside the candidate query before its limit. ANN can omit exact nearest neighbors; tune and measure
recall and latency for the deployment's embedding model and filter selectivity. SQLite uses Scope-partitioned
sqlite-vec native exact KNN for bounded search. Normal startup audits vector deployment readiness; ordinary Top-K
selects usable vectors and validates selected results rather than auditing the whole eligible collection. Complete
threshold enumeration used by generation and exact recovery probes still reject incomplete eligible vectors.
Enumeration remains exact on every backend.

For complete Artifact results and advanced retrieval controls, use
`POST /v1/scopes/S/artifacts/atomic-memory/search`. This unified route requires `scope.read`, defaults to text,
Expand Down
12 changes: 12 additions & 0 deletions docs/en/docs/workflows/topic-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@ For complete Artifact results, explicit text/vector/hybrid modes, admission, sco
[Fusion algorithms and parameters](search-fusion.md). The dedicated route keeps its existing summary response and
`0`–`100` score scale.

Explicit vector/hybrid search uses the configured query embedding provider even in an empty Scope; provider errors and
fallback depend on the requested mode. SQLite ranks exact nearest Topics across both title/summary and detail vectors.
It first checks a bounded exact neighbor prefix. If that prefix contains enough eligible distinct Topics or is exhausted,
it returns; otherwise one full exact query replaces the prefix. Worst-case latency grows with eligible chunks. At exactly equal
distances, boundary Topic membership and the choice of an equally near chunk need not follow global identity or
ordinal order. These choices can change fused candidates and scores; the final fused-score tie rules still apply.
seekdb/OceanBase use approximate title/summary retrieval. Detail search ranks current chunks by each Topic's minimum
distance before the candidate limit and payload reads. This scans eligible chunks and has no constant-latency guarantee.
The native fused result remains approximate because its title channel uses ANN; measure recall and tail latency for
the deployment. Startup leaves existing detail HNSW indexes intact and does not create a detail graph for new databases.
Detail queries do not use that graph; an existing graph still incurs database maintenance costs.

## Read exact detail

Do not reconstruct the latest version from a title. Pass the `artifact` from a search hit (`family`, `artifact_id`, and
Expand Down
5 changes: 5 additions & 0 deletions docs/zh/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export POWERCONTEXT_HOME=/srv/powercontext
默认 SQLite 数据库是该目录下的 `powercontext.db`。后台处理器的意图与调度检查点保存在同一数据库中。
已有部署须先完成[停机迁移](artifact-processing-migration.md)。

内置 SQLite 后端要求 Python 链接的 SQLite 库(`sqlite3.sqlite_version`)为 **3.38.0 或更新版本**,
启用 FTS5、JSON 函数,并为向量检索加载 sqlite-vec。这沿用
[Memory 设计](../../rfcs/0014_memory_layer_design.md#sqlite-adapter)约定的后端下限,包括原生向量 `IN` 过滤能力。
旧版本会在初始化数据库或接受写入之前被拒绝。

## Server

Server 配置使用 `POWERCONTEXT_SERVER_` 前缀。
Expand Down
7 changes: 5 additions & 2 deletions docs/zh/docs/workflows/atomic-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,11 @@ Atomic Memory 将每条事实、偏好或决策保存为独立的 `atomic-memory

`mode` 可选 `text`、`vector`、`hybrid`,默认 `text`。向量和混合模式要求可用且匹配的 Embedding profile。
搜索只返回 active 记忆,`hits[].memory` 包含正文、精确 Artifact 引用和状态版本;还返回 `score`、
`matched_by`。引用结果时保留 `memory.artifact`。普通向量搜索在资格过滤后计算精确 L2,再排序和限制条数;
当前路径不使用 ANN,不能据此推断生产后端性能或验收结果。
`matched_by`。引用结果时保留 `memory.artifact`。seekdb 和 OceanBase 的普通向量搜索使用原生近似最近邻检索,
Scope、kind、tag 的资格条件位于候选查询内部,在限制条数前执行。ANN 可能遗漏精确最近邻,需要针对部署的
Embedding 模型和过滤选择率测量召回与延迟。SQLite 的有界检索使用按 Scope 分区的 sqlite-vec 原生精确 KNN。
正常启动时核查向量部署是否完整;普通 Top-K 选择可用向量并校验选中结果,不在每次请求中审计整个候选集合。
生成使用的完整阈值枚举和精确恢复探测仍拒绝资格范围内不完整的向量;完整枚举在所有后端上保持精确。

需要完整 Artifact 结果和高级检索参数时,使用 `POST /v1/scopes/S/artifacts/atomic-memory/search`。
统一入口要求 `scope.read`,默认文本检索,支持 `filters`、准入、RRF 排名常数与权重、`min_score` 和可选评分元数据。
Expand Down
11 changes: 11 additions & 0 deletions docs/zh/docs/workflows/topic-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,17 @@ Content-Type: application/json
需要完整 Artifact 结果、显式文本/向量/混合模式、准入、评分阈值或通道原分时,使用[检索 Artifact](search-artifacts.md)。
Topic 的融合参数见[融合算法与参数](search-fusion.md)。专用入口保留原来的摘要响应和 `0`–`100` 评分。

显式向量或混合检索在空 Scope 中也使用配置的 query embedding provider;模型错误及回退行为由请求模式决定。
SQLite 对标题/摘要和详情两个向量通道计算精确最近主题。先检查有界的精确邻居前缀;
取得足够的合格不同 Topic 或已穷尽时即返回,否则用一次完整精确查询替换前缀。
最坏延迟会随资格范围内的 chunk 数增长。距离完全相等时,
边界 Topic 的入选和等距最佳 chunk 的选择不保证遵循全局身份或 ordinal 顺序;这可能改变融合候选和得分,
最终融合得分并列时的排序规则仍然适用。
seekdb/OceanBase 的标题/摘要使用近似检索;详情按各 Topic 当前 chunk 的最小距离排序,
再截取候选和读取正文。该路径扫描资格范围内的 chunk,不保证固定时延;标题通道仍是 ANN,
需要测量实际部署的融合召回率与尾时延。启动保留已有 detail HNSW,新库不创建 detail 图。
详情查询不使用该图;旧库中的已有图仍产生数据库维护成本。

## 读取精确详情

不要根据标题自行拼接最新版本。把搜索结果中的 `artifact`(`family`、`artifact_id`、`revision`)原样传给
Expand Down
2 changes: 1 addition & 1 deletion src/powercontext/artifacts/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ class ArtifactSearchOutcome(Protocol):
def matches(self) -> Sequence[ArtifactSearchMatch]: ...

@property
def artifacts(self) -> Sequence[Artifact[Any]] | None: ...
def artifacts(self) -> Sequence[Artifact[Any]]: ...


RequestT = TypeVar("RequestT", bound=ArtifactSearchQuery)
Expand Down
14 changes: 5 additions & 9 deletions src/powercontext/builtin/artifacts/experience/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,17 @@ class ExperienceSearchRequest(ArtifactSearchQuery):

@dataclass(frozen=True)
class ExperienceSearchOutcome:
"""Experience hits plus the admission accounting for the search that produced them.
"""Experience hits and bounded admission observations for Recall recovery planning.

It replaces the bare ``tuple[ExperienceSearchHit, ...]`` the ``experience_recall`` callback
used to return, because the Runtime's recall gate needs the retrieved/admitted pair and the
callback signature is part of the Runtime's construction surface.

``admission`` is ``None`` when the search did not measure — for example a configured index
with nothing to report — rather than a fabricated ``(0, 0)``. The counters are aggregates
with no hit identity, so carrying them costs nothing the callback did not already compute.
``admission`` is ``None`` when the index did not measure candidates. Built-in
indexes observe selected qualified matches and at most one ranked-prefix rejection witness;
these counters do not represent the whole matching collection.
"""

hits: tuple[ExperienceSearchHit, ...] = ()
admission: AdmissionCounts | None = None
matches: tuple[ArtifactSearchMatch, ...] = ()
artifacts: tuple[Experience, ...] | None = None
artifacts: tuple[Experience, ...] = ()


class ExperienceSearchHit(BaseModel):
Expand Down
13 changes: 7 additions & 6 deletions src/powercontext/builtin/artifacts/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,13 @@ def __post_init__(self) -> None:
class AdmissionCounts:
"""Per-family, per-scope admission accounting for one search.

``retrieved`` is what the backend returned *before* the admission floor was applied;
``admitted`` is what survived it. A family may also provide ``rejected`` when those two
counts are not measured on a comparable basis, for example when the trace keeps an
unbounded pre-admission count but the delivered candidate pool is capped. ``rejected``
is the number of same-search candidates rejected by the admission floor, excluding
candidate-pool truncation.
``retrieved`` counts candidates actually observed by this search, including
any bounded rejection evidence. ``admitted`` counts observed candidates that
pass the floor. These are not collection totals. A Family may provide
``rejected`` when the two observations use different bounded pools; it counts
actual rejection evidence, excluding candidate-pool truncation. Topic FTS
observes at most one rejected Topic per channel; Experience observes at most
one rejected match in its ranked prefix. Both support recovery planning without collection counts.

These are plain aggregate integers with no candidate identity, no query text and no
per-entry attribution, so the value is safe to carry in a trace and safe to hand to the
Expand Down
4 changes: 2 additions & 2 deletions src/powercontext/builtin/artifacts/skill/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ class SkillSearchHit(BaseModel):

@dataclass(frozen=True)
class SkillSearchOutcome:
"""Managed Skill hits and their optional exact Artifact materialization."""
"""Managed Skill hits and their exact Artifact materialization for public search."""

hits: tuple[SkillSearchHit, ...] = ()
matches: tuple[ArtifactSearchMatch, ...] = ()
artifacts: tuple[Skill, ...] | None = None
artifacts: tuple[Skill, ...] = ()


def skill_search_text(content: SkillContent, package: SkillPackageSnapshot | None = None, /) -> str:
Expand Down
7 changes: 3 additions & 4 deletions src/powercontext/builtin/artifacts/topic_memory/chunking.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,9 @@
TOPIC_MEMORY_CHUNK_MAX_CHARACTERS = 1_800
TOPIC_MEMORY_CHUNK_MIN_TAIL_CHARACTERS = 300
TOPIC_MEMORY_CHUNK_OVERLAP_CHARACTERS = 160
# The first-release search contract admits at most 20 Topics per channel.
# Keeping each Topic at or below 200 chunks therefore bounds sqlite-vec k at
# 4,000, below its 4,096-neighbor ceiling. Pathological Markdown that would
# exceed this limit uses the deterministic fixed-window fallback below.
# Bound per-Topic projection and inference work. Retrieval must independently
# handle distinct Topics and native neighbor limits; many close sibling chunks
# can exhaust a raw neighbor pool. Oversized Markdown uses fixed windows below.
TOPIC_MEMORY_CHUNK_MAX_COUNT = 200

_BLOCK_BOUNDARY = re.compile(r"(?:\n[ \t]*\n+)|(?=^#{1,6}[ \t]+)|(?=^[ \t]*(?:[-*+] |\d+[.)] ))", re.MULTILINE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class TopicMemorySearchResult(BaseModel):
query_embedding: MemoryQueryEmbedding | None = Field(default=None, exclude=True)
embedding_calls: int = Field(default=0, exclude=True)
matches: tuple[ArtifactSearchMatch, ...] = Field(default=(), exclude=True)
artifacts: tuple[TopicMemory, ...] | None = Field(default=None, exclude=True)
artifacts: tuple[TopicMemory, ...] = Field(default=(), exclude=True)


class PublishedTopicMemory(BaseModel):
Expand Down
Loading
Loading