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
6 changes: 4 additions & 2 deletions docs/en/agent-integrations/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Every integration on this page connects to a running OpenViking server. If you d

Query expansion and recall-result compression are two independent, optional model calls. Disable both in the Agent plugin when response latency matters most; semantic retrieval, budgeting, tier degradation, and cross-turn dedup continue to work.

The same environment variables apply to both the Claude Code and Codex memory plugins:
The same environment variables apply to both the Claude Code and Codex memory plugins. Query expansion is also switchable in OpenCode and pi; compression is Claude Code and Codex only.

```bash
export OPENVIKING_RECALL_QUERY_EXPANSION=off
Expand Down Expand Up @@ -56,4 +56,6 @@ The same settings can live in `~/.openviking/ovcli.conf`:

Environment variables take precedence over `ovcli.conf`. Restart the Agent after changing these settings so its hook processes reload the configuration. These are plugin-client settings; the server's `ov.conf` does not need to change.

When Claude Code asks the server for a digest, the context request waits longer than an ordinary request: the server's own rewrite fuse is `retrieval.recall_rewrite_timeout_s` (30s by default), and aborting earlier would discard the whole response rather than just the digest. Set `OPENVIKING_RECALL_CONTEXT_TIMEOUT_MS` (or `plugin.recallContextTimeoutMs`) to pin that deadline — keep it above the server's fuse and below the Agent's own hook timeout.
The `plugin` section is read by the Claude Code and Codex plugins, so a `plugin` entry named after another harness is currently inert. OpenCode and pi read `OPENVIKING_RECALL_QUERY_EXPANSION` from the environment (or `recallQueryExpansion` in their own config file) but not `OPENVIKING_RECALL_COMPRESS`, since neither requests a server digest.

A context request waits longer than an ordinary request, because aborting it client-side discards the whole response rather than just the stage that ran long. The server pipeline is serial and each optional stage has its own fuse: query expansion (`retrieval.recall_intent_timeout_s`, 5s) runs first, then retrieval, body reads and budgeting, and only then the digest rewrite (`retrieval.recall_rewrite_timeout_s`, 30s). The deadline therefore follows what the request actually asks for — 15s once it carries a session and can spend the expansion fuse, 45s when it also asks for a digest, and the plugin's ordinary timeout when it asks for neither. Set `OPENVIKING_RECALL_CONTEXT_TIMEOUT_MS` (or `plugin.recallContextTimeoutMs`) to pin it — keep it above the fuses the request will spend and below the Agent's own hook timeout.
9 changes: 6 additions & 3 deletions docs/en/api/06-retrieval.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,10 +655,11 @@ Injecting context every turn used to mean searching per type, reading each hit b
| `events` | overview | full | The one memory type whose body is long enough for `# Summary` extraction to be a real compression |
| `entities` / `preferences` / `experiences` | abstract | abstract | Short bodies, and the writer stores the whole body in the abstract scalar, so abstract already is the complete file |
| `resources` / `skills` | abstract | abstract | The 256-char abstract from semantic processing; bodies can be large or carry credentials, so deepening is opt-in |
| `memories` | abstract | abstract | Built-in memory types outside the four named ones — `cases`, `patterns`, `tools`, `trajectories`, skill-usage memories. Only quota-free retrieval reaches them; they own no bucket, so `quotas` cannot name them, but `detail` and `other_peer_penalty` can |
| Directory hits | overview | overview | A directory has no abstract, so it reads the `.overview.md` sidecar; a full tier is meaningless for a subtree |

- **Floor**: every result carries at least its `uri`. When a category's default tier yields nothing usable — a resource that never went through semantic processing, or an abstract that busts the per-entry cap — the entry falls back to overview instead of degrading to a bare pointer
- **Explicit `detail`**: sets that tier as both the requested start and ceiling; entries that do not fit still step down a tier rather than being truncated
- **Floor**: every result carries at least its `uri`. When a memory abstract is unavailable or busts the per-entry cap, the entry falls back to overview: the memory writer stores the whole body in that scalar, so for memory categories overview sits *below* abstract on the content ladder and the substitute discloses less. A `resources` or `skills` abstract is the short generated summary instead, so the same substitution would read a body the caller never asked for — those two degrade to a bare `uri` rather than deepen
- **Explicit `detail`**: sets that tier as both the requested start and ceiling; entries that do not fit still step down a tier rather than being truncated. The memory overview substitute above is the one case where the served `detail` can outrank the pin, and only because it carries less content than the pinned tier would
- **Overview by source type**: memory files use the leading `# Summary` section, code files use class and function signatures (reusing `code_outline`), long documents use the heading tree plus first paragraph
- **Per-entry cap**: `max_tokens ÷ candidate_count × 2`, applied to every tier except the bare `uri`; a tier exceeding it falls back to the previous tier rather than being truncated. If budget is still left over, one final deepening pass ignores the cap and is bounded only by `max_tokens`

Expand Down Expand Up @@ -744,7 +745,7 @@ curl -X POST http://localhost:1933/api/v1/search/search \
| Field | Type | Description |
|-------|------|-------------|
| `entries[].uri` | string | Entry URI, always present at every tier, expandable with the MCP `read` tool |
| `entries[].category` | string | `events`/`entities`/`preferences`/`experiences`/`resources`/`skills` |
| `entries[].category` | string | `events`/`entities`/`preferences`/`experiences`/`resources`/`skills`, or `memories` for a built-in memory type outside those four |
| `entries[].detail` | string | Tier actually served: `full`, `overview`, `abstract` or `uri` |
| `entries[].text` | string | Body for that tier; empty at the `uri` tier |
| `rendered` | string | Flat XML context block, ready to inject; empty when rewrite reports `no_relevant` |
Expand All @@ -754,6 +755,8 @@ curl -X POST http://localhost:1933/api/v1/search/search \
When `stats.rewrite` is `no_relevant`, the response keeps `entries` for
inspection but returns both `digest` and `rendered` as empty strings. This makes
the successful empty result safe for clients that predate the explicit status.
Nothing was served that turn, so those URIs also stay out of the `dedup_turns`
ledger and remain available to the later turn they are relevant to.

**Validation rules**

Expand Down
3 changes: 2 additions & 1 deletion docs/en/api/16-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ The response shape matches the context face (flat entries, flat XML in `rendered
"origins": {"actor_peer": 0, "self": 1, "other_peer": 0},
"deprecated": {
"endpoint": "/api/v1/search/recall",
"successor": "/api/v1/search/search?mode=context",
"successor": "/api/v1/search/search",
"successor_body": {"mode": "context"},
"aliases_used": ["max_chars"]
}
}
Expand Down
6 changes: 4 additions & 2 deletions docs/zh/agent-integrations/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ OpenViking 可以作为多种 Agent 运行时的长期记忆与上下文后端

查询扩展和召回结果压缩是两个独立的可选模型调用。需要优先保证响应速度时,可以在 Agent 插件端同时关闭它们;语义检索、预算控制、档位降级和跨轮去重仍会正常工作。

下面这组环境变量同时适用于 Claude Code 和 Codex
下面这组环境变量同时适用于 Claude Code 和 Codex。查询扩展在 OpenCode 和 pi 上同样可以关闭;压缩只有 Claude Code 和 Codex 支持。

```bash
export OPENVIKING_RECALL_QUERY_EXPANSION=off
Expand Down Expand Up @@ -56,4 +56,6 @@ export OPENVIKING_RECALL_COMPRESS=off

环境变量优先于 `ovcli.conf`。修改后重启对应的 Agent,让 hook 进程重新加载配置。上述设置属于插件客户端,不需要修改服务端的 `ov.conf`。

当 Claude Code 请求服务端生成 digest 时,这次 context 请求的等待时间比普通请求更长:服务端自身的 rewrite 保险丝是 `retrieval.recall_rewrite_timeout_s`(默认 30 秒),客户端提前中断会丢掉整个响应,而不只是 digest。可以用 `OPENVIKING_RECALL_CONTEXT_TIMEOUT_MS`(或 `plugin.recallContextTimeoutMs`)指定这个上限,取值应高于服务端保险丝、低于 Agent 自身的 hook 超时。
`plugin` 段目前由 Claude Code 和 Codex 插件读取,因此以其他 harness 命名的 `plugin` 条目当前不生效。OpenCode 和 pi 支持用环境变量 `OPENVIKING_RECALL_QUERY_EXPANSION`(或各自配置文件里的 `recallQueryExpansion`)关闭查询扩展,但不支持 `OPENVIKING_RECALL_COMPRESS`——这两个 harness 不会请求服务端 digest。

context 请求的等待时间比普通请求更长,因为客户端提前中断会丢掉整个响应,而不只是超时的那一段。服务端流水线是串行的,每个可选阶段各有保险丝:先是查询扩展(`retrieval.recall_intent_timeout_s`,5 秒),然后是检索、正文读取和预算规划,最后才是 digest 重写(`retrieval.recall_rewrite_timeout_s`,30 秒)。因此这个上限按请求实际启用的阶段决定——带 session、会走查询扩展时取 15 秒,同时还要 digest 时取 45 秒,两者都不涉及时沿用插件自身的普通超时。可以用 `OPENVIKING_RECALL_CONTEXT_TIMEOUT_MS`(或 `plugin.recallContextTimeoutMs`)指定这个上限,取值应高于该请求会用到的保险丝、低于 Agent 自身的 hook 超时。
11 changes: 7 additions & 4 deletions docs/zh/api/06-retrieval.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,11 @@ Agent 插件每轮注入上下文时,过去需要按类型逐个检索、再
| `events` | 概览档 | 全文档 | 唯一正文足够长、`# Summary` 抽取能真正压缩的类型 |
| `entities` / `preferences` / `experiences` | 摘要档 | 摘要档 | 正文本身很短,且写入侧把整篇正文存进了摘要标量,摘要档即完整内容 |
| `resources` / `skills` | 摘要档 | 摘要档 | 语义处理生成的 256 字符摘要;正文可能很大或含凭据,加深需显式指定 |
| `memories` | 摘要档 | 摘要档 | 四个具名类型之外的内置记忆类型——`cases`、`patterns`、`tools`、`trajectories`、技能使用记忆。只有 quota-free 检索会命中它们;它们没有自己的检索桶,`quotas` 不能指定,但 `detail` 和 `other_peer_penalty` 可以 |
| 目录命中 | 概览档 | 概览档 | 目录没有摘要,读 `.overview.md` 侧车;全文档对目录无意义 |

- **保底**:每条结果至少给出 `uri`;类别默认档拿不到可用内容时(例如资源尚未跑过语义处理,或摘要本身超出单条上限)自动回落到概览档,而不是退成裸指针
- **显式 `detail`**:把该档作为全部结果请求的起点和上限;装不下的条目仍逐档退档而不截断
- **保底**:每条结果至少给出 `uri`。记忆类摘要缺失或超出单条上限时回落到概览档:写入侧把整篇正文存进了摘要标量,所以对记忆类别而言概览档在内容阶梯上位于摘要档*之下*,这次替换披露得更少。而 `resources` / `skills` 的摘要是语义处理生成的短摘要,同样的替换会去读调用方没有请求的正文,因此这两类直接退成裸 `uri`,不向上加深
- **显式 `detail`**:把该档作为全部结果请求的起点和上限;装不下的条目仍逐档退档而不截断。上述记忆类概览档替换是实际档位唯一可能高于指定档的情况,且仅因为它比指定档携带的内容更少
- **概览档按来源取骨架**:记忆文件取开头的 `# Summary` 段,代码文件取函数与类签名(复用 `code_outline`),长文档取标题树加首段
- **单条上限**:`max_tokens ÷ 候选条数 × 2`,对除裸 `uri` 外的所有档位一律生效;某一档超出该上限时退回上一档,不做截断。预算仍有剩余时,最后一轮加深不受该上限约束,只受 `max_tokens` 约束

Expand Down Expand Up @@ -746,15 +747,17 @@ curl -X POST http://localhost:1933/api/v1/search/search \
| 字段 | 类型 | 说明 |
|------|------|------|
| `entries[].uri` | string | 条目 URI,任何档位都必然存在,可用 MCP `read` 下钻 |
| `entries[].category` | string | `events`/`entities`/`preferences`/`experiences`/`resources`/`skills` |
| `entries[].category` | string | `events`/`entities`/`preferences`/`experiences`/`resources`/`skills`,或 `memories`(四个具名类型之外的内置记忆类型) |
| `entries[].detail` | string | 实际档位:`full`、`overview`、`abstract` 或 `uri` |
| `entries[].text` | string | 该档位的正文;`uri` 档为空 |
| `rendered` | string | 扁平 XML 上下文块,可直接注入;重写返回 `no_relevant` 时为空 |
| `digest` | string | 重写成功时的摘要;失败或压缩器判定无相关记忆时为空字符串 |
| `stats` | object | 预算用量、档位分布、扩展与重写状态(`off`、`ok`、`no_relevant`、`failed` 或 `timeout`)、去重账本状态;某个检索域失败时附带 `retrieval_errors`,用于区分「检索坏了」和「确实没有相关记忆」 |

当 `stats.rewrite` 为 `no_relevant` 时,响应仍保留 `entries` 供检查,但 `digest` 和
`rendered` 都为空字符串。这样即使客户端尚未识别新状态,也不会回退注入原文。
`rendered` 都为空字符串。这样即使客户端尚未识别新状态,也不会回退注入原文。本轮
没有交付任何内容,因此这些 URI 也不会进入 `dedup_turns` 账本,之后真正相关的那一轮
仍能召回它们。

**校验规则**

Expand Down
3 changes: 2 additions & 1 deletion docs/zh/api/16-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ recall(
"origins": {"actor_peer": 0, "self": 1, "other_peer": 0},
"deprecated": {
"endpoint": "/api/v1/search/recall",
"successor": "/api/v1/search/search?mode=context",
"successor": "/api/v1/search/search",
"successor_body": {"mode": "context"},
"aliases_used": ["max_chars"]
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* ovcli.conf carried connection fields only, so every harness had to keep its
* tuning knobs in ov.conf's harness section — a server-side file that a
* client-side plugin has no business editing. The `plugin` section fixes that:
* shared keys apply to every harness, and a per-harness object overrides them.
* shared keys apply to every harness that reads them, and a per-harness object
* overrides them.
*
* {
* "url": "...", "api_key": "...",
Expand All @@ -17,6 +18,13 @@
*
* Resolution stays env → ovcli.conf plugin.<harness> → ovcli.conf plugin →
* ov.conf harness section (legacy) → defaults.
*
* Consumers: Claude Code and Codex only. The other harnesses ship this module
* through `sync.mjs` but still read their knobs from the environment, so a
* `plugin` entry named after them is inert. `HARNESS_KEYS` lists what a harness
* loader actually consumes today — add a key here as its loader starts calling
* `loadPluginSettings`, not before, so the section never promises a knob that
* silently does nothing.
*/

import { readFileSync } from "node:fs";
Expand All @@ -28,10 +36,6 @@ const DEFAULT_OVCLI_CONF_PATH = join(homedir(), ".openviking", "ovcli.conf");
export const HARNESS_KEYS = {
claudeCode: "claude_code",
codex: "codex",
opencode: "opencode",
cursor: "cursor",
trae: "trae",
pi: "pi",
};

function tryLoadJson(path) {
Expand Down
37 changes: 26 additions & 11 deletions examples/claude-code-memory-plugin/scripts/shared/recall-core.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,23 +138,38 @@ export function buildContextSearchBody(cfg = {}, options = {}) {
return body;
}

// Server default for retrieval.recall_rewrite_timeout_s plus room for the
// retrieval that precedes it, still well inside the 60s prompt-hook budget.
const SERVER_REWRITE_REQUEST_TIMEOUT_MS = 35000;
// The server pipeline is serial and each optional stage has its own fuse. A
// request is aborted client-side unless its deadline covers every stage it
// asked for, and aborting discards the whole response rather than just the
// stage that ran long.
//
// session_id -> query expansion (retrieval.recall_intent_timeout_s, 5s)
// always -> retrieval, body reads, budget planning
// rewrite -> digest (retrieval.recall_rewrite_timeout_s, 30s)
//
// Both budgets stay inside the 60s prompt-hook allowance, the rewrite one with
// a quarter to spare.
const EXPANSION_REQUEST_TIMEOUT_MS = 15000;
const SERVER_REWRITE_REQUEST_TIMEOUT_MS = 45000;

/**
* HTTP deadline for one context request, or undefined to keep the caller's own.
*
* The ordinary request timeout is shorter than the server's rewrite fuse, so a
* digest that finishes inside its own fuse would be aborted client-side. That
* loses the whole response rather than just the digest, including the
* uncompressed block the server still returns when a rewrite fails.
* Derived from the request body, because the body is what states which server
* stages will run: reading `cfg` alone cannot tell a bare retrieval from one
* that also spends the expansion or rewrite fuse.
*/
export function contextRequestTimeoutMs(cfg = {}, serverRewrite = false) {
if (!serverRewrite) return undefined;
export function contextRequestTimeoutMs(cfg = {}, body = {}) {
const wantsRewrite = body.rewrite !== undefined;
// `query_expansion` defaults to "auto" server-side, so only an explicit "off"
// takes the expansion fuse back out of the budget.
const wantsExpansion = Boolean(body.session_id) && body.query_expansion !== "off";
if (!wantsRewrite && !wantsExpansion) return undefined;

const configured = Number(cfg.recallContextTimeoutMs);
if (Number.isFinite(configured) && configured > 0) return Math.max(1000, Math.floor(configured));
return Math.max(Number(cfg.timeoutMs) || 0, SERVER_REWRITE_REQUEST_TIMEOUT_MS);
const floor = wantsRewrite ? SERVER_REWRITE_REQUEST_TIMEOUT_MS : EXPANSION_REQUEST_TIMEOUT_MS;
return Math.max(Number(cfg.timeoutMs) || 0, floor);
}

/**
Expand Down Expand Up @@ -442,7 +457,7 @@ export async function fetchAssembledContext(fetchJSON, cfg, query, options = {})
const res = await fetchJSON("/api/v1/search/search", {
method: "POST",
body: JSON.stringify(body),
}, { actorPeerId, timeoutMs: contextRequestTimeoutMs(cfg, body.rewrite !== undefined) });
}, { actorPeerId, timeoutMs: contextRequestTimeoutMs(cfg, body) });

if (!res.ok) {
const status = res.status || 0;
Expand Down
14 changes: 9 additions & 5 deletions examples/codex-memory-plugin/scripts/shared/plugin-config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
* ovcli.conf carried connection fields only, so every harness had to keep its
* tuning knobs in ov.conf's harness section — a server-side file that a
* client-side plugin has no business editing. The `plugin` section fixes that:
* shared keys apply to every harness, and a per-harness object overrides them.
* shared keys apply to every harness that reads them, and a per-harness object
* overrides them.
*
* {
* "url": "...", "api_key": "...",
Expand All @@ -17,6 +18,13 @@
*
* Resolution stays env → ovcli.conf plugin.<harness> → ovcli.conf plugin →
* ov.conf harness section (legacy) → defaults.
*
* Consumers: Claude Code and Codex only. The other harnesses ship this module
* through `sync.mjs` but still read their knobs from the environment, so a
* `plugin` entry named after them is inert. `HARNESS_KEYS` lists what a harness
* loader actually consumes today — add a key here as its loader starts calling
* `loadPluginSettings`, not before, so the section never promises a knob that
* silently does nothing.
*/

import { readFileSync } from "node:fs";
Expand All @@ -28,10 +36,6 @@ const DEFAULT_OVCLI_CONF_PATH = join(homedir(), ".openviking", "ovcli.conf");
export const HARNESS_KEYS = {
claudeCode: "claude_code",
codex: "codex",
opencode: "opencode",
cursor: "cursor",
trae: "trae",
pi: "pi",
};

function tryLoadJson(path) {
Expand Down
Loading