@openviking/opencode-plugin 0.2.4 → 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": {
@@ -130,15 +131,29 @@ user/admin API key 的 API_KEY mode 时应留空。
130
131
 
131
132
  高级场景可以用 `OPENVIKING_PLUGIN_CONFIG` 指向其他配置文件路径。
132
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
+
133
147
  ## 验证
134
148
 
135
149
  修改插件或 OpenViking 配置后,需要重启 OpenCode。
136
150
 
137
151
  进入新的 OpenCode session 后,可以让 agent 浏览 OpenViking memory,或搜索一个已索引的资源。插件应暴露 OpenViking MCP server,OpenCode 中的工具名会带 `openviking_` 前缀:
138
152
 
139
- - `openviking_recall`、`openviking_search`、`openviking_find`
140
- - `openviking_read`、`openviking_list`、`openviking_grep`、`openviking_glob`
141
- - `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`
142
157
  - `openviking_list_watches`、`openviking_cancel_watch`
143
158
 
144
159
  如果行为异常,先查看运行时文件:
@@ -158,14 +173,16 @@ curl http://localhost:1933/health
158
173
 
159
174
  插件会通过 OpenCode config 注册 OpenViking stdio MCP proxy。服务端实际返回的 `tools/list` 是最终工具清单;当前 OpenViking server 暴露:
160
175
 
161
- - `openviking_recall`:面向当前任务的平衡召回
162
- - `openviking_search`:跨 memories/resources/skills 的深度语义检索
176
+ - `openviking_search`:跨 memories/resources/skills 的深度语义检索;使用 `mode="context"` 获取面向当前任务、可直接注入的平衡上下文
163
177
  - `openviking_find`:快速语义检索
164
178
  - `openviking_remember`:存储重要事实或决策,供记忆提取
165
179
  - `openviking_read`:读取一个或多个 `viking://` 文件
166
180
  - `openviking_list`:列出 `viking://` 目录
181
+ - `openviking_tree`:展示 `viking://` 目录树
167
182
  - `openviking_grep`:精确文本或正则搜索
168
183
  - `openviking_glob`:glob 文件匹配
184
+ - `openviking_write`:创建、覆盖或追加 `viking://` 文件
185
+ - `openviking_edit`:对 `viking://` 文件做精确字符串替换
169
186
  - `openviking_add_resource`:添加 URL、本地文件、sitemap 或 feed
170
187
  - `openviking_forget`:在用户明确确认后删除 `viking://` URI
171
188
  - `openviking_list_watches` / `openviking_cancel_watch`:查看或取消资源 watch
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": {
@@ -126,15 +127,30 @@ API keys are resolved from environment variables or `~/.openviking/ovcli.conf` a
126
127
 
127
128
  For advanced setups, use `OPENVIKING_PLUGIN_CONFIG` to point to another configuration file path.
128
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
+
129
144
  ## Verify
130
145
 
131
146
  Restart OpenCode after changing plugin or OpenViking configuration.
132
147
 
133
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_*`:
134
149
 
135
- - `openviking_recall`, `openviking_search`, `openviking_find`
136
- - `openviking_read`, `openviking_list`, `openviking_grep`, `openviking_glob`
137
- - `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`
138
154
  - `openviking_list_watches`, `openviking_cancel_watch`
139
155
 
140
156
  If anything looks wrong, check the runtime files:
@@ -154,14 +170,16 @@ curl http://localhost:1933/health
154
170
 
155
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:
156
172
 
157
- - `openviking_recall`: balanced current-task recall.
158
- - `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.
159
174
  - `openviking_find`: fast semantic retrieval.
160
175
  - `openviking_remember`: store important facts or decisions for memory extraction.
161
176
  - `openviking_read`: read one or more `viking://` files.
162
177
  - `openviking_list`: list a `viking://` directory.
178
+ - `openviking_tree`: show a `viking://` directory tree.
163
179
  - `openviking_grep`: exact text or regex search.
164
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.
165
183
  - `openviking_add_resource`: add a URL, local file, sitemap, or feed.
166
184
  - `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
167
185
  - `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
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": {
@@ -140,14 +145,7 @@ when exact category ceilings are required.
140
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
141
146
  headers sent as `X-OpenViking-Account` and `X-OpenViking-User`; leave them empty
142
147
  when using API-key mode with user/admin API keys.
143
- By default the plugin derives a peer from the project directory using Claude's
144
- project-directory naming rule: every non-letter-or-digit character becomes `-`,
145
- with no path normalization. For example, `/Users/x/Dev/OpenViking` becomes
146
- `-Users-x-Dev-OpenViking`. Data-plane memory/resource requests send the
147
- effective peer as `X-OpenViking-Actor-Peer`; captured session messages store it
148
- as body `peer_id`. Configure `peerId` or `OPENVIKING_PEER_ID` to override the
149
- workspace-derived peer, or set `workspacePeer=false` /
150
- `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.
151
149
 
152
150
  Recall defaults to the broad mode: global memory, the current workspace, and
153
151
  other workspace memories can all be recalled, with other workspaces penalized
@@ -159,10 +157,28 @@ with an explicit actor peer so one person's memories are not recalled into
159
157
  another person's session.
160
158
 
161
159
  `OPENVIKING_API_KEY`, `OPENVIKING_ACCOUNT`, `OPENVIKING_USER`,
162
- 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.
163
165
 
164
166
  For advanced setups, `OPENVIKING_PLUGIN_CONFIG` can point to another config file path.
165
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
+
166
182
  OpenCode's local `read`, `glob`, and `grep` tools cannot read `viking://` URIs.
167
183
  When the agent accidentally tries that, the plugin blocks the filesystem tool
168
184
  call and points it to the OpenViking MCP tools.
@@ -171,14 +187,16 @@ call and points it to the OpenViking MCP tools.
171
187
 
172
188
  OpenCode sees the OpenViking MCP server as `openviking`, so tool names are namespaced with `openviking_`.
173
189
 
174
- - `openviking_recall`: balanced current-task recall using OpenViking's `/recall` endpoint.
175
- - `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.
176
191
  - `openviking_find`: fast semantic retrieval.
177
192
  - `openviking_remember`: store important facts or decisions for memory extraction.
178
193
  - `openviking_read`: read one or more `viking://` files.
179
194
  - `openviking_list`: list a `viking://` directory.
195
+ - `openviking_tree`: show a `viking://` directory tree.
180
196
  - `openviking_grep`: exact text or regex search.
181
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.
182
200
  - `openviking_add_resource`: add a URL, local file, sitemap, or feed.
183
201
  - `openviking_forget`: delete a `viking://` URI after explicit user confirmation.
184
202
  - `openviking_list_watches` / `openviking_cancel_watch`: inspect or cancel resource watches.
@@ -194,3 +212,20 @@ The plugin writes runtime files to `~/.config/opencode/openviking/` by default:
194
212
  - `openviking-session-state.json`
195
213
 
196
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
@@ -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
@@ -21,6 +21,9 @@ const DEFAULT_CONFIG = {
21
21
  // it has to be switchable from the client that pays the latency.
22
22
  recallQueryExpansion: "auto",
23
23
  enabled: true,
24
+ mcp: {
25
+ enabled: true,
26
+ },
24
27
  timeoutMs: 30000,
25
28
  runtime: {
26
29
  dataDir: "",
@@ -42,7 +45,7 @@ const DEFAULT_CONFIG = {
42
45
  captureMode: "semantic",
43
46
  captureMaxLength: 24000,
44
47
  captureAssistantTurns: true,
45
- captureToolMaxChars: 2000,
48
+ captureToolMaxChars: 1000000,
46
49
  commitTokenThreshold: 20000,
47
50
  commitKeepRecentCount: 10,
48
51
  profileTokenBudget: 10000,
@@ -120,6 +123,12 @@ function applyLegacyConnection(config, fileConfig) {
120
123
 
121
124
  function applyBehaviorConfig(config, fileConfig = {}) {
122
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
+ }
123
132
  if (fileConfig.timeoutMs !== undefined) config.timeoutMs = fileConfig.timeoutMs
124
133
  config.runtime = {
125
134
  ...DEFAULT_CONFIG.runtime,
@@ -236,6 +245,7 @@ function normalizeConfig(config) {
236
245
  config.accountId = config.account
237
246
  config.userId = config.user
238
247
  config.userAgent = USER_AGENT
248
+ config.harness = "opencode"
239
249
  config.timeoutMs = normalizeNumber(config.timeoutMs, DEFAULT_CONFIG.timeoutMs, 1000, 300000)
240
250
  config.repoContext.cacheTtlMs = normalizeNumber(
241
251
  config.repoContext.cacheTtlMs,
@@ -252,7 +262,7 @@ function normalizeConfig(config) {
252
262
  config.recallPeerScope = config.recallPeerScope === "actor" ? "actor" : "all"
253
263
  config.recallQueryExpansion = config.recallQueryExpansion === "off" ? "off" : "auto"
254
264
  config.captureMaxLength = Math.max(200, Math.min(100000, Math.round(Number(config.captureMaxLength) || 24000)))
255
- 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)))
256
266
  config.commitTokenThreshold = Math.max(1000, Math.round(Number(config.commitTokenThreshold) || 20000))
257
267
  const rawCommitKeepRecentCount = config.commitKeepRecentCount
258
268
  const commitKeepRecentCount = rawCommitKeepRecentCount == null ||
@@ -285,7 +295,11 @@ export function loadConfig(pluginRoot, projectDirectory) {
285
295
  config.apiKey = creds.apiKey
286
296
  config.account = creds.account
287
297
  config.user = creds.user
288
- 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, "")
289
303
  config.mcpUrl = creds.mcpUrl
290
304
  config.credentialSource = creds.credentialSource
291
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
@@ -27,6 +27,7 @@ export function createMemoryRecall({ config, sessionManager }) {
27
27
  query,
28
28
  {
29
29
  actorPeerId: effectivePeerId(config),
30
+ legacyPeerId: config.effectivePeer?.legacyPeerId ?? "",
30
31
  // The mapped OV session is what turns on server-side query expansion
31
32
  // and the cross-turn dedup ledger.
32
33
  sessionId: sessionID ? sessionManager.getMappedSessionId(sessionID) : "",
@@ -57,8 +58,10 @@ export function extractCurrentUserText(parts) {
57
58
 
58
59
  function prependSyntheticRecallPart(input, output, injection) {
59
60
  const sessionID = input.sessionID ?? output.message?.sessionID
60
- const messageID = input.messageID ?? output.message?.id
61
- 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
62
65
 
63
66
  output.parts.unshift({
64
67
  id: `prt-ov-recall-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
@@ -30,6 +30,20 @@ export function createMemorySessionManager({ config, pluginRoot }) {
30
30
  const statePath = path.join(pluginRoot, "openviking-session-state.json")
31
31
  const oldSessionMapPath = path.join(pluginRoot, "openviking-session-map.json")
32
32
  let saveTimer = null
33
+ // Serialize saves: concurrent saveState() calls (a debounced save racing
34
+ // with flushAll / flushSession / session deletion) all share the same
35
+ // `${statePath}.tmp` temp file, so one rename can fail with ENOENT after
36
+ // another already moved it. Chaining through a promise queue keeps at most
37
+ // one write+rename in flight. Each queued save re-serializes the in-memory
38
+ // `sessions` map at execution time, so the last save always persists the
39
+ // latest state.
40
+ let saveChain = Promise.resolve()
41
+
42
+ function enqueueSave() {
43
+ const run = saveChain.then(() => saveState())
44
+ saveChain = run.catch(() => {})
45
+ return run
46
+ }
33
47
 
34
48
  async function init() {
35
49
  if (config.autoCapture) await migrateLegacySessionMap()
@@ -84,7 +98,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
84
98
  function debouncedSaveState() {
85
99
  if (saveTimer) clearTimeout(saveTimer)
86
100
  saveTimer = setTimeout(() => {
87
- saveState().catch((error) => {
101
+ enqueueSave().catch((error) => {
88
102
  log("ERROR", "persistence", "Debounced save failed", { error: error?.message })
89
103
  })
90
104
  }, 300)
@@ -176,7 +190,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
176
190
  if (!sessionId) return
177
191
  await flushSession(sessionId, { commit: true, reason: event.type })
178
192
  sessions.delete(sessionId)
179
- await saveState()
193
+ await enqueueSave()
180
194
  }
181
195
 
182
196
  async function handleSessionError(event) {
@@ -250,10 +264,20 @@ export function createMemorySessionManager({ config, pluginRoot }) {
250
264
  clearTimeout(saveTimer)
251
265
  saveTimer = null
252
266
  }
267
+ // Do not abort the whole dispose flush when one session fails (e.g. server
268
+ // already GC'd a stale session). Continue so later sessions still commit.
269
+ // See https://github.com/volcengine/OpenViking/issues/4490
253
270
  for (const sessionId of sessions.keys()) {
254
- await flushSession(sessionId, { commit, reason: "flushAll" })
271
+ try {
272
+ await flushSession(sessionId, { commit, reason: "flushAll" })
273
+ } catch (err) {
274
+ console.warn(
275
+ `[opencode-plugin] flushAll skipped session ${sessionId}:`,
276
+ err?.message || err,
277
+ )
278
+ }
255
279
  }
256
- await saveState()
280
+ await enqueueSave()
257
281
  }
258
282
 
259
283
  async function flushSession(opencodeSessionId, { commit = false, reason = "manual" } = {}) {
@@ -267,7 +291,7 @@ export function createMemorySessionManager({ config, pluginRoot }) {
267
291
  } else if (added > 0) {
268
292
  await maybeCommitByThreshold(state)
269
293
  }
270
- await saveState()
294
+ await enqueueSave()
271
295
  return true
272
296
  }
273
297
 
@@ -441,15 +465,35 @@ export function createMemorySessionManager({ config, pluginRoot }) {
441
465
  for (const state of sessions.values()) {
442
466
  if (state.ovSessionId === ovSessionId) state.lastCommitTime = Date.now()
443
467
  }
444
- log("INFO", "session", "Committed OpenViking session", { openviking_session: ovSessionId, reason })
445
- return { status: "accepted", result: res.result }
468
+ const traceId = res.traceId || res.result?.trace_id
469
+ log("INFO", "session", "Committed OpenViking session", {
470
+ openviking_session: ovSessionId,
471
+ reason,
472
+ trace_id: traceId,
473
+ })
474
+ return { status: "accepted", result: res.result, traceId }
446
475
  }
447
476
  if (isRetryableFailure(res)) {
448
477
  await enqueue("commitSession", ovSessionId, body)
449
- log("WARN", "session", "Queued OpenViking session commit", { openviking_session: ovSessionId, reason })
478
+ log("WARN", "session", "Queued OpenViking session commit", {
479
+ openviking_session: ovSessionId,
480
+ reason,
481
+ trace_id: res.traceId,
482
+ status: res.status,
483
+ })
450
484
  return { status: "queued" }
451
485
  }
452
- throw new Error(`Failed to commit OpenViking session ${ovSessionId}: ${res.error?.message || res.status}`)
486
+ log("ERROR", "session", "Failed to commit OpenViking session", {
487
+ openviking_session: ovSessionId,
488
+ reason,
489
+ trace_id: res.traceId,
490
+ status: res.status,
491
+ error: res.error?.message || res.error?.code,
492
+ })
493
+ throw new Error(
494
+ `Failed to commit OpenViking session ${ovSessionId}: ${res.error?.message || res.status}` +
495
+ (res.traceId ? ` (trace_id=${res.traceId})` : ""),
496
+ )
453
497
  }
454
498
 
455
499
  async function migrateLegacySessionMap() {