Skip to content
Open
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
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
node --test \
examples/codex-memory-plugin/servers/mcp-proxy.test.mjs \
examples/codex-memory-plugin/scripts/auto-capture.test.mjs \
examples/codex-memory-plugin/scripts/config.test.mjs \
examples/codex-memory-plugin/scripts/capture-utils.test.mjs \
examples/codex-memory-plugin/scripts/marketplace.test.mjs \
examples/codex-memory-plugin/scripts/ov-credentials.test.mjs \
Expand Down
2 changes: 2 additions & 0 deletions docs/en/agent-integrations/10-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ node examples/opencode-plugin/scripts/setup.mjs
}
```

`autoRecall.scoreThreshold` follows the reranker's native score scale. Cosine-like rerankers commonly use `0.0` to `1.0`; logit-scale rerankers may require negative values.

Environment variables override `ovcli.conf`:

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/en/agent-integrations/11-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ node ~/.pi/agent/extensions/openviking/scripts/setup.mjs

An explicit peer from `OPENVIKING_PEER_ID`, `ovcli.conf`, or `ov.conf` takes precedence over `config.json`'s `peerId`. The local `peerId` takes precedence over the workspace-derived peer, which is used only when `workspacePeer` is enabled and neither explicit source provides a peer.

`scoreThreshold` follows the reranker's native score scale. Cosine-like rerankers commonly use `0.0` to `1.0`; logit-scale rerankers may require negative values.

Takeover is fail-open: if pending writes cannot flush, commit fails, the archive overview is not ready, or the branch fingerprint no longer matches, pi keeps full local history or falls back to its default compaction. Set `OV_DEBUG_LOG=/tmp/ov-pi.log` when validating boundary advances.

## Verify
Expand Down
2 changes: 2 additions & 0 deletions docs/en/agent-integrations/17-dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Behavior knobs live in the profile's Cordis patch entry:
commitTokenThreshold: 20000
```

`scoreThreshold` follows the reranker's native score scale. Cosine-like rerankers commonly use `0.0` to `1.0`; logit-scale rerankers may require negative values.

Credentials given in the patch win over the environment; behavior toggles read the environment first. The full list is documented in the [bundle README](https://github.com/volcengine/OpenViking/tree/main/examples/dsh-memory-plugin).

</details>
Expand Down
2 changes: 1 addition & 1 deletion docs/en/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ Reranking model for search result refinement. Supports VikingDB (Volcengine), Co
| `model` | str | Model name (for `openai` providers) |
| `timeout` | float | HTTP request timeout in seconds for OpenAI-compatible providers. Increase for slow or cold-starting local rerank servers. Default: `30.0` |
| `max_input_tokens` | int | Maximum estimated raw-text tokens in each query-document pair sent to the reranker. Oversized inputs retain their beginning and end. `0` disables truncation. Default: `0` |
| `threshold` | float | Score threshold between `0.0` and `1.0`; results below this are filtered out. Default: `0.1` |
| `threshold` | float | Score threshold in the reranker's native scale; results below this are filtered out. Cosine-like rerankers often use `0.0``1.0`, while logit-scale rerankers may require negative values. Default: `0.1` |
| `extra_headers` | object | Custom HTTP headers (for OpenAI-compatible providers, optional) |

**Supported providers:**
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/agent-integrations/10-opencode.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ node examples/opencode-plugin/scripts/setup.mjs
}
```

`autoRecall.scoreThreshold` 按 reranker 原始分数尺度解释;余弦类 reranker 通常是 `0.0` 到 `1.0`,logit-scale reranker 可能需要负值。

环境变量优先级高于 `ovcli.conf`:

```bash
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/agent-integrations/11-pi.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ node ~/.pi/agent/extensions/openviking/scripts/setup.mjs

`OPENVIKING_PEER_ID`、`ovcli.conf` 或 `ov.conf` 中的显式 peer 优先于 `config.json` 的 `peerId`;本地 `peerId` 又优先于 workspace 派生值。只有启用 `workspacePeer` 且所有显式来源都未提供 peer 时,才会从当前工作目录派生。

`scoreThreshold` 按 reranker 原始分数尺度解释;余弦类 reranker 通常是 `0.0` 到 `1.0`,logit-scale reranker 可能需要负值。

Takeover 是 fail-open:如果 pending 写入无法 flush、commit 失败、archive overview 尚未生成,或 branch 指纹不匹配,pi 会继续保留完整本地历史,或回退到默认 compaction。验证边界推进时可设置 `OV_DEBUG_LOG=/tmp/ov-pi.log`。

## 验证
Expand Down
2 changes: 2 additions & 0 deletions docs/zh/agent-integrations/17-dsh.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ bash <(curl -fsSL https://ovrelease.tos-cn-beijing.volces.com/memory-plugin-shar
commitTokenThreshold: 20000
```

`scoreThreshold` 按 reranker 原始分数尺度解释;余弦类 reranker 通常是 `0.0` 到 `1.0`,logit-scale reranker 可能需要负值。

patch 中写的凭证优先于环境变量;行为开关则优先读环境变量。完整参数列表见[插件 README](https://github.com/volcengine/OpenViking/tree/main/examples/dsh-memory-plugin)。

</details>
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/guides/01-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ PDF 解析配置。支持三种策略:`local`(本地 pdfplumber)、`mineru
| `model` | str | 模型名称(用于 `openai` 提供方) |
| `timeout` | float | OpenAI 兼容 provider 的 HTTP 请求超时时间,单位为秒。对于较慢或冷启动的本地 rerank 服务可适当增大。默认:`30.0` |
| `max_input_tokens` | int | 每个 query-document 对发送给 reranker 的最大估算原始文本 token 数;超长输入会保留开头和结尾。`0` 表示不截断。默认:`0` |
| `threshold` | float | 分数阈值,范围为 `0.0` 到 `1.0`。低于此值的结果会被过滤。默认:`0.1` |
| `threshold` | float | 按 reranker 原始分数尺度解释的分数阈值;低于此值的结果会被过滤。余弦类 reranker 通常是 `0.0` 到 `1.0`,logit-scale reranker 可能需要负值。默认:`0.1` |
| `extra_headers` | object | 自定义 HTTP 请求头(OpenAI 兼容 provider 可用,可选) |

**支持的提供方:**
Expand Down
2 changes: 1 addition & 1 deletion examples/claude-code-memory-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ By default the plugin derives a peer from the workspace path using Claude's proj
| `OPENVIKING_RECALL_QUERY_EXPANSION` | `auto` | `auto` lets the server widen short prompts using session context; `off` disables it |
| `OPENVIKING_RECALL_COMPRESS` | `auto` | Digest compression: `off`, `client` (host CLI), `server`, or `auto` (local first, server fallback) |
| `OPENVIKING_RECALL_COMPRESS_MAX_BULLETS` | `6` | Digest bullet ceiling |
| `OPENVIKING_SCORE_THRESHOLD` | `0.35` | Min relevance score (0–1) |
| `OPENVIKING_SCORE_THRESHOLD` | `0.35` | Min relevance score in the reranker's native scale; logit rerankers may need negative values such as `-8` |
| `OPENVIKING_MIN_QUERY_LENGTH` | `3` | Skip recall for very short queries |

Recall defaults to the broad mode: global memory, the current workspace, and other workspace memories can all be recalled, with other workspaces penalized and rendered later. Set `OPENVIKING_RECALL_PEER_SCOPE=actor` for the isolation mode, which only sees global memory plus the current workspace. In deployments where one bot serves multiple real people, such as zouk, vikingbot, or AstrBot, use the isolation mode with an explicit actor peer so one person's memories are not recalled into another person's session.
Expand Down
2 changes: 1 addition & 1 deletion examples/claude-code-memory-plugin/README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ claude
| `OPENVIKING_RECALL_TOKEN_BUDGET` | `2000` | 仅用于最终 raw-find fallback 的内联 token 预算 |
| `OPENVIKING_RECALL_MAX_CONTENT_CHARS` | `500` | 单条记忆内容字符上限 |
| `OPENVIKING_RECALL_PREFER_ABSTRACT` | `true` | 有 abstract 时优先用 abstract 而非完整 body |
| `OPENVIKING_SCORE_THRESHOLD` | `0.35` | 最低相关度得分(0–1) |
| `OPENVIKING_SCORE_THRESHOLD` | `0.35` | 按 reranker 原始分数尺度解释的最低相关度得分;logit reranker 可能需要 `-8` 这类负值 |
| `OPENVIKING_MIN_QUERY_LENGTH` | `3` | 短于此长度的 query 跳过召回 |
| `OPENVIKING_LOG_RANKING_DETAILS` | `false` | 每候选打分日志(很啰嗦) |
| `OPENVIKING_RECALL_MAX_TOKENS` | `1600` | 服务端组装上下文块的 token 预算(与本地压缩输入上限相互独立) |
Expand Down
22 changes: 14 additions & 8 deletions examples/claude-code-memory-plugin/scripts/auto-recall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,15 @@ function approve(msg) {
// Ranking (ported from openclaw-plugin/memory-ranking.ts)
// ---------------------------------------------------------------------------

function clampScore(v) {
function numericScore(v) {
if (typeof v !== "number" || Number.isNaN(v)) return 0;
return Math.max(0, Math.min(1, v));
return v;
}

function formatScore(score) {
const value = numericScore(score);
if (value >= 0 && value <= 1) return `${(value * 100).toFixed(0)}%`;
return value.toFixed(2);
}

const PREFERENCE_QUERY_RE = /prefer|preference|favorite|favourite|like|偏好|喜欢|爱好|更倾向/i;
Expand Down Expand Up @@ -80,7 +86,7 @@ function lexicalOverlapBoost(tokens, text) {
}

function rankItem(item, profile) {
const base = clampScore(item.score);
const base = numericScore(item.score);
const abstract = (item.abstract || item.overview || "").trim();
const cat = (item.category || "").toLowerCase();
const uri = (item.uri || "").toLowerCase();
Expand Down Expand Up @@ -263,12 +269,12 @@ async function buildInjectionBlock(items, actorPeerId = "") {
let hintCount = 0;

for (const item of items) {
const score = (clampScore(item.score) * 100).toFixed(0);
const uriLine = `- [${item._sourceType} ${score}%] ${item.uri}`;
const score = formatScore(item.score);
const uriLine = `- [${item._sourceType} ${score}] ${item.uri}`;

if (budgetRemaining > 0) {
const content = await resolveItemContent(item, actorPeerId);
const contentLine = `- [${item._sourceType} ${score}%] ${content}`;
const contentLine = `- [${item._sourceType} ${score}] ${content}`;
const lineTokens = estimateTokens(contentLine);

// First item always included even if over budget (openclaw spec §6.2)
Expand Down Expand Up @@ -421,7 +427,7 @@ async function main() {
}

const profile = buildQueryProfile(userPrompt);
const filtered = raw.filter(it => clampScore(it.score) >= cfg.scoreThreshold);
const filtered = raw.filter(it => numericScore(it.score) >= cfg.scoreThreshold);
filtered.sort((a, b) => rankItem(b, profile) - rankItem(a, profile));
const deduped = dedupeItems(filtered);
const picked = deduped.slice(0, cfg.recallLimit);
Expand All @@ -430,7 +436,7 @@ async function main() {
filteredCount: filtered.length,
dedupedCount: deduped.length,
pickedCount: picked.length,
items: picked.map(it => ({ type: it._sourceType, uri: it.uri, score: clampScore(it.score) })),
items: picked.map(it => ({ type: it._sourceType, uri: it.uri, score: numericScore(it.score) })),
});

if (picked.length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions examples/claude-code-memory-plugin/scripts/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,10 @@ export function loadConfig() {
))),
recallLimitConfigured: Boolean(process.env.OPENVIKING_RECALL_LIMIT) ||
hasOwn(cc, "recallLimit"),
scoreThreshold: Math.min(1, Math.max(0, num(
scoreThreshold: num(
process.env.OPENVIKING_SCORE_THRESHOLD,
num(cc.scoreThreshold, 0.35),
))),
),
minQueryLength: Math.max(1, Math.floor(num(
process.env.OPENVIKING_MIN_QUERY_LENGTH,
num(cc.minQueryLength, 3),
Expand Down
17 changes: 17 additions & 0 deletions examples/claude-code-memory-plugin/scripts/config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const OVERRIDES = [
"OPENVIKING_CLI_CONFIG_FILE",
"OPENVIKING_API_KEY",
"OPENVIKING_BEARER_TOKEN",
"OPENVIKING_SCORE_THRESHOLD",
];

/**
Expand Down Expand Up @@ -112,3 +113,19 @@ test("no api_key anywhere reports no source", () => {
assert.equal(cfg.credentialPath, null);
});
});

test("score threshold accepts negative reranker logits from the environment", () => {
withConfigs({
env: { OPENVIKING_SCORE_THRESHOLD: "-8" },
}, () => {
assert.equal(loadConfig().scoreThreshold, -8);
});
});

test("score threshold accepts positive reranker logits above one from the environment", () => {
withConfigs({
env: { OPENVIKING_SCORE_THRESHOLD: "4.5" },
}, () => {
assert.equal(loadConfig().scoreThreshold, 4.5);
});
});
22 changes: 14 additions & 8 deletions examples/claude-code-memory-plugin/scripts/debug-recall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,15 @@ async function fetchJSON(path, init = {}, options = {}) {
// Pure functions — copied from auto-recall.mjs
// ---------------------------------------------------------------------------

function clampScore(v) {
function numericScore(v) {
if (typeof v !== "number" || Number.isNaN(v)) return 0;
return Math.max(0, Math.min(1, v));
return v;
}

function formatScore(score) {
const value = numericScore(score);
if (value >= 0 && value <= 1) return value.toFixed(4);
return value.toFixed(2);
}

const PREFERENCE_QUERY_RE = /prefer|preference|favorite|favourite|like|偏好|喜欢|爱好|更倾向/i;
Expand Down Expand Up @@ -133,7 +139,7 @@ function lexicalOverlapBoost(tokens, text) {
}

function rankForInjection(item, profile) {
const base = clampScore(item.score);
const base = numericScore(item.score);
const abstract = (item.abstract || item.overview || "").trim();
const cat = (item.category || "").toLowerCase();
const uri = item.uri.toLowerCase();
Expand Down Expand Up @@ -173,11 +179,11 @@ function pickMemories(items, limit, queryText) {

function postProcess(items, limit, threshold) {
const seen = new Set();
const sorted = [...items].sort((a, b) => clampScore(b.score) - clampScore(a.score));
const sorted = [...items].sort((a, b) => numericScore(b.score) - numericScore(a.score));
const result = [];
for (const item of sorted) {
if (item.level !== 2) continue;
if (clampScore(item.score) < threshold) continue;
if (numericScore(item.score) < threshold) continue;
const cat = (item.category || "").toLowerCase() || "unknown";
const abs = (item.abstract || item.overview || "").trim().toLowerCase();
const key = abs ? `${cat}:${abs}` : `uri:${item.uri}`;
Expand Down Expand Up @@ -302,7 +308,7 @@ function printSearchResults(label, items) {
return;
}
for (const item of items) {
const score = clampScore(item.score).toFixed(4);
const score = formatScore(item.score);
console.log(` ${C.bold}${item.uri}${C.reset}`);
dim(` score=${score} level=${item.level} category=${item.category || "(none)"}`);
dim(` abstract: ${(item.abstract || item.overview || "(none)").trim().slice(0, 120)}`);
Expand Down Expand Up @@ -378,7 +384,7 @@ async function main() {
const rankedItems = [...processed].sort((a, b) => rankForInjection(b, profile) - rankForInjection(a, profile));

for (const item of rankedItems) {
const base = clampScore(item.score);
const base = numericScore(item.score);
const abstract = (item.abstract || item.overview || "").trim();
const cat = (item.category || "").toLowerCase();
const uri = item.uri.toLowerCase();
Expand Down Expand Up @@ -412,7 +418,7 @@ async function main() {
const lines = [];
for (const item of memories) {
console.log(` ${C.green}*${C.reset} ${C.bold}${item.uri}${C.reset}`);
dim(` score=${clampScore(item.score).toFixed(4)} level=${item.level} category=${item.category || "(none)"}`);
dim(` score=${formatScore(item.score)} level=${item.level} category=${item.category || "(none)"}`);

let lineText;
if (item.level === 2) {
Expand Down
22 changes: 14 additions & 8 deletions examples/claude-code-memory-plugin/scripts/shared/recall-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,15 @@ export function downgradeToRecallBody(contextBody = {}, cfg = {}) {
return body;
}

function clampScore(v) {
function numericScore(v) {
if (typeof v !== "number" || Number.isNaN(v)) return 0;
return Math.max(0, Math.min(1, v));
return v;
}

function formatScore(score) {
const value = numericScore(score);
if (value >= 0 && value <= 1) return `${(value * 100).toFixed(0)}%`;
return value.toFixed(2);
}

function buildQueryProfile(query) {
Expand All @@ -209,7 +215,7 @@ function lexicalOverlapBoost(tokens, text) {
}

function rankItem(item, profile) {
const base = clampScore(item.score);
const base = numericScore(item.score);
const abstract = (item.abstract || item.overview || "").trim();
const cat = (item.category || "").toLowerCase();
const uri = (item.uri || "").toLowerCase();
Expand Down Expand Up @@ -349,12 +355,12 @@ async function buildFallbackInjectionBlock(fetchJSON, items, cfg, actorPeerId =
let hintCount = 0;

for (const item of items) {
const score = (clampScore(item.score) * 100).toFixed(0);
const uriLine = `- [${item._sourceType} ${score}%] ${item.uri}`;
const score = formatScore(item.score);
const uriLine = `- [${item._sourceType} ${score}] ${item.uri}`;

if (budgetRemaining > 0) {
const content = await resolveItemContent(fetchJSON, item, cfg, actorPeerId);
const contentLine = `- [${item._sourceType} ${score}%] ${content}`;
const contentLine = `- [${item._sourceType} ${score}] ${content}`;
const lineTokens = estimateTokens(contentLine);

if (lineTokens > budgetRemaining && contentCount > 0) {
Expand Down Expand Up @@ -598,14 +604,14 @@ export async function buildRecallBlock(fetchJSON, cfg, query, options = {}) {

const profile = buildQueryProfile(trimmed);
const scoreThreshold = Number.isFinite(Number(cfg.scoreThreshold)) ? Number(cfg.scoreThreshold) : 0.35;
const filtered = raw.filter((it) => clampScore(it.score) >= scoreThreshold);
const filtered = raw.filter((it) => numericScore(it.score) >= scoreThreshold);
filtered.sort((a, b) => rankItem(b, profile) - rankItem(a, profile));
const picked = dedupeItems(filtered).slice(0, recallLimit);
log("recall_picked", {
rawCount: raw.length,
filteredCount: filtered.length,
pickedCount: picked.length,
items: picked.map((it) => ({ type: it._sourceType, uri: it.uri, score: clampScore(it.score) })),
items: picked.map((it) => ({ type: it._sourceType, uri: it.uri, score: numericScore(it.score) })),
});

if (picked.length === 0) return null;
Expand Down
11 changes: 6 additions & 5 deletions examples/claude-code-memory-plugin/scripts/skill-experience.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ function skillNameFromContent(path) {
return basename(path.replace(/\/SKILL\.md$/, ""));
}

function clampScore(value) {
function formatScore(value) {
const num = Number(value);
if (!Number.isFinite(num)) return 0;
return Math.max(0, Math.min(1, num));
if (!Number.isFinite(num)) return "0%";
if (num >= 0 && num <= 1) return `${Math.round(num * 100)}%`;
return num.toFixed(2);
}

async function findExperiences(query) {
Expand All @@ -119,9 +120,9 @@ function buildContext(skillName, items) {
`Relevant prior experience for skill: ${skillName}`,
];
for (const item of items) {
const score = Math.round(clampScore(item.score) * 100);
const score = formatScore(item.score);
const text = String(item.abstract || item.overview || item.uri || "").replace(/\s+/g, " ").trim();
lines.push(`- [experience ${score}%] ${text} (${item.uri})`);
lines.push(`- [experience ${score}] ${text} (${item.uri})`);
}
lines.push("Use these as operational guidance, not user facts.");
lines.push("</openviking-context>");
Expand Down
Loading