@openviking/opencode-plugin 0.2.3 → 2026.9.9-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INSTALL-ZH.md CHANGED
@@ -92,6 +92,7 @@ export { OpenVikingPlugin, default } from "./openviking/index.mjs"
92
92
  ```json
93
93
  {
94
94
  "enabled": true,
95
+ "mcp": { "enabled": true },
95
96
  "timeoutMs": 30000,
96
97
  "repoContext": { "enabled": true, "cacheTtlMs": 60000 },
97
98
  "autoRecall": {
@@ -110,6 +111,9 @@ export { OpenVikingPlugin, default } from "./openviking/index.mjs"
110
111
  }
111
112
  ```
112
113
 
114
+ `autoRecall.limit` 是遗留的配额缩放输入,不是最终结果上限。显式设置为
115
+ 1 到 5 时,有效总配额仍为 6,因为六个 coding 分类会各保留一个检索槽位。
116
+
113
117
  推荐通过环境变量提供 API Key,而不是写入配置文件:
114
118
 
115
119
  ```bash
@@ -127,17 +131,30 @@ user/admin API key 的 API_KEY mode 时应留空。
127
131
 
128
132
  高级场景可以用 `OPENVIKING_PLUGIN_CONFIG` 指向其他配置文件路径。
129
133
 
134
+ ### 仅 Hooks 模式
135
+
136
+ 如果其他 MCP server 已经提供 OpenViking,可以关闭本插件附带的 MCP 注册,同时保留生命周期 hooks:
137
+
138
+ ```json
139
+ {
140
+ "mcp": { "enabled": false }
141
+ }
142
+ ```
143
+
144
+ repository context、自动 recall、消息 capture 和生命周期 commit 会继续工作,也不会添加或覆盖
145
+ OpenCode 的 `mcp.openviking` 配置。
146
+
130
147
  ## 验证
131
148
 
132
149
  修改插件或 OpenViking 配置后,需要重启 OpenCode。
133
150
 
134
151
  进入新的 OpenCode session 后,可以让 agent 浏览 OpenViking memory,或搜索一个已索引的资源。插件应暴露 OpenViking MCP server,OpenCode 中的工具名会带 `openviking_` 前缀:
135
152
 
136
- - `openviking_recall`、`openviking_search`、`openviking_find`
137
- - `openviking_read`、`openviking_list`、`openviking_grep`、`openviking_glob`
138
- - `openviking_remember`、`openviking_add_resource`、`openviking_forget`、`openviking_health`
153
+ - `openviking_search`、`openviking_find`
154
+ - `openviking_read`、`openviking_list`、`openviking_tree`、`openviking_grep`、`openviking_glob`
155
+ - `openviking_remember`、`openviking_write`、`openviking_edit`、`openviking_add_resource`
156
+ - `openviking_list_watches`、`openviking_cancel_watch`、`openviking_forget`、`openviking_health`
139
157
  - `openviking_list_watches`、`openviking_cancel_watch`
140
- - `openviking_code_search`、`openviking_code_outline`、`openviking_code_expand`
141
158
 
142
159
  如果行为异常,先查看运行时文件:
143
160
 
@@ -156,18 +173,19 @@ curl http://localhost:1933/health
156
173
 
157
174
  插件会通过 OpenCode config 注册 OpenViking stdio MCP proxy。服务端实际返回的 `tools/list` 是最终工具清单;当前 OpenViking server 暴露:
158
175
 
159
- - `openviking_recall`:面向当前任务的平衡召回
160
- - `openviking_search`:跨 memories/resources/skills 的深度语义检索
176
+ - `openviking_search`:跨 memories/resources/skills 的深度语义检索;使用 `mode="context"` 获取面向当前任务、可直接注入的平衡上下文
161
177
  - `openviking_find`:快速语义检索
162
178
  - `openviking_remember`:存储重要事实或决策,供记忆提取
163
179
  - `openviking_read`:读取一个或多个 `viking://` 文件
164
180
  - `openviking_list`:列出 `viking://` 目录
181
+ - `openviking_tree`:展示 `viking://` 目录树
165
182
  - `openviking_grep`:精确文本或正则搜索
166
183
  - `openviking_glob`:glob 文件匹配
184
+ - `openviking_write`:创建、覆盖或追加 `viking://` 文件
185
+ - `openviking_edit`:对 `viking://` 文件做精确字符串替换
167
186
  - `openviking_add_resource`:添加 URL、本地文件、sitemap 或 feed
168
187
  - `openviking_forget`:在用户明确确认后删除 `viking://` URI
169
188
  - `openviking_list_watches` / `openviking_cancel_watch`:查看或取消资源 watch
170
- - `openviking_code_search`、`openviking_code_outline`、`openviking_code_expand`:查看已索引代码符号
171
189
  - `openviking_health`:检查 OpenViking server 健康状态
172
190
 
173
191
  使用建议:
package/INSTALL.md CHANGED
@@ -92,6 +92,7 @@ Example configuration:
92
92
  ```json
93
93
  {
94
94
  "enabled": true,
95
+ "mcp": { "enabled": true },
95
96
  "timeoutMs": 30000,
96
97
  "repoContext": { "enabled": true, "cacheTtlMs": 60000 },
97
98
  "autoRecall": {
@@ -110,6 +111,10 @@ Example configuration:
110
111
  }
111
112
  ```
112
113
 
114
+ `autoRecall.limit` is a legacy quota-scaling input, not a final result cap.
115
+ Explicit values from 1 through 5 produce an effective total quota of 6 because
116
+ each coding category keeps one retrieval slot.
117
+
113
118
  It is recommended to provide the API key through an environment variable instead of writing it into the configuration file:
114
119
 
115
120
  ```bash
@@ -122,17 +127,31 @@ API keys are resolved from environment variables or `~/.openviking/ovcli.conf` a
122
127
 
123
128
  For advanced setups, use `OPENVIKING_PLUGIN_CONFIG` to point to another configuration file path.
124
129
 
130
+ ### Hook-only mode
131
+
132
+ If another MCP server already exposes OpenViking, set the bundled MCP registration to `false` while
133
+ keeping this plugin's lifecycle hooks active:
134
+
135
+ ```json
136
+ {
137
+ "mcp": { "enabled": false }
138
+ }
139
+ ```
140
+
141
+ Repository context, automatic recall, message capture, and lifecycle commits remain enabled. This
142
+ does not add or overwrite OpenCode's `mcp.openviking` entry.
143
+
125
144
  ## Verify
126
145
 
127
146
  Restart OpenCode after changing plugin or OpenViking configuration.
128
147
 
129
148
  In a new OpenCode session, ask the agent to browse OpenViking memory or search for a known indexed resource. The plugin should expose the OpenViking MCP server, with tools namespaced by OpenCode as `openviking_*`:
130
149
 
131
- - `openviking_recall`, `openviking_search`, `openviking_find`
132
- - `openviking_read`, `openviking_list`, `openviking_grep`, `openviking_glob`
133
- - `openviking_remember`, `openviking_add_resource`, `openviking_forget`, `openviking_health`
150
+ - `openviking_search`, `openviking_find`
151
+ - `openviking_read`, `openviking_list`, `openviking_tree`, `openviking_grep`, `openviking_glob`
152
+ - `openviking_remember`, `openviking_write`, `openviking_edit`, `openviking_add_resource`
153
+ - `openviking_list_watches`, `openviking_cancel_watch`, `openviking_forget`, `openviking_health`
134
154
  - `openviking_list_watches`, `openviking_cancel_watch`
135
- - `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`
136
155
 
137
156
  If anything looks wrong, check the runtime files:
138
157
 
@@ -151,18 +170,19 @@ curl http://localhost:1933/health
151
170
 
152
171
  The plugin registers OpenViking's stdio MCP proxy through OpenCode config. The server's real `tools/list` response is the source of truth; current OpenViking servers expose:
153
172
 
154
- - `openviking_recall`: balanced current-task recall.
155
- - `openviking_search`: deep semantic retrieval across memories, resources, and skills.
173
+ - `openviking_search`: deep semantic retrieval across memories, resources, and skills; use `mode="context"` for balanced, injection-ready context.
156
174
  - `openviking_find`: fast semantic retrieval.
157
175
  - `openviking_remember`: store important facts or decisions for memory extraction.
158
176
  - `openviking_read`: read one or more `viking://` files.
159
177
  - `openviking_list`: list a `viking://` directory.
178
+ - `openviking_tree`: show a `viking://` directory tree.
160
179
  - `openviking_grep`: exact text or regex search.
161
180
  - `openviking_glob`: glob file matching.
181
+ - `openviking_write`: create, overwrite, or append to a `viking://` file.
182
+ - `openviking_edit`: exact string replacement in a `viking://` file.
162
183
  - `openviking_add_resource`: add a URL, local file, sitemap, or feed.
163
184
  - `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
164
185
  - `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
165
- - `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`: inspect indexed code symbols.
166
186
  - `openviking_health`: check OpenViking server health.
167
187
 
168
188
  Usage guidance:
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A unified OpenCode plugin for OpenViking repository retrieval and long-term memory.
4
4
 
5
+ > **Requires an OpenViking server with `viking://~` home-alias support.** Recall targets the
6
+ > caller's own context space through `viking://~/memories` and `viking://~/skills`; the uid-less
7
+ > `viking://user/memories` shorthand is rejected by newer servers.
8
+
5
9
  This is the only OpenCode plugin example maintained in this repository. It supersedes the former split examples for indexed repository prompt injection and long-term memory.
6
10
 
7
11
  The plugin uses OpenCode hooks for lifecycle behavior and registers OpenViking's standard stdio MCP proxy for model tools. It does not install or require an OpenCode skill, and agents do not need to run `ov` shell commands.
@@ -114,6 +118,7 @@ Create `~/.config/opencode/openviking-config.json`:
114
118
  ```json
115
119
  {
116
120
  "enabled": true,
121
+ "mcp": { "enabled": true },
117
122
  "timeoutMs": 30000,
118
123
  "repoContext": { "enabled": true, "cacheTtlMs": 60000 },
119
124
  "autoRecall": {
@@ -132,17 +137,15 @@ Create `~/.config/opencode/openviking-config.json`:
132
137
  }
133
138
  ```
134
139
 
135
- API keys are resolved from environment variables or `~/.openviking/ovcli.conf` and sent as `Authorization: Bearer ...` by both hooks and the MCP proxy. `account` and `user` are trusted-mode identity
140
+ `autoRecall.limit` is a legacy quota-scaling input, not a final result cap.
141
+ Explicit values from 1 through 5 produce an effective total quota of 6 because
142
+ each coding category keeps one retrieval slot. Use Context `quotas` directly
143
+ when exact category ceilings are required.
144
+
145
+ API keys are resolved from environment variables or `~/.openviking/ovcli.conf` and sent as `Authorization: Bearer ...` by both hooks and the MCP proxy. Recall goes through the server-side context face (`POST /api/v1/search/search` with `mode="context"`), falling back to the deprecated `/api/v1/search/recall` on older deployments. `account` and `user` are trusted-mode identity
136
146
  headers sent as `X-OpenViking-Account` and `X-OpenViking-User`; leave them empty
137
147
  when using API-key mode with user/admin API keys.
138
- By default the plugin derives a peer from the project directory using Claude's
139
- project-directory naming rule: every non-letter-or-digit character becomes `-`,
140
- with no path normalization. For example, `/Users/x/Dev/OpenViking` becomes
141
- `-Users-x-Dev-OpenViking`. Data-plane memory/resource requests send the
142
- effective peer as `X-OpenViking-Actor-Peer`; captured session messages store it
143
- as body `peer_id`. Configure `peerId` or `OPENVIKING_PEER_ID` to override the
144
- workspace-derived peer, or set `workspacePeer=false` /
145
- `OPENVIKING_WORKSPACE_PEER=0` to turn workspace-derived peers off.
148
+ By default the plugin derives a peer from the git identity of the project directory: the normalized `origin` URL, else the repository root path. Outside a git repository no peer is sent at all, and what is remembered there goes to the user-level space `viking://user/<you>/memories`. `git@github.com:volcengine/OpenViking.git` becomes `github.com-volcengine-openviking`; the path fallback keeps the older naming rule where every non-letter-or-digit character becomes `-`, so `/Users/x/Dev/OpenViking` becomes `-Users-x-Dev-OpenViking`. One repository therefore keeps one peer across subdirectories, worktrees, clones and machines, while a fork's different origin keeps it separate. Derivation reads `.git` directly, so no `git` binary is needed. The plugin does not read workspace `.openviking/config.json` files, so a `peer.id` written there has no effect. Data-plane memory/resource requests send the effective peer as `X-OpenViking-Actor-Peer`; captured session messages store it as body `peer_id`. Configure `peerId` or `OPENVIKING_PEER_ID` to override the derived peer, or set `workspacePeer=false` / `OPENVIKING_WORKSPACE_PEER=0` to send no peer at all. Memories written under the older directory-derived peer stay reachable: the default broad recall sweeps every peer under the user, and `recallPeerScope="actor"` asks that previous peer separately.
146
149
 
147
150
  Recall defaults to the broad mode: global memory, the current workspace, and
148
151
  other workspace memories can all be recalled, with other workspaces penalized
@@ -154,10 +157,28 @@ with an explicit actor peer so one person's memories are not recalled into
154
157
  another person's session.
155
158
 
156
159
  `OPENVIKING_API_KEY`, `OPENVIKING_ACCOUNT`, `OPENVIKING_USER`,
157
- and `OPENVIKING_PEER_ID` take precedence over values in this file.
160
+ and `OPENVIKING_PEER_ID` take precedence over values in this file. The config
161
+ file's `peerId` still applies whenever shared credentials (ovcli.conf or
162
+ environment variables) do not carry a peer of their own, so an authenticated
163
+ setup keeps writing peer-scoped data instead of dropping into the shared user
164
+ tree.
158
165
 
159
166
  For advanced setups, `OPENVIKING_PLUGIN_CONFIG` can point to another config file path.
160
167
 
168
+ ### Hook-only mode
169
+
170
+ When OpenViking is already exposed through another MCP server, retain the lifecycle hooks while
171
+ skipping this plugin's bundled MCP registration:
172
+
173
+ ```json
174
+ {
175
+ "mcp": { "enabled": false }
176
+ }
177
+ ```
178
+
179
+ This leaves repository context, automatic recall, message capture, and lifecycle commits enabled.
180
+ It does not add or overwrite OpenCode's `mcp.openviking` entry.
181
+
161
182
  OpenCode's local `read`, `glob`, and `grep` tools cannot read `viking://` URIs.
162
183
  When the agent accidentally tries that, the plugin blocks the filesystem tool
163
184
  call and points it to the OpenViking MCP tools.
@@ -166,18 +187,19 @@ call and points it to the OpenViking MCP tools.
166
187
 
167
188
  OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
168
189
 
169
- - `openviking_recall`: balanced current-task recall using OpenViking's `/recall` endpoint.
170
- - `openviking_search`: deep semantic retrieval across memories, resources, and skills.
190
+ - `openviking_search`: deep semantic retrieval across memories, resources, and skills; use `mode="context"` for balanced, injection-ready context.
171
191
  - `openviking_find`: fast semantic retrieval.
172
192
  - `openviking_remember`: store important facts or decisions for memory extraction.
173
193
  - `openviking_read`: read one or more `viking://` files.
174
194
  - `openviking_list`: list a `viking://` directory.
195
+ - `openviking_tree`: show a `viking://` directory tree.
175
196
  - `openviking_grep`: exact text or regex search.
176
197
  - `openviking_glob`: glob file matching.
198
+ - `openviking_write`: create, overwrite, or append to a `viking://` file.
199
+ - `openviking_edit`: exact string replacement in a `viking://` file.
177
200
  - `openviking_add_resource`: add a URL, local file, sitemap, or feed.
178
201
  - `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
179
202
  - `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
180
- - `openviking_code_search`, `openviking_code_outline`, `openviking_code_expand`: inspect indexed code symbols.
181
203
  - `openviking_health`: check OpenViking server health.
182
204
 
183
205
  The proxy forwards the server's real `tools/list` response; the plugin does not maintain a separate native tool list.
@@ -190,3 +212,20 @@ The plugin writes runtime files to `~/.config/opencode/openviking/` by default:
190
212
  - `openviking-session-state.json`
191
213
 
192
214
  Set `runtime.dataDir` in config to override this directory.
215
+
216
+ ## Automated npm releases
217
+
218
+ Changes merged into upstream `main` under `examples/opencode-plugin/` trigger
219
+ `.github/workflows/opencode-plugin-release.yml`. Releases use the same calendar
220
+ version convention as the OpenClaw plugin: `YYYY.M.D`, then `YYYY.M.D-N` for
221
+ subsequent releases on that date (Asia/Shanghai). No manual source version bump
222
+ is required. The workflow changes the version only in the package being published;
223
+ it does not commit generated version changes back to the repository.
224
+
225
+ Releases are serialized. A queued run checks out current `main`, so several quick
226
+ merges may be included in one package. The npm manifest records
227
+ `openvikingSourceCommit`; rerunning a successfully published commit skips publication.
228
+ Registry failures stop the workflow instead of treating an unavailable registry as
229
+ an unused version. Only upstream `main` can publish the official npm `latest` tag.
230
+ Manual dispatch on `main` retries a failed release using the existing npm credentials
231
+ or Trusted Publishing configuration.
package/index.mjs CHANGED
@@ -24,7 +24,7 @@ export async function OpenVikingPlugin({ client, directory }) {
24
24
 
25
25
  const repoContext = createRepoContext({ config })
26
26
  const sessionManager = createMemorySessionManager({ config, pluginRoot: dataDir })
27
- const recall = createMemoryRecall({ config })
27
+ const recall = createMemoryRecall({ config, sessionManager })
28
28
  const sessionInject = createSessionInject({ config, sessionManager })
29
29
  const vikingUriGuard = createVikingUriGuard()
30
30
 
@@ -44,8 +44,15 @@ export async function OpenVikingPlugin({ client, directory }) {
44
44
 
45
45
  return {
46
46
  config: async (opencodeConfig) => {
47
- const injected = injectOpenVikingMcpConfig(opencodeConfig, pluginRoot)
48
- log(injected ? "INFO" : "WARN", "mcp", injected ? "Registered OpenViking MCP server" : "OpenViking MCP server was not registered")
47
+ const injected = injectOpenVikingMcpConfig(opencodeConfig, pluginRoot, config.mcp.enabled)
48
+ const hookOnly = !config.mcp.enabled
49
+ log(
50
+ injected || hookOnly ? "INFO" : "WARN",
51
+ "mcp",
52
+ injected ? "Registered OpenViking MCP server" :
53
+ hookOnly ? "Skipped bundled MCP registration in hook-only mode" :
54
+ "OpenViking MCP server was not registered",
55
+ )
49
56
  },
50
57
 
51
58
  event: async ({ event }) => {
package/lib/config.mjs CHANGED
@@ -1,9 +1,14 @@
1
1
  import fs from "fs"
2
2
  import path from "path"
3
3
  import { homedir } from "os"
4
- import { resolveOpenVikingCredentials } from "./shared/credentials.mjs"
4
+ import { buildUserAgent, readManifestVersion, resolveOpenVikingCredentials } from "./shared/credentials.mjs"
5
5
  import { resolveEffectivePeerId } from "./shared/workspace-peer.mjs"
6
6
 
7
+ const USER_AGENT = buildUserAgent(
8
+ "opencode",
9
+ readManifestVersion(new URL("../package.json", import.meta.url)),
10
+ )
11
+
7
12
  const DEFAULT_CONFIG = {
8
13
  endpoint: "http://127.0.0.1:1933",
9
14
  apiKey: "",
@@ -12,7 +17,13 @@ const DEFAULT_CONFIG = {
12
17
  peerId: "",
13
18
  workspacePeer: true,
14
19
  recallPeerScope: "all",
20
+ // Server-side query expansion costs a model call before retrieval starts, so
21
+ // it has to be switchable from the client that pays the latency.
22
+ recallQueryExpansion: "auto",
15
23
  enabled: true,
24
+ mcp: {
25
+ enabled: true,
26
+ },
16
27
  timeoutMs: 30000,
17
28
  runtime: {
18
29
  dataDir: "",
@@ -23,7 +34,7 @@ const DEFAULT_CONFIG = {
23
34
  },
24
35
  autoRecall: {
25
36
  enabled: true,
26
- limit: 6,
37
+ limit: 10,
27
38
  scoreThreshold: 0.35,
28
39
  maxContentChars: 500,
29
40
  preferAbstract: true,
@@ -34,7 +45,7 @@ const DEFAULT_CONFIG = {
34
45
  captureMode: "semantic",
35
46
  captureMaxLength: 24000,
36
47
  captureAssistantTurns: true,
37
- captureToolMaxChars: 2000,
48
+ captureToolMaxChars: 1000000,
38
49
  commitTokenThreshold: 20000,
39
50
  commitKeepRecentCount: 10,
40
51
  profileTokenBudget: 10000,
@@ -112,6 +123,12 @@ function applyLegacyConnection(config, fileConfig) {
112
123
 
113
124
  function applyBehaviorConfig(config, fileConfig = {}) {
114
125
  if (fileConfig.enabled !== undefined) config.enabled = fileConfig.enabled !== false
126
+ const mcp = fileConfig.mcp && typeof fileConfig.mcp === "object" ? fileConfig.mcp : {}
127
+ config.mcp = {
128
+ ...DEFAULT_CONFIG.mcp,
129
+ ...mcp,
130
+ enabled: mcp.enabled !== false,
131
+ }
115
132
  if (fileConfig.timeoutMs !== undefined) config.timeoutMs = fileConfig.timeoutMs
116
133
  config.runtime = {
117
134
  ...DEFAULT_CONFIG.runtime,
@@ -123,6 +140,8 @@ function applyBehaviorConfig(config, fileConfig = {}) {
123
140
  }
124
141
 
125
142
  const autoRecall = fileConfig.autoRecall ?? {}
143
+ config.recallLimitConfigured = autoRecall.limit !== undefined ||
144
+ fileConfig.recallLimit !== undefined
126
145
  config.autoRecall = {
127
146
  ...DEFAULT_CONFIG.autoRecall,
128
147
  ...autoRecall,
@@ -152,9 +171,11 @@ function applyBehaviorConfig(config, fileConfig = {}) {
152
171
  "debugLogPath",
153
172
  "workspacePeer",
154
173
  "recallPeerScope",
174
+ "recallQueryExpansion",
155
175
  ]) {
156
176
  if (fileConfig[key] !== undefined) config[key] = fileConfig[key]
157
177
  }
178
+ config.recallQueryExpansionConfigured = fileConfig.recallQueryExpansion !== undefined
158
179
  }
159
180
 
160
181
  function applyEnv(config) {
@@ -162,7 +183,10 @@ function applyEnv(config) {
162
183
  if (process.env.OPENVIKING_AUTO_RECALL !== undefined) {
163
184
  config.autoRecall.enabled = envBool("OPENVIKING_AUTO_RECALL") ?? config.autoRecall.enabled
164
185
  }
165
- if (process.env.OPENVIKING_RECALL_LIMIT) config.autoRecall.limit = process.env.OPENVIKING_RECALL_LIMIT
186
+ if (process.env.OPENVIKING_RECALL_LIMIT) {
187
+ config.autoRecall.limit = process.env.OPENVIKING_RECALL_LIMIT
188
+ config.recallLimitConfigured = true
189
+ }
166
190
  if (process.env.OPENVIKING_SCORE_THRESHOLD) config.autoRecall.scoreThreshold = process.env.OPENVIKING_SCORE_THRESHOLD
167
191
  if (process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS) {
168
192
  config.autoRecall.maxContentChars = process.env.OPENVIKING_RECALL_MAX_CONTENT_CHARS
@@ -172,6 +196,10 @@ function applyEnv(config) {
172
196
  config.autoRecall.preferAbstract = envBool("OPENVIKING_RECALL_PREFER_ABSTRACT") ?? config.autoRecall.preferAbstract
173
197
  }
174
198
  if (process.env.OPENVIKING_RECALL_PEER_SCOPE) config.recallPeerScope = process.env.OPENVIKING_RECALL_PEER_SCOPE
199
+ if (process.env.OPENVIKING_RECALL_QUERY_EXPANSION) {
200
+ config.recallQueryExpansion = process.env.OPENVIKING_RECALL_QUERY_EXPANSION
201
+ config.recallQueryExpansionConfigured = true
202
+ }
175
203
  if (process.env.OPENVIKING_WORKSPACE_PEER !== undefined) {
176
204
  config.workspacePeer = envBool("OPENVIKING_WORKSPACE_PEER") ?? config.workspacePeer
177
205
  }
@@ -216,6 +244,8 @@ function normalizeConfig(config) {
216
244
  config.baseUrl = config.endpoint
217
245
  config.accountId = config.account
218
246
  config.userId = config.user
247
+ config.userAgent = USER_AGENT
248
+ config.harness = "opencode"
219
249
  config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
220
250
  config.repoContext.cacheTtlMs = normalizeNumber(
221
251
  config.repoContext.cacheTtlMs,
@@ -223,17 +253,25 @@ function normalizeConfig(config) {
223
253
  1000,
224
254
  60 * 60 * 1000,
225
255
  )
226
- config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) || 6)))
256
+ config.autoRecall.limit = Math.max(1, Math.min(50, Math.round(Number(config.autoRecall.limit) || 10)))
227
257
  config.autoRecall.scoreThreshold = Math.max(0, Math.min(1, Number(config.autoRecall.scoreThreshold) || 0))
228
258
  config.autoRecall.maxContentChars = Math.max(100, Math.min(5000, Math.round(Number(config.autoRecall.maxContentChars) || 500)))
229
259
  config.autoRecall.tokenBudget = Math.max(200, Math.min(50000, Math.round(Number(config.autoRecall.tokenBudget) || 2000)))
230
260
  config.autoRecall.minQueryLength = Math.max(1, Math.min(64, Math.round(Number(config.autoRecall.minQueryLength) || 3)))
231
261
  config.captureMode = config.captureMode === "keyword" ? "keyword" : "semantic"
232
262
  config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
263
+ config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto"
233
264
  config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
234
- config.captureToolMaxChars = Math.max(200, Math.min(20000, Math.round(Number(config.captureToolMaxChars) || 2000)))
265
+ config.captureToolMaxChars = Math.max(200, Math.min(1000000, Math.round(Number(config.captureToolMaxChars) || 1000000)))
235
266
  config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
236
- config.commitKeepRecentCount = Math.max(0, Math.round(Number(config.commitKeepRecentCount) || 10))
267
+ const rawCommitKeepRecentCount = config.commitKeepRecentCount
268
+ const commitKeepRecentCount = rawCommitKeepRecentCount == null ||
269
+ (typeof rawCommitKeepRecentCount === "string" && rawCommitKeepRecentCount.trim() === "")
270
+ ? Number.NaN
271
+ : Number(rawCommitKeepRecentCount)
272
+ config.commitKeepRecentCount = Number.isFinite(commitKeepRecentCount)
273
+ ? Math.max(0, Math.round(commitKeepRecentCount))
274
+ : DEFAULT_CONFIG.commitKeepRecentCount
237
275
  config.profileTokenBudget = Math.max(500, Math.round(Number(config.profileTokenBudget) || 10000))
238
276
  config.resumeContextBudget = Math.max(1024, Math.round(Number(config.resumeContextBudget) || 32000))
239
277
  if (!Array.isArray(config.bypassSessionPatterns)) config.bypassSessionPatterns = []
@@ -257,7 +295,11 @@ export function loadConfig(pluginRoot, projectDirectory) {
257
295
  config.apiKey = creds.apiKey
258
296
  config.account = creds.account
259
297
  config.user = creds.user
260
- config.peerId = creds.peerId
298
+ // Shared credentials only carry a peer when ovcli.conf sets actor_peer_id
299
+ // (or OPENVIKING_PEER_ID is exported); without one the project config's
300
+ // peerId still applies so authenticated setups keep writing peer-scoped
301
+ // data instead of silently dropping into the shared user tree (#4487).
302
+ config.peerId = creds.peerId || str(fileConfig.peerId, "")
261
303
  config.mcpUrl = creds.mcpUrl
262
304
  config.credentialSource = creds.credentialSource
263
305
  config.credentialPath = creds.cliPath || creds.ovPath || ""
@@ -11,8 +11,9 @@ export function createOpenVikingMcpConfig(pluginRoot) {
11
11
  }
12
12
  }
13
13
 
14
- export function injectOpenVikingMcpConfig(config, pluginRoot) {
14
+ export function injectOpenVikingMcpConfig(config, pluginRoot, enabled = true) {
15
15
  if (!config || typeof config !== "object") return false
16
+ if (!enabled) return false
16
17
  config.mcp = config.mcp && typeof config.mcp === "object" ? config.mcp : {}
17
18
  const current = config.mcp[OPENCODE_MCP_NAME]
18
19
  if (current?.enabled === false) return false
@@ -2,14 +2,15 @@ import { buildRecallBlock } from "./shared/recall-core.mjs"
2
2
  import { isBypassed } from "./shared/session-model.mjs"
3
3
  import { effectivePeerId, fetchJSON, log } from "./utils.mjs"
4
4
 
5
- export function createMemoryRecall({ config }) {
5
+ export function createMemoryRecall({ config, sessionManager }) {
6
6
  async function injectRelevantMemories(input, output) {
7
7
  if (!config.autoRecall?.enabled) return
8
8
  const query = extractCurrentUserText(output.parts ?? [])
9
9
  if (!query) return
10
10
  if (query.length < config.minQueryLength) return
11
+ const sessionID = input.sessionID ?? output.message?.sessionID
11
12
  if (isBypassed(config, {
12
- sessionId: input.sessionID ?? output.message?.sessionID,
13
+ sessionId: sessionID,
13
14
  cwd: input.directory ?? input.cwd,
14
15
  })) return
15
16
 
@@ -17,11 +18,19 @@ export function createMemoryRecall({ config }) {
17
18
  if (!health.ok) return
18
19
 
19
20
  const block = await buildRecallBlock(
20
- (path, init = {}, options = {}) => fetchJSON(config, path, init, { ...options, timeoutMs: 5000 }),
21
+ // 5s is this hook's own budget for a bare retrieval; when the request
22
+ // also spends a server fuse the helper hands down a longer deadline, and
23
+ // overriding it here would abort a request the server was still inside.
24
+ (path, init = {}, options = {}) =>
25
+ fetchJSON(config, path, init, { ...options, timeoutMs: options.timeoutMs ?? 5000 }),
21
26
  config,
22
27
  query,
23
28
  {
24
29
  actorPeerId: effectivePeerId(config),
30
+ legacyPeerId: config.effectivePeer?.legacyPeerId ?? "",
31
+ // The mapped OV session is what turns on server-side query expansion
32
+ // and the cross-turn dedup ledger.
33
+ sessionId: sessionID ? sessionManager.getMappedSessionId(sessionID) : "",
25
34
  log: (stage, data) => log("DEBUG", "recall", stage, data),
26
35
  },
27
36
  )
@@ -35,12 +44,13 @@ export function createMemoryRecall({ config }) {
35
44
  return { injectRelevantMemories }
36
45
  }
37
46
 
38
- function extractCurrentUserText(parts) {
47
+ export function extractCurrentUserText(parts) {
39
48
  const texts = []
40
49
  for (const part of parts) {
41
50
  if (part.type !== "text" || typeof part.text !== "string") continue
51
+ if (part.synthetic || part.ignored) continue
42
52
  if (part.text.includes("<openviking-context")) return null
43
- if (!part.synthetic && !part.ignored) texts.push(part.text)
53
+ texts.push(part.text)
44
54
  }
45
55
  const joined = texts.join(" ").trim()
46
56
  return joined || null
@@ -48,8 +58,10 @@ function extractCurrentUserText(parts) {
48
58
 
49
59
  function prependSyntheticRecallPart(input, output, injection) {
50
60
  const sessionID = input.sessionID ?? output.message?.sessionID
51
- const messageID = input.messageID ?? output.message?.id
52
- if (!sessionID || !messageID) return false
61
+ // messageID is optional in opencode's chat.message API (messageID?: string).
62
+ // When absent, generate a fallback so recall context is always injected.
63
+ const messageID = input.messageID ?? output.message?.id ?? `ov-recall-${Date.now()}`
64
+ if (!sessionID) return false
53
65
 
54
66
  output.parts.unshift({
55
67
  id: `prt-ov-recall-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,