Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,6 @@ integrations/pi/plugins/powercontext/*.tgz

# Local acceptance databases and redacted run artifacts
.artifacts/

# pnpm package store is a local cache, never committed.
.pnpm-store/
2 changes: 1 addition & 1 deletion desktop/src-tauri/src/transport/operations.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"contractSha256": "7aa91ae9af78300e7bdbcc2e8b2135da5749469290a1aee67bc255c3e9a07b0c",
"contractSha256": "fdc752737cc446c96a1482debcde927860e1ce30539b7a647a78c8a64f94b112",
"operations": {
"get_liveness": {
"method": "GET",
Expand Down
2 changes: 2 additions & 0 deletions desktop/ui/src/generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2218,6 +2218,7 @@ export interface components {
};
AtomicMemorySearchHit: {
memory: components["schemas"]["AtomicMemoryRecord"];
/** @description Raw RRF score. Hybrid mode applies the deployment's normalized relative FTS/vector weights. */
score: number;
matched_by: ("text" | "vector")[];
};
Expand Down Expand Up @@ -4091,6 +4092,7 @@ export interface components {
title: string;
summary: string;
snippet: string | null;
/** @description Weighted RRF relevance normalized to the 0..100 range. */
score: number;
matched_by: components["schemas"]["TopicMemoryMatchedBy"][];
};
Expand Down
2 changes: 1 addition & 1 deletion desktop/ui/src/generated/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

// Generated from openapi/powercontext.yaml. Do not edit.
export const contractSha256 = "7aa91ae9af78300e7bdbcc2e8b2135da5749469290a1aee67bc255c3e9a07b0c";
export const contractSha256 = "fdc752737cc446c96a1482debcde927860e1ce30539b7a647a78c8a64f94b112";
export const operations = {
"get_liveness": {
"method": "GET",
Expand Down
19 changes: 19 additions & 0 deletions docs/en/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ Server settings use the `POWERCONTEXT_SERVER_` prefix.
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ROUND1_MIN_SEMANTIC_SIMILARITY` | `0.15` | Semantic-similarity admission floor for the first expansion round; must not exceed the round-zero `0.3` |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ROUND2_MIN_SEMANTIC_SIMILARITY` | `0.10` | Semantic-similarity admission floor for the second expansion round; must not exceed the first round's value |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ALLOW_WITH_RERANK` | `false` | Whether expansion may still run when `MEMORY_RERANK_ENABLED` is set; by default reranking ends the expansion |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_FTS_WEIGHT` | `1.0` | Relative FTS contribution in hybrid Atomic Memory and Topic Memory RRF; `0` excludes FTS-only matches from hybrid results |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_VECTOR_WEIGHT` | `1.0` | Relative vector contribution in hybrid Atomic Memory and Topic Memory RRF; `0` excludes vector-only matches from hybrid results |
| `POWERCONTEXT_SERVER_RUNTIME_MEMORY_SCHEDULE_SECONDS` | unset | Memory automatic admission interval; `SCHEDULE_SECONDS` remains a compatibility alias |
| `POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_SCHEDULE_SECONDS` | unset | Topic Memory automatic admission interval; unset disables new automatic admission |
| `POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_SOURCE_WINDOW_LIMIT` | `10` | Maximum Sources per Topic Memory Window, capped at 100; one Scope invocation can finish several Windows |
Expand Down Expand Up @@ -191,6 +193,23 @@ Server settings use the `POWERCONTEXT_SERVER_` prefix.
| `POWERCONTEXT_SERVER_RUNTIME_EXPERIENCE_SCHEDULE_SECONDS` | unset | Experience automatic admission interval; unset preserves accepted work and stops new automatic admission |
| `POWERCONTEXT_SERVER_EXTERNAL_SKILLS` | automatic local project targets | JSON override containing the host identity and explicit Agent Skill targets |

The recall channel weights are a ratio and apply to Atomic Memory and Topic Memory only after hybrid candidates pass
their channel admission floors. Single-channel searches remain unweighted; `auto` applies the weights only when it
selects hybrid search. Atomic Memory excludes zero-weight channels from `matched_by` and recall-gate relevance.
Topic Memory retains their evidence for Topics selected by a positive-weight channel.
Scaling both values equally has no effect. Setting one value to `0` excludes that channel from hybrid fusion, but a
higher weight cannot restore a candidate rejected before fusion. A zero weight does not disable that channel's
retrieval or admission work: vector embeddings may still be generated, and retrieval/admission counts still include
the zero-weight channel. A Topic Artifact search request that supplies its own `fusion` parameters replaces the
deployment recall weights entirely. The deployment weights apply only when the request does not specify `fusion`
parameters. After normalization, each non-zero channel weight must be at least the smallest normal IEEE 754 binary64
value (`2.2250738585072014e-308`); smaller values are rejected at startup. Only an explicit `0` disables a channel's
fusion contribution. Evaluate weight changes together with recall-gate rounds, semantic admission floors, and
historical Topic candidate selection instead of treating the knobs independently. In particular, weighting FTS more
heavily can move a Topic's normalized score across
`POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_HISTORY_RRF_THRESHOLD` (default `70`) and change which historical Topics
are sent to the planner.

The recall-sufficiency gate is disabled by default. When enabled, the Runtime assesses the first recall's candidate
count, family coverage, top-candidate score, and lexical coverage during `prepare_context`; when it judges the result
insufficient, it may expand up to two more rounds and relaxes the semantic-similarity admission floor each round. The
Expand Down
10 changes: 6 additions & 4 deletions docs/en/rfcs/0014_memory_layer_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,12 +775,14 @@ Candidates below either threshold are not restored merely to fill `limit`. The a
backend's internal order within each channel, and then use reciprocal rank fusion:

```text
rrf_score(candidate) = Σ 1 / (60 + rank_in_channel)
hybrid_rrf_score(candidate) = Σ channel_weight / (60 + rank_in_channel)
```

`rank_in_channel` starts at 1. Single-channel `fts` and `vector` modes use the same one-channel formula to produce their
public score. A candidate rejected by or absent from one channel receives a contribution only from the other, and
`matched_by` contains only admitting channels. The OceanBase adapter
`rank_in_channel` starts at 1. The deployment's non-negative FTS/vector weights are relative: PowerContext normalizes
them to a mean of one, defaults both to `1.0`, and requires at least one to be positive. They apply only to `hybrid`;
single-channel `fts` and `vector` modes keep the unweighted `1 / (60 + rank)` public score. A zero-weight channel does
not contribute candidates or appear in `matched_by`. A candidate rejected by or absent from one channel receives a
contribution only from the other. The OceanBase adapter
may use native `HYBRID_SEARCH` in version 4.6+ as an optimization, but it must preserve the same active-head filtering,
RRF parameters, and stable tie-breaks externally. The 4.3.5 baseline performs RRF in the application. SQLite performs
the same application-level RRF over FTS5 and Vec1 results. Equal RRF scores are broken by ascending
Expand Down
7 changes: 6 additions & 1 deletion docs/en/rfcs/1417_topic_memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,12 @@ windows. Changing the Chunk policy requires rebuilding the corresponding retriev
Each retrieval channel first collapses by Topic. When multiple Detail chunks from one Topic match, only the best
position is retained for the snippet. The two or four channels enabled for the current deployment are fused by RRF
rather than by comparing raw full-text scores with raw vector distances. A Topic occupies at most one final result
position, and `matched_by` records which channels matched it. The per-channel candidate limit applies only after this
position, and `matched_by` records every admitted channel that matched the selected Topic. A zero-weight channel makes
no ranking contribution and cannot select a Topic by itself, but its evidence remains in `matched_by` when another
positive-weight channel selects the same Topic. Hybrid mode applies the deployment's normalized relative FTS/vector
weights to both Topic and Detail channels of the corresponding kind. FTS-only and vector-only modes remain unweighted.
The weighted theoretical maximum for the enabled channels is the denominator that keeps the public score normalized
to `0..100`. The per-channel candidate limit applies only after this
Topic collapse, so one Topic with many matching chunks cannot hide another Topic. FTS snippets center a bounded window
on an Analyzer v1 query match; a vector-only detail hit uses a stable window from its matched chunk.
The first-release search limit and historical candidate configuration are both bounded to 20. Search queries are
Expand Down
13 changes: 13 additions & 0 deletions docs/zh/docs/operate/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ Server 配置使用 `POWERCONTEXT_SERVER_` 前缀。
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ROUND1_MIN_SEMANTIC_SIMILARITY` | `0.15` | 第一轮追加搜索使用的语义相似度准入下限;启用时不得高于首轮的 `0.3` |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ROUND2_MIN_SEMANTIC_SIMILARITY` | `0.10` | 第二轮追加搜索使用的语义相似度准入下限;启用时不得高于第一轮的值 |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_GATE_ALLOW_WITH_RERANK` | `false` | 已启用 `MEMORY_RERANK_ENABLED` 时是否仍允许追加搜索;默认在 rerank 之后不再追加 |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_FTS_WEIGHT` | `1.0` | hybrid Atomic Memory 和 Topic Memory RRF 中 FTS 的相对贡献;`0` 会从 hybrid 结果中排除仅命中 FTS 的候选 |
| `POWERCONTEXT_SERVER_RUNTIME_RECALL_VECTOR_WEIGHT` | `1.0` | hybrid Atomic Memory 和 Topic Memory RRF 中 vector 的相对贡献;`0` 会从 hybrid 结果中排除仅命中 vector 的候选 |
| `POWERCONTEXT_SERVER_RUNTIME_MEMORY_SCHEDULE_SECONDS` | 未设置 | Memory 自动准入间隔;`SCHEDULE_SECONDS` 保留为兼容别名 |
| `POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_SCHEDULE_SECONDS` | 未设置 | Topic Memory 自动准入间隔;未设置时不接纳新的自动调用 |
| `POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_SOURCE_WINDOW_LIMIT` | `10` | 每个 Topic Memory Window 的 Source 数量上限,硬上限为 100;一次 Scope 调用可完成多个 Window |
Expand Down Expand Up @@ -182,6 +184,17 @@ Server 配置使用 `POWERCONTEXT_SERVER_` 前缀。
| `POWERCONTEXT_SERVER_RUNTIME_EXPERIENCE_SCHEDULE_SECONDS` | 未设置 | Experience 自动准入间隔;未设置时保留已接受工作,停止新的自动准入 |
| `POWERCONTEXT_SERVER_EXTERNAL_SKILLS` | 自动生成本机项目 target | 覆盖默认值的 host identity 和显式 Agent Skill targets JSON object |

召回通道权重表示相对比例,应用于 Atomic Memory 和 Topic Memory,只在 hybrid 候选通过各通道准入下限后生效。
单通道搜索保持无权重行为;`auto` 只有在选择 hybrid 搜索时才应用权重。Atomic Memory 的 `matched_by` 和 recall gate
相关度不包含零权重通道;Topic Memory 则为正权重通道选中的 Topic 保留零权重通道的证据。
同比例缩放两个值不会改变结果。将其中一个设为
`0` 会从 hybrid 融合中排除该通道,但提高权重不能恢复融合前已被拒绝的候选。零权重不会关闭该通道的检索和准入工作:
vector 查询仍可能生成 embedding,检索与准入计数也仍包含零权重通道。如果 Topic Artifact 搜索请求显式提供自己的 `fusion`
参数,它会完整取代部署级召回权重;只有请求未指定 `fusion` 时,部署级权重才生效。归一化后,每个非零通道权重必须不低于 IEEE 754 binary64
最小正规数(`2.2250738585072014e-308`),更小的值会在启动时被拒绝。只有显式配置 `0` 才表示关闭该通道的融合贡献。调整权重时应同时评估
recall gate 轮次、语义准入下限和历史 Topic 候选选择,不要将这些配置孤立调整。特别是,提高 FTS 权重可能使 Topic 的归一化分数跨过
`POWERCONTEXT_SERVER_RUNTIME_TOPIC_MEMORY_HISTORY_RRF_THRESHOLD`(默认为 `70`),从而改变传给规划器的历史 Topic 集合。

召回充分性门控默认关闭。启用后,Runtime 在 `prepare_context` 阶段评估首轮候选的数量、来源家族覆盖、最优候选分数和词法
覆盖;判定为不足时最多追加两轮搜索,并在每轮放宽候选准入的语义相似度下限。门控判断本身不调用模型,追加轮次沿用同一请求的
Scope、家族、条数限制和上下文预算,并复用已经生成的查询向量。第一轮和第二轮的语义相似度下限必须保持递减顺序,违反该顺序
Expand Down
8 changes: 5 additions & 3 deletions docs/zh/rfcs/0014_memory_layer_design.md
Original file line number Diff line number Diff line change
Expand Up @@ -680,11 +680,13 @@ MVP 定义四种 mode:
低于阈值的候选不会为了填满 `limit` 而恢复。准入后的候选分别保持 backend 内部排序,再使用 reciprocal rank fusion:

```text
rrf_score(candidate) = Σ 1 / (60 + rank_in_channel)
hybrid_rrf_score(candidate) = Σ channel_weight / (60 + rank_in_channel)
```

`rank_in_channel` 从 1 开始;单独的 `fts` 或 `vector` mode 也用单通道公式生成公开 score。被某通道拒绝或未被该通道
召回的候选只贡献另一通道分数,`matched_by` 也只包含实际准入的通道。OceanBase adapter 可以使用 4.6+ 的原生
`rank_in_channel` 从 1 开始。部署的 FTS/vector 权重为非负相对比例:PowerContext 会将其归一化到平均值
为 1,默认均为 `1.0`,并要求至少一个为正数。权重只影响 `hybrid`;单独的 `fts` 或 `vector` mode 仍以
`1 / (60 + rank)` 生成公开 score。零权重通道不产生候选,也不出现在 `matched_by` 中。被某通道拒绝或未被该通道召回的
候选只贡献另一通道分数。OceanBase adapter 可以使用 4.6+ 的原生
`HYBRID_SEARCH` 优化,但对外必须保持
同样的 active-head 过滤、RRF 参数和稳定 tie-break;4.3.5 baseline 使用应用层 RRF。SQLite 使用 FTS5 与 Vec1 结果执行
相同的应用层 RRF。相同 RRF score 按 `memory_artifact_id`、UTF-8 `entry_id`、`entry_version_id` 升序打破平局。
Expand Down
5 changes: 4 additions & 1 deletion docs/zh/rfcs/1417_topic_memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,10 @@ Chunk 策略变化需要重建对应检索投影。

每个检索通道先按 Topic collapse,同一 Topic 的多个 detail 命中只保留最佳位置用于 snippet。当前部署启用的
两个或四个通道通过 RRF 排名融合,不比较全文分数和向量距离的原始数值。最终一个 Topic 最多占一个结果位置,并保留
`matched_by` 说明命中通道。每个通道都先按 Topic 折叠,再应用候选上限,因此单个 Topic 的大量命中 chunk 不会
`matched_by` 说明被选中 Topic 通过准入的所有命中通道。零权重通道不贡献排名分数,也无法单独选中 Topic;但当同一 Topic
被其他正权重通道选中时,该通道的证据仍保留在 `matched_by` 中。`hybrid` mode 对对应类型的 Topic 和 Detail 通道同时应用部署级
归一化 FTS/vector 相对权重,单独的 FTS 和 vector mode 仍不加权。开启通道的加权理论最大值作为归一化分母,使公开 score 保持在
`0..100`。每个通道都先按 Topic 折叠,再应用候选上限,因此单个 Topic 的大量命中 chunk 不会
遮蔽其他 Topic。全文 snippet 以 Analyzer v1 查询命中为中心截取有界窗口;只有向量命中时,则稳定截取其命中 chunk
内部的窗口。
首版搜索 limit 与历史候选配置统一限制为 20。查询最多 8,192 个字符、64 个不同的 Analyzer term;构造后端
Expand Down
3 changes: 2 additions & 1 deletion integrations/dify/plugin/powercontext_dify/contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,8 @@
"$ref": "#/components/schemas/AtomicMemoryRecord"
},
"score": {
"type": "number"
"type": "number",
"description": "Raw RRF score. Hybrid mode applies the deployment's normalized relative FTS/vector weights."
},
"matched_by": {
"type": "array",
Expand Down
4 changes: 4 additions & 0 deletions integrations/dify/plugin/tools/pc_search.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ output_schema:
score:
$ref: '#/definitions/Result/properties/hits/items/properties/score'
type: number
description: Raw RRF score. Hybrid mode applies the deployment's normalized
relative FTS/vector weights.
matched_by:
$ref: '#/definitions/Result/properties/hits/items/properties/matched_by'
type: array
Expand Down Expand Up @@ -249,6 +251,8 @@ output_schema:
- type: 'null'
score:
type: number
description: Raw RRF score. Hybrid mode applies the deployment's normalized
relative FTS/vector weights.
matched_by:
type: array
items:
Expand Down
3 changes: 3 additions & 0 deletions openapi/powercontext.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5141,6 +5141,7 @@ components:
$ref: '#/components/schemas/AtomicMemoryRecord'
score:
type: number
description: Raw RRF score. Hybrid mode applies the deployment's normalized relative FTS/vector weights.
matched_by:
type: array
items:
Expand Down Expand Up @@ -10205,6 +10206,8 @@ components:
score:
type: number
minimum: 0
maximum: 100
description: Weighted RRF relevance normalized to the 0..100 range.
matched_by:
type: array
minItems: 1
Expand Down
24 changes: 21 additions & 3 deletions src/powercontext/builtin/artifacts/memory/fusion.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@
MemoryHit,
MemoryMatchedBy,
)
from powercontext.builtin.artifacts.search import AdmissionFloor, admits_fts_text, unit_l2_cosine_similarity
from powercontext.builtin.artifacts.search import (
AdmissionFloor,
RecallChannelWeights,
admits_fts_text,
unit_l2_cosine_similarity,
)

_RRF_CONSTANT = 60
_MIN_SEMANTIC_SIMILARITY = 0.3
Expand Down Expand Up @@ -65,8 +70,13 @@ def fuse_rankings(
fts: Sequence[MemoryChannelHit],
vector: Sequence[MemoryChannelHit],
limit: int,
channel_weights: RecallChannelWeights | None = None,
) -> tuple[MemoryHit, ...]:
"""Fuse ordered channel candidates with reciprocal rank fusion."""
"""Fuse ordered channel candidates with reciprocal rank fusion.

``channel_weights=None`` is the historical unweighted behavior used by the
single-channel modes. A caller supplies weights only for hybrid search.
"""

if limit < 1:
raise ValueError("memory search limit must be positive") # noqa: TRY003
Expand All @@ -76,15 +86,22 @@ def fuse_rankings(
channels: dict[_HitIdentity, set[MemoryMatchedBy]] = {}
relevance: dict[_HitIdentity, float] = {}

weights: dict[MemoryMatchedBy, float] = {
"fts": 1.0 if channel_weights is None else channel_weights.fts,
"vector": 1.0 if channel_weights is None else channel_weights.vector,
}
for channel, ranking in (("fts", fts), ("vector", vector)):
weight = weights[channel]
if weight == 0.0:
continue
seen: set[_HitIdentity] = set()
for rank, candidate in enumerate(ranking, start=1):
identity = _identity(candidate)
if identity in seen:
continue
seen.add(identity)
candidates.setdefault(identity, candidate)
scores[identity] = scores.get(identity, 0.0) + 1.0 / (_RRF_CONSTANT + rank)
scores[identity] = scores.get(identity, 0.0) + weight / (_RRF_CONSTANT + rank)
channels.setdefault(identity, set()).add(channel)
if channel == "vector" and candidate.distance is not None:
similarity = unit_l2_cosine_similarity(candidate.distance)
Expand All @@ -107,6 +124,7 @@ def fuse_rankings(
text=candidates[identity].text,
score=scores[identity],
matched_by=tuple(channel for channel in ("fts", "vector") if channel in channels[identity]),
score_upper_bound=sum(weights[channel] for channel in channels[identity]) / (_RRF_CONSTANT + 1),
relevance=relevance.get(identity),
)
for identity in ordered
Expand Down
Loading
Loading