mirror of
https://github.com/volcengine/OpenViking.git
synced 2026-09-29 04:02:57 +08:00
python-sdk@0.1.12
644
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f316f27569 |
fix(storage): 用文件锁替代本地存储 PID 检查 (#5159)
避免残留 PID 和并发启动导致误判,仅保护 local/cuvs 后端;远程向量后端自动跳过,保留显式跳过开关并删除旧 PID 兼容逻辑。 |
||
|
|
80c59f13eb |
feat(ingest): add a MiMo / MiMoCode log source (#5130)
Co-authored-by: zhangzhangco <zhangzhangco@example.com> |
||
|
|
0ec8d25996 |
fix(plugins): resolve one connection for every plugin's hooks and MCP proxy (#5132)
* fix(codex): read the MCP proxy's connection from the hooks' loadConfig
The proxy resolved url, api_key, account and user through the bare
credential chain while taking everything else from loadConfig(). Since
the loader became buildPluginConfig() it adds layers that chain never
sees: ovcli.conf's plugin.codex apiKey/accountId/userId, and ov.conf's
codex.apiKey when ovcli.conf names only the server. With either, hooks
authenticated and every MCP tool call went out without a key.
Codex also hands a stdio MCP server only the env vars .mcp.json lists,
and OPENVIKING_AUTH_MODE was not one of them, so an env-set auth mode
decided the identity headers for hooks but not for MCP calls.
* fix(dsh): forward the resolved auth mode and timeout to the MCP proxy
The proxy runs as a child whose env DSH scrubs, so the parent forwards
what it resolved. It forwarded the endpoint, key, account, user and
peer but not the auth mode or request timeout, so a Cordis patch that
set either configured the in-process runtime and not the MCP calls.
The proxy now also takes its credential source and watched paths from
the resolved config instead of a second credential-chain call, and a
shared test keeps every proxy that ships beside hooks off that chain.
* refactor(shared): one connection resolver for hooks and the MCP proxy
The credential chain lived in two layers. `resolveOpenVikingCredentials()`
could not read ovcli.conf's `plugin.<harness>` keys, the ov.conf harness
fallback or the root-key tail; `buildPluginConfig()` patched those in, and
any caller that used the lower layer alone resolved a different key and
identity than the hooks did.
`resolveConnection(harness, { env, files, hostInput, rootKeyFallback })`
now answers server, key, identity and auth mode in one place, reading only
host input, the environment and the two ~/.openviking files. The hook
loader and `buildProxyConnection()` both consume it, and the old
two-layer entry points (`resolveOpenVikingCredentials`, `resolveAuthMode`,
the credentials.mjs CLI) are gone so a half-resolved chain cannot be
written again.
Behaviour is unchanged for every hook harness (checked field by field
against the previous implementation over thousands of generated file/env
combinations). Two deliberate additions: the portable agent-plugins proxy
now honours ovcli.conf's `plugin` connection keys and ov.conf's harness
section like every other harness, and dsh hands the host's `authMode`
(or `auth_mode`) over as host input, ranking it with the host's endpoint,
key and identity.
* fix(shared): a forced env credential source reads only the environment
`OPENVIKING_CREDENTIAL_SOURCE=env` is documented as "env vars only", but
only the url honoured it: the key, account, user, ovcli.conf's actor peer
and the auth mode still fell through to ovcli.conf, its plugin keys,
ov.conf and the root key when the variable was unset. A process that
exported an empty key to mean "no key" was silently handed whatever the
files held.
Forced to `env`, the connection now reads no file and an unset variable
stays empty; the url defaults to http://127.0.0.1:1933. The `peerId`
setting keeps its own layers. The doctor labels that mode instead of
pointing at files the chain skipped.
* refactor(shared): one proxy-config mapper and one forwarded-env list
Each proxy entrypoint copied a dozen fields out of its loader by hand,
under two sets of names, and the copies had drifted. What the proxy
process must be handed was a second hand-kept list, in Codex's
`.mcp.json` and in its test.
`toMcpProxyConfig(cfg, options)` maps a resolved loader or proxy
connection to the proxy config once. `MCP_PROXY_ENV_VARS` names every
variable that changes what a proxy sends; Codex's `env_vars` is now
checked against it, which adds the missing `OPENVIKING_STATE_DIR`.
* refactor(plugins): every loader takes an env, every proxy exports readProxyConfig(env)
The six MCP proxy entrypoints now reduce to one line: resolve through the
harness's own loader (or `buildProxyConnection` for the hook-less package)
and hand the result to `toMcpProxyConfig`. Every one exports
`readProxyConfig(env)`, and the codex, claude-code, opencode and agent-hook
loaders accept an injected env, so a test can drive a hook and its proxy
from the same inputs without touching process.env.
Mapping through one function fixes what the hand copies had lost: the
Claude Code and DSH proxies never passed `mcpUrl`, so
`OPENVIKING_MCP_URL` moved the hooks and left the tools behind.
The source guard now requires the shared mapper and the exported reader
in every proxy, and `buildProxyConnection` reports its two config paths
instead of a watch list of its own.
* fix(dsh): forward the resolved connection to the MCP proxy
DSH starts its MCP subprocess with the parent's environment minus
credential-shaped names (`/KEY|PASSWORD|SECRET|TOKEN/i`), so the bundle
forwards what it resolved. It forwarded the values but not the mode, and
only the non-empty ones:
- A child that receives `OPENVIKING_URL` runs the chain unpinned. Where
the parent's chain was pinned to an ovcli.conf that names only a url,
the parent sent no key while the proxy fell through to ov.conf's
`server.root_api_key`, so the tools reached the server as root while
the hooks were anonymous.
- `OPENVIKING_ACCOUNT`, `OPENVIKING_USER` and `OPENVIKING_PEER_ID` survive
DSH's scrub, so a value the parent's chain ignored filled the gap in
the child and went out as an identity header.
- With no peer to forward, the proxy derived one from its own launch
directory and sent an actor peer the runtime did not.
`forwardConnectionEnv(connection)` now writes every credential variable,
the empty ones too, with the forced `env` source, so the child reads no
file and resolves exactly the parent's url, MCP url, key, identity, auth
mode and peer. The proxy takes its peer from that environment only.
`buildMcpConfig` moves to `mcp-env.mjs`, which carries no host
dependency, so shared tests can build the child environment without the
DSH bridge.
* test(shared): prove the proxy and the hooks resolve one connection
The existing guards checked shape — that a proxy called the shared
builder — never that it reached the server as the same caller its hooks
did, which is how two harnesses shipped proxies that disagreed with them.
`mcp-hook-parity.test.mjs` runs every harness that ships a proxy beside
hooks through a dozen configurations: ovcli.conf's own fields, its
`plugin.<harness>` and shared plugin keys, ov.conf-only installs, the
pinned fallbacks to a harness key and to the root key, credential and
auth-mode variables, a forced source over stale variables, an explicit
MCP URL, a host's own input, and a workspace file that tries to move the
connection. The hook loader sees the full environment; the proxy sees
only what its host lets through — Codex's `env_vars`, DSH's scrubbed
inheritance plus the forwarded connection, everyone else's full
environment — and the url, key, identity and identity-header switch they
put on the wire must match. Scenarios with a known answer pin it too, and
a coverage check fails when a new proxy or hook client has no row.
The two codex-only proxy tests the matrix now covers are removed.
* docs(plugins): one connection for hooks and MCP, and version bumps
The capability reference, plugin development guide, Agent Plugins and
Codex pages (en/zh), both doctor references and the plugin READMEs now
describe the chain `resolveConnection()` runs: host input first, the
pinned ovcli.conf branch and what still falls through it, the auth mode
reading `OPENVIKING_AUTH_MODE` and the `plugin` keys in every mode, a
forced `env` source reading no file, and the two ways a connection crosses
into an MCP process (Codex's forwarded-variable list, dsh's forwarded
connection). The parity test is registered with the credential tests.
Versions move past both this branch's base and main: claude-code 0.5.2,
codex 0.9.2, agent-hook 0.3.2, opencode 0.3.2, dsh 0.4.3, pi 0.3.2,
agent-plugins 0.1.2.
|
||
|
|
9ff55e8dac | docs: convert WorkBuddy guide screenshots to lossless WebP (#5134) | ||
|
|
6fb370cfba |
fix(memory-plugin): run shell commands that carry viking:// and attach a notice (#5131)
* fix(memory-plugin): split the viking:// URI guard into deny and notice A shell command that carries a viking:// URI is not necessarily trying to open it: ov CLI arguments, HTTP payloads and grep patterns all mention one. The guard used to deny every such command, and models learned to split the URI to get past it. evaluateUriGuard now denies only file tools whose path is a viking:// URI. evaluateUriNotice returns a notice for shell tools instead, naming the plugin, the replacement tool and telling the model to ignore it when the URI is intentional. preToolUseOutput wraps both for the PreToolUse hosts. * fix(memory-plugin): stop treating grep's pattern as a path pattern was one of the path keys, so Grep(pattern="viking://", path="/repo") was denied on every host that guards grep, although it only searches local files for the text. It stays a location for glob, which the generic sweep still reaches. * fix(dsh): run shell commands that carry viking:// and attach a notice bash is no longer denied by tools/pre-execute. A tools/post-execute listener delegates to the rest of the chain first, then appends a plugin context with form "notice" when the command carried a viking:// URI, so a later listener's block or content replacement survives. pluginMessage moves to capture.mjs and takes the whole source, since a notice needs a summary as well as a form. * fix(pi): notice on tool_result instead of blocking bash tool_call now denies only read/grep/find/ls on a viking:// path. A bash command that carries a URI runs, and tool_result appends the notice after the result's own content blocks. The shared plugin-config test reads pi's version from its package.json, like the other harnesses, instead of a literal. * fix(agent-hook-plugin): trae notices shell commands, cursor stops guarding the shell TRAE and ZCode use the shared preToolUseOutput, so Bash and RunCommand on TRAE get additionalContext instead of a deny. ZCode's matcher still names no shell tool, because its strict output schema is not verified to accept that envelope. Cursor has no channel that shows the model a note after a shell command, so beforeShellExecution is dropped. The installer prunes an entry an older install left behind, and the guard ignores a shell event that still arrives. * feat(opencode): notice on tool.execute.after bash had no guard on opencode. A command that carries a viking:// URI now gets the notice appended to its output; read/glob/grep keep their deny in tool.execute.before. * feat(claude-code): guard Edit/Write and notice on Bash The PreToolUse matcher grows from Read|Glob|Grep to Read|Glob|Grep|Edit|Write|Bash. Edit and Write on a viking:// path are denied like the read tools, and a Bash command that carries one gets additionalContext. The script is now just preToolUseOutput, so the shared library drops the guarded option that only this script used. * feat(codex): add the PreToolUse URI guard Codex gets the same uri-guard script as claude-code on a Bash matcher. Its Edit and Write matchers are aliases for apply_patch, whose input is a patch body with no path to deny, so the hook only ever adds a notice. The doctor expects the sixth hook trust record, and users approve it once in /hooks. * docs: capability reference rows for the deny/notice guard |
||
|
|
8ae1700232 |
fix(codex): replace retired default model with GPT-5.6 Terra (#5120)
Update the Codex OAuth setup default and examples after GPT-5.4 retirement. Document migration for saved configurations and exercise the actual wizard model selection in the existing setup test. Fixes #5072 |
||
|
|
ca777889c6 |
feat(studio): add VikingBot conversations and Feishu onboarding (#5109)
* feat(studio): add VikingBot conversations and Feishu scan onboarding * fix(studio): authorize bot management and preserve selected account * fix(studio): support Feishu onboarding on HTTP dev hosts * feat(studio): show live VikingBot scheduled tasks * fix(studio): isolate VikingBot web conversations by explicit source * fix(studio): group IM platforms in a unified channel picker * fix(studio): share VikingBot conversations across both chat entry points * docs(studio): clarify shared Agent conversation history * fix(studio): hide conversation source filters until IM is configured * feat(studio): make bots the primary list with channel filtering * style(studio): improve bot onboarding spacing and form width * fix(feishu): accept numeric console permission identifiers * fix(feishu): request current chat read permission * fix(feishu): create agents using the developer console template * fix(studio): reflect bot setup progress in action labels * style(studio): use compact rows in the bot platform picker * fix(studio): remove scheduled tasks tab from VikingBot * fix(studio): make Feishu connection verification optional * feat(studio): confirm deletion of VikingBot web conversations * feat(studio): delete bot connections and workbench conversations with confirmation * feat(studio): show group chat names and latest message previews * style(studio): show conversation times and reveal delete on hover * style(studio): unify web and group conversation rows * style(studio): distinguish workbench and VikingBot navigation icons * fix(studio): use meaningful conversation titles and channel tags * fix(studio): sort conversations across channels by latest activity * fix(studio): default conversation titles to first user message * fix(studio): persist sender identities and resolve Feishu member names * refactor(studio): simplify manual Feishu connection to one form * fix(docs): exclude internal Studio bot routes from public API checks * fix(studio): defer conversation creation until first message * refactor(studio): remove unused scheduler API and obsolete setup code * refactor(studio): align bot management routes with account admin APIs * fix(studio): import icon for Feishu conversation history * feat(studio): configure Feishu group reply preferences * fix(studio): hide internal management routes from public schema * refactor(studio): consolidate onboarding task actions * test(api): accept explicit missing-host DNS errors * fix(studio): retain persisted conversations and report Feishu outages * fix(studio): isolate managed bot sessions across connections * test(fs): verify copied index records without semantic ranking * fix(studio): show Feishu group labels and unify runtime user selection * refactor(studio): use compact bot cards and settings dialogs |
||
|
|
db84a4916c |
docs(acl): 删除已移除 relations 接口的文档残留 (#5103)
Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com> |
||
|
|
bc1384811d |
fix(studio): add server-side user pagination and username search (#5024)
* fix(studio): show all users with pagination and username search * feat(admin): paginate and search Studio users on the server |
||
|
|
8941acc864 | docs: suggest the volces registry mirror when ghcr.io is hard to reach (#5099) | ||
|
|
114f6c51b6 |
feat(web-studio): add Compile workflows and lifecycle documentation (#5098)
* feat(web-studio): add Compile workflows and lifecycle documentation * docs: remove unrelated lifecycle documents and assets * fix(web-studio): protect Compile arguments and restore task navigation * fix(studio): align compile page widths * fix(compile): preserve retry identity and durable task history * fix(studio): recover compile submissions across terminal reloads * docs: document compile capabilities and submission lookup APIs * fix(studio): preserve compile form submission recovery identity |
||
|
|
fa82231544 |
fix(vectordb): 本地字段 JSON 改用 text 并兼容旧数据 (#5091)
* fix(vectordb): 本地字段 JSON 改用 text 并兼容旧数据 为当前记录和 Delta 日志增加版本化编码,解除 fields 的 64 KiB 限制,保留旧格式读取和增量恢复。 * test(vectordb): 将格式迁移覆盖集中到恢复契约 删除重复的大字段生命周期和序列化细节验证,保留已有恢复测试中的新旧记录混存、大字段更新与删除覆盖。 * refactor(vectordb): 在 BytesRow 内部兼容本地记录格式 将新旧格式识别收敛到原有编码器,避免添加或剥离版本头时复制整行,并让原生解码直接读取不可变字节。 |
||
|
|
945527244d |
fix(api): 区分参数错误、锁冲突和内部故障 (#5097)
保留来源访问和原生锁获取的错误类型,修正目录参数误用的 HTTP 分类,避免将请求错误和正常竞争误报为服务故障。 |
||
|
|
35d76c8fd3 |
docs: clarify OpenClaw assemble, PathLock and snapshot semantics (#5092)
* docs: clarify OpenClaw assembly, path locks and snapshot semantics * docs: simplify transformContext guidance * docs: distinguish assembled context from persisted session messages * docs: show overview field and summary message wrapper explicitly |
||
|
|
d53e3d5843 |
fix(embedding): deprecate summary_only without dropping content fallback (#5056)
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com> |
||
|
|
fd719cbb7c | fix(resources): support paused watches for native Git imports (#5066) | ||
|
|
2be6814d44 |
feat(ingest): add a WorkBuddy log source (#5084)
* feat(ingest): add a WorkBuddy log source
WorkBuddy (Tencent's agentic coding assistant) writes append-only JSONL sessions to
`~/.workbuddy/projects/<project-slug>/<session-uuid>.jsonl`. The layout looks like
Claude Code's, but the record schema is different, so the existing adapters parse zero
usable turns from it.
Adds `openviking/ingest/sources/workbuddy.py` (`@register_source("workbuddy")`):
* turns are `type == "message"` with a top-level `role`; text is joined from flat
`input_text` / `output_text` content blocks;
* epoch-millisecond timestamps are converted with `iso_from_epoch_ms`;
* the title comes from the `ai-title` record, which can sit far into the file, so
`session_ref_for_file` walks the head under a line/byte budget and caches the result
on (mtime, size) -- discovery runs on every poll;
* user turns are host-composed: WorkBuddy inlines the system prompt, project context,
quoted history and reminders around the real ask. The adapter strips those blocks and
keeps only `<user_query>`, and drops turns where nothing survives (compaction
summaries, continuation prompts) instead of storing them as memory.
Measured on a real 89-session corpus: 89 sessions discovered, 40 with titles, 10 models,
and zero injected markers (`system-reminder`, `user_query`, `Project Context`, ...) in
the imported user text.
Docs: `workbuddy` is added to the supported-harness table and the `ov.conf` example in
`docs/{en,zh}/agent-integrations/09-log-ingestion.md`.
Refs #5051
* fix(ingest): keep the settled ai-title when the WorkBuddy log re-titles a session
WorkBuddy re-emits a ``type == "ai-title"`` record every time it re-titles the
session, so a log can hold several. Only the last one is the settled name; the
earlier ones are what the session was called before it found its topic.
The adapter read the title out of its head window and kept the first record it
saw, so a re-titled session was stored under a superseded name: on a real
corpus, 2 of 40 titled sessions came back with a stale title.
The head window is not fixable by widening it. The newest title is *appended*,
and was measured at a 7.3 MB / 8.2 MB offset of a 9 MB log -- outside any head
budget a discovery-time peek can afford. Read the title from a second bounded
window at the tail instead, since the log is append-only and the newest title is
the last one written. Neither window alone covers the corpus (of 40 titled
sessions, 36 had the settled title in both, 2 only in the head, 2 only in the
tail), so the tail result wins and the head result is the fallback.
Round-trip on the 89-session corpus: 40 titled sessions, 0 stale titles.
Discovery is cached on (mtime, size): 635 ms cold for the corpus, 0.3 ms warm.
---------
Co-authored-by: zhangzhangco <zhangzhangco@users.noreply.github.com>
Co-authored-by: zhangzhangco <zhangzhangco@example.com>
|
||
|
|
a126eaff21 |
fix(namespace): 统一 agent 公共目录并删除旧身份逻辑 (#5069)
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com> |
||
|
|
31f017bc16 |
fix(openclaw): commit the generated shared runtime copies again (#5062)
#4773 moved examples/openclaw-plugin/shared/ out of git on the grounds that the plugin ships as a package and builds its copies at pack time. That holds for ClawHub and npm, but not for every way the plugin is installed or built: - ov-install's GitHub source downloads the plugin file by file from a git ref. install-manifest.json lists shared/ as required, the GitHub contents API answers 404 for it on main, and the helper exits after three retries. It is reached with --plugin-version=main/master/v*, --github-repo, PLUGIN_SOURCE=github, and whenever npm resolution fails and the helper falls back to GitHub. The helper cannot run sync.mjs on that path, and a helper-side fix would not reach the copies already installed globally. - build.sh, bot/scripts/install_local_openclaw_plugin.sh and the guide's `npm run typecheck` / `npm run build` steps never run sync.mjs, so on a fresh clone tsc cannot resolve ./shared/recall-core.mjs. The closure is two modules (three files with the declaration), so mark the target committed, drop its .gitignore entry, commit the copies, and let the shared-sync workflow regenerate them on main alongside the other committed lanes. The capability reference and plugin-development guide now list OpenClaw with the committed targets. |
||
|
|
a372c2dcfe |
docs(plugins): run sync.mjs before copying a plugin out of a checkout (#5063)
#4773 stopped committing the generated shared runtime of pi, opencode, dsh and openclaw: .gitignore now covers their shared/ directories and each package builds them at pack time. The manual source installs were not updated, so copying examples/pi-coding-agent-extension or examples/opencode-plugin/lib out of a fresh clone ships a plugin whose imports point at ./shared/*.mjs files git never had, and it fails to load with a missing-module error. Running the opencode setup wizard from a checkout fails the same way. Add `node examples/memory-plugin-shared/sync.mjs` ahead of the copy in the pi and opencode guides (zh/en docs, opencode README/INSTALL/INSTALL-ZH), ahead of the opencode setup wizard, and to the dsh README's note on linking a checkout, plus a troubleshooting row for the missing-module error. |
||
|
|
65b009c578 |
fix(plugins): stop the pi installer loading the extension twice (#5059)
The installer copied the extension into ~/.pi/agent/extensions/openviking and then ran `pi install` on the same path. That directory is already one of pi's auto-discovery roots, so pi found the extension twice: auto-discovery registered its index.ts while the `pi install` packages entry pointed at the directory. pi dedupes on the canonical path, and a directory does not match its own index.ts, so both survived and the extension loaded twice — a duplicate `/viking` command and a doubled Extensions listing. Drop the `pi install` call and instead `pi remove` the path to purge any stale packages entry left by older installer versions (pi remove on a local path only edits settings, it never deletes the copied files). Flip the validation check so a lingering packages entry is reported as a warning rather than success, and update the READMEs and pi docs (zh/en) accordingly. Co-authored-by: TRAE CLI <traecli@bytedance.com> |
||
|
|
aa3ee2f431 |
refactor(plugins): resolve every harness's knobs from one declaration, and generate the packaged copies at pack time (#4773)
* fix(codex): stop dropping turns on an outage, and honour bypass patterns Two capabilities every other JS harness has were wired in the shared library but never reached codex. Offline queue. `catchUpTurns` called `sendSessionMessages` without `enqueueOnRetryable`, so a 5xx or a connection failure returned a count and the turns were gone: codex hooks are short-lived subprocesses with no retry of their own, and the `capturedTurnCount` cursor only compensates while the process survives long enough for another Stop. The queue module was vendored into `scripts/shared/` all along with no caller, and nothing ever replayed it. The send now enqueues, and SessionStart drains the queue after its health check — the one codex hook that runs against a known-healthy server. Bypass. `isBypassed` had no codex caller, so `bypassSessionPatterns` did nothing there: a directory a user had excluded still had its turns captured and still got memories injected, and the `bypass.session_patterns` key a repository commits in `.openviking/config.json` was silently inert for every codex user in that repository. All five hooks now consult the shared matcher, and the loader reads the same keys and the same `OPENVIKING_BYPASS_SESSION[_PATTERNS]` env vars as claude-code. SessionStart is deliberately only half-suppressed under bypass: it skips peer registration and injection, but still replays the queue and still sweeps. Both finish work recorded by sessions that were not bypassed, and this hook is the only place codex runs either, so bailing out would strand that data for as long as the user kept working in a bypassed repository. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * fix(pi): use the shared bypass matcher, and keep the pre-git peer reachable `config.ts` kept only `.peerId` out of `resolveEffectivePeerId()` and dropped `legacyPeerId`, so under `recallPeerScope: "actor"` — where the effective peer is the only one asked — every memory written before the git-derived peer replaced the path-derived one became unreachable. The shared recall builder has read `options.legacyPeerId` for the dual read all along; pi just never passed it. Bypass was a hand-written `matchBypass()` under this extension's own `bypassPatterns` key: it understood a leading or trailing `*` and nothing else, so `**/scratch/**` did not work here while it did everywhere else. It now calls the shared `isBypassed`, reads `bypassSessionPatterns` like every other harness, and honours `OPENVIKING_BYPASS_SESSION[_PATTERNS]`. `bypassPatterns` still reads. That does change one behaviour for existing setups: a bare path used to match its subdirectories as a prefix, and a glob does not. The README says to write `"/tmp/scratch**"` where `"/tmp/scratch"` used to be enough. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * chore(plugins): remove the deprecated standalone TRAE CLI integration `examples/trae-cli-memory-hooks` was added on 2026-08-17 (#4026) for TraeCode CLI 1.0 and deprecated the next day by #4079, which routed `--harness trae-cli` to the codex plugin alias instead. Since then `install_trae_cli` has not been in the installer's dispatch block — `normalize_trae_cli_harness` rewrites the harness to `codex` before it could run — so the directory, its 588 lines and its CI test have been dead weight that the marketplace archive still shipped to every user. The uninstall path stays: `remove_legacy_trae_cli_integration` and `agent_remove_trae_cli_configs` delete what an old install left on disk and never read this directory, which is what the retention test in `release-marketplace.test.mjs` claimed to protect. `install-agent-hooks.test.mjs` covers that path and still passes. Also drops the write half that only `install_trae_cli` called. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * fix(plugins): make a changed plugin prove it shipped Claude Code resolves an installed plugin by the `version` string in its manifest, not by the commit the marketplace ref points at. That string has read 0.4.5 since #4389 on 2026-08-27, with seven commits to the plugin and the shared library behind it — so nobody running the marketplace build has received any of them. Codex's manifest has been frozen at 0.8.1 for ten. Every vendored-copy diff those commits paid for went to a payload no user received. Both versions move, and a new PR check fails when files under a marketplace-distributed plugin change without its version string moving with them. The generated `shared/` copies live inside each plugin directory, so a shared-library change reaches the check through them. Also removes `claude-code-memory-plugin/package-lock.json`: 1174 lines locking 95 packages for a private manifest that declares no dependencies, still pinned at 0.4.4. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * docs(agent-integrations): record what codex, pi and trae-cli now do The capability matrices said codex has no on-disk queue and counted four hooks where hooks.json declares five; both are now wrong. pi's bypass is no longer a prefix match under its own key. And the removed TRAE CLI 1.0 integration is described by what remains of it: the installer's uninstall path. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * refactor(plugins): derive each plugin's vendored set from what it imports sync.mjs kept hand-written capability groups, and the groups got spread into each other: the file's own header states the rule it could not enforce ("what a plugin ships must equal what it imports"). Both directions had failed in practice — a module a plugin imported but no list named became ERR_MODULE_NOT_FOUND on the first hook of a fresh install, and modules nobody imported sat vendored in plugin trees, landing in every review diff forever. The lists are gone. Each target now says only where its code lives and where its copies go; the file set is the transitive closure of the imports that code actually resolves into the vendored directory — including re-export shims like claude-code's scripts/lib/, which are the sole importer of several modules. The sync reports a copy nothing imports and a lib module no plugin reaches, and sync.test.mjs asserts what is on disk equals that closure in both directions. The run is byte-identical for everything still shipped, and it named the two copies nobody had noticed: claude-code's capture-utils (515 lines, its auto-capture hand-rolls the same filter) and codex's uri-guard (78 lines, codex has no PreToolUse hook). ZCode stops vendoring altogether. Its only install path is `assemble_agent_integration`, the same one cursor and trae use, which already placed the canonical runtime beside it — it just imported its own committed copy instead. Pointing its nine imports at `../../memory-plugin-shared/lib/`, the path that resolves both in the repository and under `$OV_HOME/agent-integrations/`, drops 4294 generated lines and leaves cursor, trae and zcode reading one runtime. The installer's assembly list grows by the three modules zcode needs, derived by the same closure code so it cannot drift either, and the end-to-end archive install test covers it. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * fix(plugins): run cursor and trae capture through the shared filter zcode got this in #4594; cursor and trae never did. Both sent whatever their transcript parser produced straight to the extractor: `/compact`, a bare "ok", a punctuation-only turn and a `[openviking-memory]` status line all became memories, and a turn past `captureMaxLength` went out uncapped. Every other harness has run `shouldCaptureText` on the way in for months. The decision now lives beside the runtime the thin harnesses compose, as `filterCaptureTurns`, so a harness gets it by calling rather than by carrying its own copy — and the installer already assembles `capture-utils.mjs` for these three since they share one runtime. Both hooks keep hashing the raw turn rather than the filtered text, so raising captureMaxLength never resends a turn the server already holds truncated, and a dropped turn is recorded as handled so a re-read of the same transcript does not re-evaluate it every run. The TRAE hook test used `last_assistant_message: "done"` — an acknowledgement the filter drops, and correctly so. Its fixture now carries a turn worth remembering. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * fix(plugins): bump every plugin the shared change reached, and pair the manifests The version check added earlier in this branch caught what a human review would not have: `filterCaptureTurns` landed in the shared library, its generated copy changed inside opencode and dsh, and neither manifest moved — so both hosts would have kept installing the build without it. cursor, trae and pi move for the same reason. It also caught a mismatch this branch introduced: cursor carries the version in both `.cursor-plugin/plugin.json` and `openviking.integration.json`, the host installs by the first and the installer decides "nothing changed" by the second, and bumping only one makes a plugin report upgraded while behaving as it did. zcode had been sitting at 0.1.1 against 0.1.2 on main for the same reason. The check now compares the pair, so neither can drift again. Claude-Session: https://claude.ai/code/session_01GJouP1ghK4gwXw7sDjuZX6 * refactor(plugins): resolve every harness's knob from one declaration A setting only stayed uniform across the harnesses when a shared module read it on the hot path. `recallLimit`, `scoreThreshold` and `captureMaxLength` agree everywhere because `recall-core` and `capture-utils` read them; the switches, which nothing shared read, drifted into four spellings — a boolean `autoRecall`, an `autoRecall` object in opencode, `syncTurns` in dsh and pi, and no recall switch at all in the thin hook runtime. Three more inventories had to be kept in step by hand: the doctor's known-knob set, the workspace file's dotted-key map, and the implicit list inside `loadAgentHookConfig`. `lib/config-schema.mjs` is now the one declaration, and the rest are projections of it. `resolveSettings()` resolves any harness through the layers the shared README has documented since the `plugin` section landed — env, the workspace file and registry, `ovcli.conf` `plugin.<harness>`, `ovcli.conf` `plugin`, ov.conf's harness section, defaults — which only Claude Code and Codex implemented. cursor, trae, trae-cn and zcode read the environment and nothing else before this, so a `plugin` entry named after them was inert and `ov config switch` moved their credentials while leaving their behaviour behind. The two per-plugin config files are gone with it. pi's `config.json` shipped with the extension holding exactly the code defaults, so nothing could tell an operator's choice from the factory setting; opencode searched four paths for `openviking-config.json`. Both harnesses read `ovcli.conf` now, as the others do. The `.d.mts` stubs move to lib/ beside the modules they describe — pi and openclaw each carried a different, both incomplete, declaration of `recall-core` — and sync ships them to the targets written in TypeScript. The switches move into the capability modules that own them: `isRecallEnabled` and `isCaptureEnabled` accept every spelling and treat any of them saying off as off, which is the one mechanism in this repository that has actually held a name still. * build(plugins): generate the packaged plugins' shared copies at pack time `sync.mjs` had zero references under `.github/`: the generator ran on whoever last remembered, and CI only checked the result byte for byte. So every change to a 6,000-line library arrived as a 20,000-line diff, and the copies were committed for plugins that never needed them committed. The split is delivery, not taste. A host that installs Claude Code, Codex or the Agent Plugins package points at a directory in this repository and can only load what git holds, so those copies stay committed — and a push to main regenerates them, which is what stops one going stale behind a merged pull request. opencode, dsh and openclaw publish as npm packages and pi is tarred by the installer, so those four build their copies on the way out: a `prepack` script for the packages, the marketplace staging script for the archive the installer reads, and `sync.mjs` in a source checkout for a direct `install.sh` run. `npm pack` was verified to regenerate a deleted directory and ship the same file set. Two things had been holding those directories in place. Their release triggers matched only `examples/<plugin>/**`, so a shared fix reached npm through the vendored copy changing — the triggers name the library now. The version gate found a changed plugin the same way, so it treats a change under `memory-plugin-shared/lib` as a change to every plugin, and gains cursor and trae, which are distributed the same way and were never gated. pi's own `recall-ledger` moves to `lib/`, where the extension's other own modules live, so the generated directory holds nothing but generated files. * test(plugins): pin the layer order for the harnesses that had no config test cursor, trae, trae-cn and zcode compose one shared loader and none of them owns a config test, so nothing would have caught the `plugin` section going inert again. This asserts the whole stack through that loader — the shared block, a per-harness override under either spelling, the workspace file over ovcli.conf, and the environment over all of it — and pins that a default never reports as configured, since several fields reach the server only when the user asked for them. * docs(plugins): point opencode and pi at ovcli.conf, and say when copies are made Both plugins documented a configuration file that no longer exists, down to the four paths opencode searched for it and the nested blocks neither loader reads any more. Their knobs live in `ovcli.conf`'s `plugin` section now, so the examples are ovcli.conf examples, the flattened names are the ones the schema declares, and each document says the resolution order and links to the one file that declares every knob. Two claims that had gone the other way: opencode and pi read the workspace `.openviking/config.json` now, so a `peer.id` written there does apply, and the shared README says which plugins keep their generated copies in git and which build them at pack time, because a fresh checkout has to run the generator once before their tests will resolve an import. * fix(opencode): name the file the 401 hint should send the user to * fix(plugins): let the canonical knob name win over its own alias A file that carries both spellings — what a rename leaves behind — resolved to whichever the layer loop reached last, which was the older name. * fix(plugins): write each generated copy through a rename The marketplace staging script runs the generator now, so it can rewrite a vendored module while a test is byte-comparing it. A partial file read that way fails as a drifted copy, which is a confusing way to say nothing drifted. * fix(dsh,pi): honour the recall switch these two never read Both gained a capture switch when `syncTurns` became an alias, but their recall paths still retrieved on every prompt: nothing in either called the switch, so `autoRecall: false` — settable from ovcli.conf, a workspace file or the environment — was accepted everywhere and obeyed nowhere. * docs(agent-integrations): say which harnesses read which configuration layer The capability reference had cursor, trae, trae-cn and zcode down as environment-variable-only, opencode reading a four-level `openviking-config.json` search and pi its own `config.json`, and the workspace files and `plugin` section scoped to Claude Code and Codex. All nine resolve through the same loader now, so the tables, the quick-scope list and the profile cards say so, and the module table gains the schema the layers project from. The distribution paragraph was counting modules against a `HOOK_SHARED_FILES` list that no longer exists and calling zcode the only harness that vendors the hook runtime, which stopped being true when its copies were dropped. The counts are the real ones, and the paragraph now says when each target's copies are generated, since that is what decides whether git holds them. * build(plugins): keep node_modules out of the marketplace archive Three of the staged plugin trees carry installed development dependencies, so the copy shipped 134MB nobody unpacks and spent most of the release test's 54 seconds on them. Copying through tar leaves them behind while still carrying the generated shared copies, which a tracked-file listing would miss. * build(plugins): read the installer's shared-module list from the sync The installer carried a second, hand-written copy of the closure sync.mjs already computes, and only a regex over the bash text held the two equal. The sync writes that closure to lib/MANIFEST now and the installer copies what it names, so the set has one source and the test compares two lists instead of scraping a script. The installer cannot just run the sync: a marketplace archive is flat, and the generator resolves an empty list there. * build(plugins): derive what the marketplace archive must contain The stage script hand-listed 56 required paths beside three generators that already knew them, and it had drifted from all three: eleven of the twenty-two shared modules the installer assembles, four of the thirty-eight copies the sync generates, and neither marketplace manifest. The list is now computed from each plugin's own manifests, the imports those entrypoints reach, and the sync's target list, leaving the script with only the directories it ships. * fix(plugins): resolve credentials from the calling harness's ov.conf section The shared resolver read `ov.conf`'s `codex` block for every caller, so the api key, account, user and peer of opencode, pi, dsh, cursor, trae, trae-cn and zcode all came out of a block named after another harness, while the block named after them did nothing. A user who wrote `opencode.apiKey` saw no effect and a stale `codex` block quietly authenticated everything else. `resolveOpenVikingCredentials` now takes the harness and reads the section named after it, under either spelling. The layer keeps its place — after ovcli.conf, ahead of `server.root_api_key` — and the ovcli-pinned mode still skips ov.conf entirely, so codex, the caller that keeps the default, is unchanged. For users this means the `<harness>` block finally configures that harness, and anyone who was relying on the `codex` block to supply credentials to a different harness has to copy them into their own block. dsh merges its section in explicitly because the cordis patch occupies its legacy layer, and codex now honours an api key set through `ovcli.conf`'s `plugin` section, which the credential chain has never seen. cursor, trae, trae-cn and zcode honour it too: the hook runtime spread the resolved credentials over the settings, so an empty key from the chain used to erase the one `plugin.<harness>.apiKey` had supplied. The doctor warns about all of these blocks now, since the server refuses to start on any of them. * fix(plugins): let the credential chain supply the peer on claude-code and dsh Claude Code had no peer logic at all: it took only the user agent from the shared credential module, so `actor_peer_id` in ovcli.conf and `peerId` in ov.conf's `claude_code` block were read by every other harness and ignored here. `ov config switch` moved the peer and Claude Code kept writing under the one derived from the workspace. It now takes the peer from the same chain, and a `plugin` entry or a workspace file still names the more specific answer for that directory, which is the rule the thin harnesses already follow. dsh had the opposite fault: it assigned the credential peer over whatever the layers resolved, and that chain is the empty string whenever no actor peer is named, so `plugin.dsh.peerId` was resolved and then thrown away. The credential peer is a fallback now instead of an overwrite. * fix(plugins): resolve the peer through one chain on every harness Six loaders each wrote their own peer order, and two of them disagreed with the rest: codex let a `peerId` written for the harness win, while opencode and pi let `ovcli.conf`'s account-wide `actor_peer_id` win. One ovcli.conf therefore produced two different peers depending on which host read it, and `ov config switch` moved the peer for some harnesses and not others. `resolvePluginPeerId()` in the shared library is now that order, once: a peer the host named, then `OPENVIKING_PEER_ID` (suppressed when the credentials are pinned to ovcli.conf, where the environment is meant not to apply), then the workspace file, the registry and `ovcli.conf`'s `plugin` section, then `ovcli.conf`'s `actor_peer_id`, and last `ov.conf`'s harness block, which keeps the place it has always had. Nothing named means nothing explicit, which is what makes `peer.source` derive one. For opencode and pi users this is a precedence change: a `peerId` under `plugin` or `plugin.<harness>` in ovcli.conf, or a `peer.id` in a workspace file, now outranks that file's `actor_peer_id` instead of being outranked by it. Anyone who wrote both and wanted the actor peer has to drop the plugin one. It is one for claude-code too: `ov.conf`'s `claude_code.peerId` was the only peer that harness read, and it now sits under `actor_peer_id` like every other harness's block. Otherwise nothing moves: `ov.conf`'s harness block is ranked here rather than left to the credential chain, which drops it whenever the credentials are pinned to ovcli.conf, so that block names the peer in the same place whether they are pinned or not. * fix(plugins): shape the cursor and trae MCP proxies like every other one Both proxies hand-built the config object the shared `buildMcpProxyConfig` produces everywhere else, and had done so since before that helper existed, so they never picked up the fixes it carries. The visible one is the actor peer: they put whatever peer the config layers resolved straight onto `X-OpenViking-Actor-Peer`, while the default `recallPeerScope` of `all` means broad recall and every other proxy deliberately sends no such header. Cursor and TRAE therefore recalled at a narrower scope than the rest on identical configuration. Two smaller ones come with it: the watch list now includes the default credential paths, so a proxy started before the first `ov login` picks up the file it creates instead of never reloading, and the request timeout is clamped rather than taken raw. The guard is the file list in `mcp-proxy-config.test.mjs`, which named five proxies by hand and so never covered the two that were wrong. It is a directory scan now, pinned to a count so a rename cannot quietly empty it, and it asserts that every proxy in the tree reaches the shared builder. * fix(plugins): put one set of headers on the wire from every harness Seven request builders each wrote their own header block and three of them disagreed. Codex sent the api key twice, as `Authorization: Bearer` and again as `X-API-Key`, which the open-source server prefers when both arrive — so a gateway rewriting one of them changed which credential authenticated. Claude Code read `cfg.accountId` where the other six read `cfg.account`. And only Codex asked whether the server was in trusted mode before naming the operator: everywhere else `X-OpenViking-Account` / `X-OpenViking-User` went out whenever they resolved, including to `api_key` servers that read both out of the key, ignore the headers, and leave the identity visible to every proxy on the path. Both doctors have been warning users about that as if it were already fixed. On the wire, from this commit: - `X-API-Key` stops. Codex's `ov-session.mjs`, `auto-recall.mjs` and `session-start-commit.mjs` were the only senders; `Authorization: Bearer` is unchanged and remains the only credential header. A gateway that needs `X-API-Key` has to add it itself. openclaw keeps sending it and is untouched. - `X-OpenViking-Account` and `X-OpenViking-User` are sent only under `sendIdentityHeaders`, which is `authMode === "trusted"`. Codex already behaved this way; claude-code, opencode, dsh, pi and the cursor / trae / trae-cn / zcode hook runtime now do too, and so do the two senders outside the hook stacks: every harness's stdio MCP proxy, which takes the switch through `buildMcpProxyConfig` the way it already takes the actor peer, and Claude Code's status-line server probe. Auth mode resolves through `resolveAuthMode()` in the shared credentials module: `plugin.<harness>` `authMode`, then `plugin.authMode`, then `ov.conf` `<harness>.authMode` on the three harnesses whose loader reads that section as a settings layer (claude-code, codex and dsh), then `ov.conf` `server.auth_mode`, then trusted whenever an account or a user resolved at all — the chain Codex already had, now shared. An operator whose server is in `api_key` mode and who has an account in ovcli.conf stops sending it; nothing else changes. - `X-OpenViking-Actor-Peer`, `User-Agent` and `Content-Type` are unchanged. Claude Code's config now also exposes the resolved identity as `account` / `user` beside the existing `accountId` / `userId`, and its doctor and its status-line probe use the headers the plugin would really send instead of always sending both. The Agent Plugins package resolves an auth mode of its own, so its proxy still names the operator to a trusted server. The free half of the same theme: `session-start-commit.mjs` carried a private `requestJSON` / `commitOvSession` pair that duplicated `ov-session.mjs`'s down to the envelope. It imports them now, which is one header block fewer to keep in step. `wire-headers.test.mjs` drives six of the seven stacks through a stubbed fetch and asserts one header map for all of them, in trusted mode and in api_key mode; `auto-recall.test.mjs` covers the seventh, which only exists as a subprocess, against a real server socket; and `mcp-proxy-config.test.mjs` puts the proxy's two identity headers on the same switch. * test(openclaw): run only the live test copies under tests/ut Six files sat directly under tests/ as copies of suites that had since moved on, and vitest's default include meant two of them still ran on every `npm test` against an older shape of the code. What each one is: - `tests/context-engine-assemble.test.ts` is an earlier cut of `tests/ut/context-engine-assemble.test.ts` — same imports one directory shallower, 360 lines against the live copy's 838. Five of its six cases are in the live copy verbatim; the sixth, "records senderId from runtimeContext in assemble diagnostics", is covered by the `senderIdFound` assertions in `tests/ut/context-engine-modules.test.ts` and `tests/ut/context-engine-afterTurn.test.ts`. - `tests/context-bloat-730.test.ts` imports `memory-ranking.js`, `config.js` and `auto-recall.js`, and every symbol it exercises already has a home under `tests/ut`: `postProcessMemories` and `pickMemoriesForInjection` in `memory-ranking.test.ts`, `buildMemoryLinesWithBudget` and `estimateTokenCount` in `build-memory-lines.test.ts`, `recallScoreThreshold` and `recallMaxInjectedChars` in `config.test.ts` and `query-config.test.ts`. - `tests/test-memory-chain.py` and `tests/test-tool-capture.py` are earlier drafts of `tests/e2e/test-memory-chain.py` and `tests/e2e/test-tool-capture.py`, which drive the same phases against a live gateway at greater length. - `tests/demo-memory-ajie.py` and `tests/demo-memory-xiaomei.py` are one manual gateway demo under two personas, differing in the user name and the port, and named by no doc, script or manifest. Nothing referenced them: the only prose pointer to an assemble test names the `tests/ut` copy, and `.clawhubignore` plus `tsconfig.build.json` already kept the whole directory out of the published package. So that a file dropped beside the suite is not silently picked up again, `vitest.config.ts` now pins `test.include` to `tests/ut/**/*.test.ts`. The CI job's `--exclude` filters still apply on top of it — 42 files listed, 41 after the architecture-boundaries exclude. The one file left in `__tests__` falls outside the pin until it moves into `tests/ut` with the rest. `architecture-boundaries.test.ts` kept `tests/context-bloat-730.test.ts` in the list of files it reads for index-facade imports, which would have thrown on the missing path, so that entry goes too. * test(openclaw): move the shouldBypassSession cases under tests/ut `vitest.config.ts` now collects only `tests/ut`, and the six cases in `__tests__/bypass-session-patterns.test.ts` were the sole behaviour coverage of `compileSessionPatterns`, `matchesSessionPattern`, `shouldBypassSession` and the `ingestReplyAssistIgnoreSessionPatterns` fallback in the config schema, so they had stopped running. They move beside the other `text-utils.ts` tests, one directory deeper, and the emptied `__tests__` directory goes with the two build files that still had to name it. * docs(openclaw): drop five reports that nothing points at any more Four are dated one-off write-ups of manual runs, and three of them name the repository and branch they were made against, neither of which is this one: - `docs/openviking-install-real-scenario-verification-report.md` (2026-06-05) walks one machine's install of plugin `2026.6.2` from `iaasng/arkclaw-openviking-plugin`, against a hosted endpoint and a specific OpenClaw build. - `docs/openviking-dynamic-query-config-test-report.md` (2026-06-04) is the acceptance run for the runtime query-config work in that same repository; `docs/openviking-runtime-query-config.md` is the reference that documents the feature and stays. - `docs/workmemory-v2-test-report.md` (2026-05-02) is a `locomo10` benchmark of Working Memory v2, beside the design note that still describes it. - `openclaw-multi-tenant-test-report.md` records a session driven through two ad-hoc remote ports and a live Feishu bot, reproducible by nobody. The fifth, `docs/oc-resource-skill-import-design.md`, is an RFC whose own opening paragraph says the implementation went the other way: no unified `ov_import(kind=...)` tool and no `/ov-import` command, which is the shape the rest of the document is about. One line pointed at any of them — the Working Memory entry in the guide's reference list, whose neighbour is the design note and stays. Nothing shipped moves: `docs/` is outside the package's `files` list, the top-level report was never in it, and the ClawHub release workflow names no document. * test(openclaw): drop the unreferenced tool-result compression bench tests/toolresult_compression_tests/ was manual measurement tooling: run-sccs-bench.mjs spawns an external openclaw binary turn by turn and reports token usage, so it asserts nothing and needs three repositories cloned to hardcoded /root paths plus a live server before it can run at all. Nothing outside the directory named it — the two invocation lines the plan cites were in its own README. The behaviour it exercised is covered by tests/ut: tool-round-trip.test.ts asserts that an externalized tool result survives conversion with its preview text, its viking://session/<id>/tool-results/<id> ref and original_chars intact, and tools.test.ts asserts the restore half through openviking_tool_result_read / _search / _list. * test(openclaw): assert the prepack script that regenerates shared at pack time The packaged plugin's prepack gained a `sync.mjs` prefix when the generated shared copies stopped being committed, but the manifest contract still pinned prepack to `npm run build` alone. The openclaw-tests job runs that file, so the suite has been red on a stale expectation rather than on a real contract break. * chore(claude-code): drop the two unwired debug scripts Neither debug-recall.mjs nor debug-capture.mjs is reachable from hooks.json, install.sh, or the sync target, so they drifted away from the hooks they were meant to mirror; debug-capture also rewrites the live session's capture cursor through an API flow the plugin no longer uses. The doctor skill, both READMEs and the capability reference lose their pointers in the same change. * docs(pi): rewrite DESIGN.md around the modules that exist The spec was written in the future tense of a plan only partly executed: it specified an `index_builder.ts` that was never built (the profile block from shared/profile-inject.mjs folded into systemPrompt took its role), gave no section at all to config.ts or takeover.ts, carried per-file line estimates that went stale on every commit, and repeated its Knowledge Index sample twice. Its Implementation Order and Testing Strategy described work the code and tests/ have both already done. It is now one section per module actually shipped, each written from that module's current code, plus the event walkthrough, the design ancestry and the two rationales the code cannot state for itself. TAKEOVER.md's model, runtime flow, compaction and failure modes move into the takeover section so the one harness capability that is unique to pi is documented beside the module that implements it; its configuration table was already in README.md. * docs(codex): fold VERIFICATION.md into a README Testing section The 320-line manual SOP was pinned to plugin v0.6.0 and every step it walked is now asserted by the node --test suite that CI runs. Only its two live legs -- extraction landing in the user namespace and the interactive Codex smoke test -- need a real server, so they stay as a short appendix next to the command that actually runs the suite. * chore(opencode): drop three unreferenced helpers from lib/utils.mjs makeMultipartRequest was a near-copy of makeRequest that no caller ever reached, and the wired URI check is lib/viking-uri-guard.mjs, not validateVikingUri; ensureRemoteUrl had no caller either. * chore(pi): drop six unreachable OVClient methods createSession, addMessageParts, addMessagePayload and deleteSession have no caller: the OV session id is derived locally and the server-side session is created implicitly by the batch messages endpoint, messages go out as one addMessage or through that batch endpoint, and nothing in the extension deletes a session. resolveScopeSpace and resolveTargetUri were a URI-space rewriter that no tool path ever reached, and they held the only readers of RESERVED_USER, RESERVED_AGENT and the resolvedSpaces cache. The sync-barrier stub kept an addMessagePayload key for the same reason; sync.ts queues and replays through fetchJSON, so the key asserted nothing. * chore(dsh): drop nine unreachable OpenVikingClient methods health, ensureSession, getSessionArchive, find, read, list, stat, forget and addResource had no caller: the runtime only ever uses fetchJSON, healthResult, ensureSessionResult, getSession, addMessage and commitSession, and the tool surface the read and write helpers look like they serve is bridged through the stdio MCP proxy, so they could never be reached. Only tests called them. The peer-override and live-recall cases move onto ensureSessionResult and healthResult, which take the same arguments, and the 409/ALREADY_EXISTS tolerance moves to a runtime test: initializeState carries its own copy of that check, so the behaviour stays pinned where it now lives. * chore(codex): drop the ov-credentials pass-through scripts/ov-credentials.mjs re-exported two names from shared/credentials.mjs and repeated that module's CLI verb-for-verb. Nothing ran it as a command, so the file only added a second path to the same resolver, one that every credential fix since the shared library landed had to remember to keep in step. Its three importers now reach the shared module directly. The test file keeps its name because .github/workflows/pr.yml lists it by path, and it asserts the shared resolver either way. * chore(install): drop the pre-stdio layout migrations The rc-wrapper stripper and the two legacy-marketplace migrations cleaned up after installs that predate the stdio MCP proxy and the unified `openviking` marketplace name, both of which landed in July 2026. Every install since then writes the current layout, so the three helpers ran on every upgrade only to find nothing, while keeping four constants and a TOML rewriter alive for a shape the installer can no longer produce. The installer-side cleanup is what goes; both doctors still detect a leftover `openviking-plugins-local` install and tell the user how to remove it by hand. The Codex README's step list loses the line that promised the migration, and the installer test that asserted a Cursor-only run left the rc blocks alone goes with the code it was pinning. * chore(claude-code): drop the dead archive-abstract loop SessionStart rendered up to five `<archive-abstract>` entries from the session context's `pre_archive_abstracts`, but the server hard-codes that field to an empty array: openviking/session/session.py returns `[]` from both context builders and says so in a comment ("保留字段返回空数组,保持 API 向下兼容"). The loop is dead twice over, since the field's entries are objects while the filter kept only strings. What resumed and compacted sessions actually receive is unchanged: the `<session-archive>` block still carries `latest_archive_overview`, which is the one field the endpoint fills. The capability reference loses the "≤5 pre_archive_abstracts" claim it made for claude-code alone. * refactor(plugins): put the four JS hook stacks on one HTTP module Claude Code, Codex, dsh and the thin-harness runtime each carried their own AbortController, header block and envelope parser for the same server, which is how the wire drifted apart in the first place. lib/ov-http.mjs owns that shape now — Bearer only, identity headers only when the config says the server is trusted with the operator's name — so the next change to it lands once instead of four times. Also fixes the usage line in credentials.mjs, which still named the wrapper that was deleted out from under it. * refactor(plugins): move the Claude Code and Codex session stacks onto the runtime Claude Code's scripts/lib/ov-session.mjs kept its own copy of the session helpers the shared hook runtime already had — a fetch builder, the pending-queue enqueue, add-message, commit, and the two session getters — so a fix to how a failed write is parked had to be made twice, and the two copies had already drifted: only Claude Code's set pendingQueued / pendingEnqueueFailed and warned about a non-retryable failure. The runtime carries that contract now. makeAgentFetchJSON takes the timeout and the actor-peer getter its callers vary (Codex knows its peer only after loading state under the session lock, and Claude Code names one per call), and resolves the workspace peer on demand so a caller that never reads it pays nothing. addAgentMessage and commitAgentSession report what became of a failed write the way the capture hooks log it, and commitAgentSession takes the retention payload Claude Code's threshold commit sends. Both harnesses' modules are wrappers over it, keeping Codex's result-or-null fetchJSON beside the envelope fetchJSONRes. That report now reaches further than Claude Code: the three thin harnesses on this runtime — cursor, trae/trae-cn and zcode — name a write no retry can fix on stderr too. * refactor(plugins): put the last header blocks on the shared builder opencode and pi still carried a whole fetch stack of their own, and the doctor, the MCP proxy, the Claude Code statusline probe and the Codex recall hook still spelled the header block out by hand. All six go through lib/ov-http.mjs now: opencode's fetchJSON and makeRequest are a shell over it that keeps its throwing contract and its two hints, pi's client keeps its typed methods over the same envelope, and the four header-only callers ask buildOvHeaders. A test asserts what that buys: X-OpenViking-Actor-Peer appears in the plugin family's non-test sources only in lib/ov-http.mjs, so the next hand-rolled header block fails before it can drift. Its scope comes from the sync targets, so a harness added there is covered without touching the test. Codex's recall hook stops overriding the per-call actor peer, which makes its legacy-peer sweep ask for the legacy peer instead of asking twice for the effective one. * refactor(claude-code): put auto-recall back on the shared recall core Claude Code's auto-recall.mjs carried a line-for-line copy of the recall core's query profile, lexical overlap boost, ranking, dedup, user-space resolution, multi-source search and budgeted block builder — 280 lines that only its own statusline numbers kept it from calling. Those numbers now come from the shared side: buildRecallBlockDetailed returns the block together with the counts the fallback builder already computed, plus a stage that says which path produced it, so a host can tell an empty recall the server had nothing to offer from one the score threshold emptied. buildRecallBlock stays the string-returning shell over it that opencode and pi call. The hook is left with the Claude Code envelope and the last-recall.json snapshot the statusline reads, and a test covers all eight reasons that file records — including a guard that fails if the ranking pipeline is ever copied back into this plugin. * chore(claude-code): drop the dead per-message capture filter auto-capture.mjs's shouldCapture has had no call site since the hook moved to batches: its length bounds, slash-command, punctuation-only and question-only rules each drop a whole multi-turn batch for what one turn in it looks like, and the comment at the batch gate has said so ever since. That left four regexes reachable only from a function nothing calls. The batch gate keeps what it always did — skip an empty batch, and in keyword mode require some user turn to carry a trigger phrase — and its comment stops naming the function that is gone. * refactor(claude-code): read the transcript through the shared capture utilities Claude Code's transcript walk was written twice — once in auto-capture, once in subagent-stop — and neither copy was the shared one every other harness uses, so a fix to block handling reached nine harnesses and skipped this one. Both now go through scripts/cc-transcript.mjs, which re-exports the shared capture utilities and adds only what is genuinely Anthropic-shaped: a tool_result nests its output in a content array and names its call by id, and that output is dropped from the turn's text while travelling verbatim in the turn's tool part. The kept turns, their order and their parts are unchanged, so the capture cursor still means what it did. The shared sanitizer now also strips <system-reminder> blocks and [Subagent Context] lines; without them this switch would have started sending Claude's own notes to itself to OpenViking as if the user had written them. * test(codex): pin the digest URI repair the shared compressor performs The recall compressor is a small model spawned through `codex exec`, and it occasionally rewrites a long viking:// URI while rephrasing a bullet, which leaves a dead link in the injected digest. Since codex went onto the shared `compressRecallContext`, every citation is snapped back onto the URIs the server actually returned before the digest is injected; this test pins that a mangled URI comes back repaired, with the short-input short-circuit disabled so the compressor is actually exercised. * refactor(plugins): open every hook entry with the shared stage The Claude Code and Codex hook entries each read stdin, re-resolved the config for the payload's directory and answered the enabled and bypass gates in their own words, and the copies had drifted: Codex's PreCompact never re-checked its own switch after the reload, and the two auto-recalls asked the two gates in opposite orders. runHookStage is that opening. An entry hands it a config loader, its gates and its envelope, and keeps only its own work, which now returns what the envelope should carry instead of writing stdout from wherever it happens to stop. The stage also carries the bypass verdict for the hook that must not stop on it — Codex's SessionStart still sweeps and replays for other sessions inside a bypassed repository — and a single-shot emit for the one that answers before its worker starts. The write-path preamble stays in the entry: a detaching hook has to spawn its worker before stdin is consumed, and only the entry knows the response its host expects. Claude Code's uri-guard keeps its own code as well; it reads no config and answers no gates. * refactor(plugins): fold the agent URI guard into the shared one lib/agent-uri-guard.mjs was a second module over lib/uri-guard.mjs, holding one more hint table and nothing else, and the three harnesses that imported it each carried their own stdin read, entrypoint check and deny envelope — thirty-odd lines apiece for the choice between two envelope shapes. evaluateUriGuard is that module's body with the two things a host actually differs on made arguments: the hint table, and the tool names that host guards at all — Claude Code and opencode never see the shell in their matchers, pi does, and the set was previously implied by whichever table a host happened to import. The deny envelopes and the hook plumbing move in beside it, so cursor, trae and zcode keep only the envelope they answer with. * refactor(plugins): let the last four URI guards call the shared evaluator Claude Code, opencode, dsh and pi each kept a private copy of the guard loop — normalize the tool name, look it up in a local table, sweep the arguments for a viking:// URI, format the message — because their tables name different replacement tools. The loop is the same everywhere; only the table is host data, and it has to stay host data: read(uris="..."), openviking_read(uris=[...]) and viking_read(uri=..., level=...) cannot be spelled from one set of strings. So each of the four now hands its table to evaluateUriGuard and keeps only the decision its host answers with. Claude Code's table was character-identical to the shared one, so it passes the guarded set instead — read, glob and grep, leaving Bash to the model as its test has always pinned — and its stdin read, entrypoint check and deny envelope go the way the other hook guards' did. * refactor(plugins): run both doctors from one shared entrypoint The Claude Code and Codex doctors were forked from one commit and still carry the same run: parseArgs and tryJson byte for byte, expandHome inlined, the same eight sections in the same order, the same --json envelope and exit code. The copies had drifted apart in the details — Codex never learned to say which node PATH resolves to when it differs from the one running the script. runDoctor owns that run now. A harness hands it its name, its CLI, the three sections only it can produce (install, config, activity) and the spelling it uses for the configured account and user; it gets the argument parsing, the section order, the envelope and the exit code back. Codex keeps assessHooksFeature, parseFeaturesList and the isDirectRun guard its test imports, and its auth-mode verdict arrives through onSummary. Both wrappers also stop scanning shell rc files for the pre-2026-07 wrapper blocks: the installer that wrote them is gone, so only the OPENVIKING_* exports those files may still carry are worth reporting. * refactor(plugins): compose the doctor configuration section from shared segments Both doctors printed the same section from two copies that had drifted, so a check written for one host was invisible on the other. The section is now six shared segments a host calls in order, and the three checks that only one of them had — Codex's hook-budget warning, Claude Code's extra_headers X-API-Key and NODE_TLS_REJECT_UNAUTHORIZED warnings — run on both. * refactor(plugins): declare which recall knobs the request omits by default recall-core sends limit, max_tokens, query_expansion and rewrite_max_bullets only when a layer actually set them, so the server's own defaults stand where the user expressed no preference. That decision lived in five hand-written `<name>Configured` projections and nowhere in the schema, so a loader had no way to know which knobs it owed a flag. The schema marks the four now, and a test ties the marked set to the fields recall-core actually gates. * refactor(plugins): assemble every harness configuration in one place Six loaders spelled out the same sequence — read the credential files, resolve the knobs, resolve the peer, derive the timeouts, the log path, the user agent and the send-only-when-configured flags — and each spelled a slightly different subset of it. ov.conf's `<harness>.authMode` reached only the three that passed a legacy layer, two of the four knobs the server defaults for itself were reported as configured on two harnesses, and Claude Code resolved its api key on a chain of its own. buildPluginConfig() is that sequence, once. What is left in a loader is what only that harness knows: Claude Code's four-valued credential source, Codex's on/off reading of the digest switch, opencode's three section knobs, pi's older debug-log variable, dsh's cordis input. The api key now resolves the same way everywhere, with ovcli.conf's `plugin` section ranked where the file it lives in ranks — under that file's own `api_key`, over ov.conf. Codex read it last, behind `server.root_api_key`; its doctor, its reference table and the capability reference said so, and now say what the code does. * refactor(plugins): resolve the portable bundle's connection from the shared loader The Agent Plugins package kept its own copy of the credential chain, the User-Agent, the timeout and the debug logger, so every fix to the shared resolution had to be mirrored by hand and the proxy fabricated a credential source out of whichever config file happened to load. Importing the full loader is not the answer either: it would pull the knob schema and the workspace layers into a bundle that has no hooks to run them, and those copies are committed. `buildProxyConnection()` is the connection half of that loader, which is all a stdio proxy needs. The api_key still ends at ov.conf's `server.root_api_key` even when ovcli.conf pins the chain to itself: this package ships without an installer, so an install that names only a url there has nobody to migrate its key. * refactor(plugins): run every thin-harness hook from one entry Cursor, TRAE and ZCode kept eleven shim scripts between them whose whole body was an event assignment and an import of the harness dispatcher, and each spelled the plugin root its own way — `${CURSOR_PLUGIN_ROOT}`, `__OPENVIKING_TRAE_ROOT__`, `${ZCODE_PLUGIN_ROOT}` — so the installer carried one substitution branch per spelling and a template that used the wrong one failed only at the first hook of a fresh install. The shared entry takes the event and the client from argv, keeping TRAE's trailing client-id form, and one placeholder leaves one regex to render it. The installed hooks.json is now checked against the tree it was rendered for: a command naming a script the install did not put on disk used to surface only when the host first ran it. * refactor(plugins): merge the three thin harnesses into one plugin Cursor, TRAE and ZCode were three marketplace directories running the same state machine — the same 2000ms session-start debounce, the same prompt dedup by event id and 500ms window, the same recall cache and cross-process lock — around four things that genuinely differ: the event vocabulary, the response envelope, how a prompt is read out of the payload, and how a finished turn is captured. Everything else was triplicated, so a fix landed in whichever copy the author happened to open: the URI guard existed three times over one shared evaluator, the MCP proxy three times over one shared builder, and the guard that pins how many proxies exist counted eight. `agent-hook-plugin` keeps one dispatcher on stage 4's `runHookStage`, one URI guard, one MCP proxy and one doctor, and puts the four differences in `hosts/<id>.mjs`. The adapters sit at that depth because `../../memory-plugin-shared/lib` has to resolve both here and in an installed `agent-integrations/<client>/`, so `hosts/<id>/` holds only what a host reads as configuration. The installer copies per host and reads the templates from there, and the thin harnesses get the doctor entry the full plugins have had since stage 5. The archive guard was reaching none of this: the three plugins' hooks.json named the shared entry across the plugin boundary, so nothing required their own dispatchers to be in the release archive. hooks.json names the plugin's own `scripts/hook.mjs` again, which puts every adapter back in the derived requirement set, and the checker is handed the directories the staging script declares instead of listing whatever the stage happens to hold. * fix(install): reclaim the URI-guard hook entries on uninstall `--uninstall` recognised an entry as OpenViking's only by the hook script it named, and the URI guard was not on that list. Uninstalling Cursor therefore left beforeReadFile and beforeShellExecution in ~/.cursor/hooks.json, and TRAE kept its PreToolUse entry, all of them running a uri-guard.mjs under agent-integrations/<client>/ that the same uninstall had just deleted: the host reported a failing hook on every file read and shell command until the user edited the file by hand. The uninstall filter now reclaims anything the installer wrote, by the OPENVIKING_INTEGRATION_ID the rendered command carries, the way the install-time filter and the TRAE CLI uninstall already did. * ci(plugins): select the plugin test files by glob The step named 54 paths by hand, so a new test file was only covered once someone remembered to add it there too. Seven globs select the same set: the union and the old list differ in neither direction (82 files each), and no generated shared/ copy holds a test for a glob to pick up by accident. examples/*/scripts/*.test.mjs 19 examples/*/scripts/lib/*.test.mjs 2 examples/*/servers/*.test.mjs 2 examples/*/tests/*.test.mjs 20 examples/*-plugin/*.test.mjs 11 examples/memory-plugin-shared/*.test.mjs 27 agent-plugins/*.test.mjs 1 Two things the job could not see before. A stale lib/MANIFEST or committed shared copy passed CI, because nothing looked at the tree after the generator ran; `git diff --exit-code` does now. And the two installer tests ran beside the other 80: staging the marketplace regenerates the shared copies those files import, and both fork whole installs, which is the load the two unexplained single-test failures appeared under during this work. They now run in a step of their own at --test-concurrency=1, and install-agent-hooks.test.mjs copies examples/ into a tmpdir so `--source dev` no longer installs out of the tree everything else is reading. Neither failure reproduced in five full runs here. The only failure five runs did find was deterministic and local: a gitignored openclaw-plugin/shared left behind by work that has moved to another branch, which the generator reports as stale but never deletes. The openclaw job keeps its exclude; only its counts change, measured on this branch: 42 files, 675 tests, and 2 pre-existing architecture-boundaries failures rather than 4. * ci(plugins): bring pi into the plugin version gate pi's version was a hand-written constant in config.ts, which put it out of reach of check-plugin-version-bumps.sh: the extension is copied wholesale by the installer and reports that constant as the build talking on the wire, so a shipped change under a frozen version had nothing to catch it. The extension now carries a package.json, EXTENSION_VERSION reads it through the shared readManifestVersion, and the gate watches that file the way it watches the other five manifests. The manifest changes nothing about how the extension loads or ships. pi's loader reads a package.json only for a pi.extensions field and falls back to index.ts without one; pi install parses a local path as a directory rather than an npm package, and only installs dependencies for git-cloned sources; the installer's tar copies the directory whole, so the file is in the marketplace archive and the installed copy resolves the same 0.3.0 the constant held. "type": "module" states the format node was already detecting per load, which is what the warning naming this file asked for. Against origin/main the gate still reports four plugins: pi's manifest is new on this branch, so it takes the same no-baseline skip agent-hook-plugin takes, and is enforced from the next base onward. * ci(plugins): merge the two npm plugin releases into one matrix The dsh and opencode release workflows were the same 81 lines with seven values swapped, so every fix to the publish flow had to be made twice and one copy could drift unnoticed. plugin-npm-release.yml carries the flow once and lists the two packages as matrix entries. Every way the two files differed, and how the matrix expresses it: workflow name one "Plugin npm Release"; the per-package "Publish @openviking/..." survives as the job name job name name: Publish ${{ matrix.package }} work directory defaults.run.working-directory: ${{ matrix.directory }} concurrency moved from the workflow to the job and keyed by matrix.directory, so the two packages still queue independently rather than behind each other push paths the union of both plugin directories plus this filename; the shared lib entry was already identical in both install command matrix.install: npm ci for dsh, npm install for opencode, which ships no lockfile name assertion compared against matrix.package through an env var instead of a literal inside the node script The union filter means a dsh-only push also starts the opencode leg. That leg validates and then stops at the npm view already-published check, the same check that already made a shared-lib push a no-op for whichever package was not bumped. Everything else is byte-identical to what both files ran: permissions, checkout, node 24, the validate step, and the publish step with its NPM_TOKEN-or-OIDC fallback and --provenance. actionlint reports nothing on the new file. RELEASE.md named the opencode workflow by filename, so its bullet now names the merged workflow and both packages. * refactor(plugins): give each skill copy its own sync target SKILL_TARGETS was the one list in the generator with its own shape — a skill plus an array of directories — so the delivery flag every other target carries had nowhere to live, and nothing asserted that git holds the skill copies a host installs by path. One entry per copy, `{ skill, dir, committed }`, and the test that decides which generated copies belong in git reaches skills as well as vendored modules. Every skill copy is committed and stays that way: .gitignore covers the vendored shared/ directories only, and a host installs a skill by copying its path out of this repository, so a copy git does not hold ships nothing. Nothing else moves — the same two skills reach the same six directories and the generator writes the same bytes. openclaw-plugin and agent-plugins stay out on purpose: their skills are different files over different tool surfaces, not copies of these. * refactor(install): move the installer's JavaScript into lib/install install.sh carried a 328-line JSONC editor and three near-identical hooks/mcp merges as node heredocs. Nothing could exercise them except running the whole installer against a scratch HOME, and they had already drifted: the uninstall copy of the ownership test learned to reclaim the URI-guard hook entries and the install copy never did, so reinstalling left a stale guard behind. jsonc-edit.mjs is the editor, moved verbatim behind one entry point. host-json-config.mjs is the merge, once: a single read that refuses to overwrite what it could not parse, a single atomic write, a single answer to "is this hook entry ours", and three commands over them — write, remove and merge-zcode. The ownership list is the uninstall side's, so an install now replaces a stale uri-guard.mjs entry instead of appending beside it, and the zcode fold reclaims by the same rule as the other three hosts rather than by a substring of its own. lib/install/ is the installer's code, not the plugins'. No shared module imports it, so it enters no vendoring closure and no lib/MANIFEST; one `./install/...` import from a module that hooks do import would put it in both, and install-lib-closure.test.mjs now fails on exactly that. Finding it is the one thing that is not a straight move. `--uninstall` runs before any source is resolved and the documented uninstall pipes this script from a URL, where there is no sibling directory to read — reaching for the checkout there would clone a repository just to remove hooks. So the assembled runtime keeps a copy of the directory beside the manifest modules, and install_lib_dir looks next to the running script, then there, then at the checkout. install-opencode-jsonc.test.mjs is six in-process cases over the editor — comments wherever they sit, trailing commas, a single-quoted value holding a brace and a `//`, an existing nested mcp object, idempotence, and a server the user disabled — plus the one end-to-end install that proves install.sh still hands the module the right arguments. The capability reference's install.sh line count goes with them; it was already wrong and this makes it wronger. * refactor(tests): share the mock server and the hook runner Seven test files carried their own copy of the same three helpers and the copies had drifted: two writeJson signatures, and two withMockOpenViking contracts, one of which called .catch() on the handler's return value and so could not take a synchronous handler at all. Three more carried a hook runner that turned a non-zero exit into a rejected promise — a result no test could assert on, which is why the async ZCode test spawns the hook by hand where it wants to read the exit code. testing/support.mjs already held buildConfigForTest and is the right home for the rest: it sits outside lib/, so sync.mjs vendors none of it into a shipped plugin, and a helper imported from a *.test.mjs file would have registered that file's own tests a second time. Two things the shared versions do that no copy did. The mock logs every request it saw and hands the log to the callback, which the three tests that only recorded pathnames now assert against instead of keeping an array of their own. And runHookScript never rejects: the exit code comes back like stdout does, and the call sites say expectExit where a clean exit is part of the expectation. writeJson takes the status last and defaults it to 200, so the two-argument callers read unchanged and claude-code's auto-capture moves its status to the end. claude-code's auto-recall is not one of the seven — it never carried readRequestBody — and keeps its two local copies. * refactor(tests): make the config and credential contracts shared The credential chain is one resolver every harness calls, but only codex tested it, so a change to the chain broke six harnesses and one suite reported it. That file moves to memory-plugin-shared, where the glob picks it up as the shared contract it always was. The five per-harness config suites had the same problem in reverse: each one re-tested the layer stack, the peer order and the cwd rule that plugin-config.test.mjs already holds every loader to, and buried the one or two cases only that harness answers. Three contract tests finish that file — a knob walked through every layer, a default told apart from a choice, and the workspace layer read from the cwd the loader is handed rather than the process's — and the per-harness suites keep what is theirs. Trimmed: claude-code and codex scripts/config.test.mjs, opencode and pi tests/config.test.mjs. Every case deleted from them is answered by a shared one in plugin-config.test.mjs, workspace-peer.test.mjs or the credentials contract this commit moves. dsh loses nothing: the cordis input is a layer no other harness has. Two things only a harness knows had no test at all, and now do: claude-code's tri-state digest mode, which it reads from the shared on/off switch under either env spelling, and codex's boolean reading of that same switch, where a compressor counts as configured only once it has been told what to run. * refactor(tests): test the MCP proxy contract once The proxy is one shared module, but its protocol contract was tested through two harness entrypoints — twelve cases under codex, one of them again under opencode — and both entrypoints re-exported the factory they import purely so a test could reach it. A change to the core meant editing two suites, and the opencode case differed from codex's only in which User-Agent string its fixture made up. The twelve cases and their fixture move to mcp-proxy-core.test.mjs, beside the two transport-error cases already there, and the file now has one proxy builder: it takes a fetchImpl, so an injected fetch and a real loopback upstream are the same harness. Neither host file keeps a case of its own — every one of the twelve exercises the shared core through the harness's config shape, which mcp-proxy-config.test.mjs already covers per harness. One case the split never had: a proxy with no local tool provider. Most harnesses run it that way, so the local-tool branches have to stay invisible — the listing is the upstream's, and a call named like a local tool is still the upstream's to answer. `examples/*/servers/*.test.mjs` now matches nothing. The pattern stays, for the next case that really is one host's, under nullglob so it contributes nothing rather than reaching node as a literal; opencode's test script drops it. opencode keeps its servers/mcp-proxy.mjs export map entry, which describes the entrypoint the package exposes, not the deleted test. * refactor(tests): test the shared runtime where it lives Three suites tested shared code from a harness directory, so a second harness reusing that code was covered only by accident. The recall timeout case, the queue cases that never touch cc's ov-session, and findLastHumanTurnIndex now sit beside the module they exercise. findLastHumanTurnIndex moves with its cases: it is a generic helper on the shared turn shape, and codex's capture-utils.mjs already re-exports everything shared, so its one caller needs no edit. * refactor(plugins): label each credential from the chain that resolved it Three doctors each re-walked ovcli.conf and ov.conf to say where the url, the key and the identity came from — the walk `resolveOpenVikingCredentials` had just finished. They were not the same walk. Claude Code and Codex rebuilt the key's origin by hand and ignored the `apiKeySource`/`credentialPath` every config now carries, so a key the chain took from one layer could be reported as coming from another; the thin-harness doctor read `apiKeySource` but then let ov.conf's harness block name the account under a pin the chain stops above. `credentialSources(cfg, cliConf, ovConf, { section })` lives in doctor-core now, with the section defaulting to the calling harness. The key's layer comes off `cfg.apiKeySource`; the files are asked only which field inside that layer holds the value, which is the one thing the layer cannot say — `plugin.<harness>. apiKey` from `api_key`, a harness block from `server.root_api_key`. The identity follows the chain step for step, plugin section included. The stage-5 guard could not see the three copies because the core never exported the name. It does now, so a fourth one fails that test. The two harness test copies move to doctor-core.test.mjs, beside the function they describe. * ci(plugins): generate OpenClaw shared runtime before tests Co-authored-by: TRAE CLI <traecli@bytedance.com> * docs(plugins): define hook and MCP development standard Co-authored-by: TRAE CLI <traecli@bytedance.com> * docs(plugins): publish bilingual development guide Co-authored-by: TRAE CLI <traecli@bytedance.com> * fix(plugins): restore runtime and distribution contracts Generate shared dependencies before validation and source installs. Keep Codex retries owned by the transcript cursor and defer threshold commits until the tail is delivered. Enforce the shared hook enable gate, restore auth-mode environment precedence, and preserve pi request options. Co-authored-by: TRAE CLI <traecli@bytedance.com> * refactor(plugins): use a host-neutral hook manifest Move the shared hook integration metadata out of the Claude-specific directory and update version checks, diagnostics, installation verification, archive validation, tests, and documentation. Co-authored-by: TRAE CLI <traecli@bytedance.com> --------- Co-authored-by: TRAE CLI <traecli@bytedance.com> |
||
|
|
2a38736eb2 |
feat(admin): 异步清理账号数据 (#5015)
* feat(admin): asynchronously clean up deleted accounts Disable accounts before queueing cleanup so requests return promptly and failed deletions remain retryable. Share the user cleanup queue, settle existing work, and exhaust vector records in batches of at most 100. * fix(storage): tolerate index lag during account cleanup Enumerate vector IDs before deleting them in batches of at most 100. Use a temporary file to bound memory and stop treating stale counts or read-back results as delete failures. |
||
|
|
b7bd0b5d64 |
feat(memory): add account-scoped memory template configuration (#4770)
* feat(memory): add account-scoped memory template configuration * feat(memory): support managed account content templates Validate and safely render editable content templates for events, soul and identity. Enforce a 50000-Unicode-character limit for editable descriptions, with boundary tests and API documentation. * fix(ci): align API catalog and copy overwrite tests * feat(memory): allow safe string methods in account templates Replace the unreleased account content filter contract with zero-argument upper/lower/strip methods on plain strings. Guard receiver types before attribute lookup, align the Events default and docs, and cover method safety plus API-to-body rendering. * fix(memory): preserve inherited deployment content templates * fix(memory): reject cross-template file conflicts before writes * fix(memory): treat account descriptions as plain text * fix(memory): publish templates atomically without read locks * fix(memory): unify restricted description template rendering * fix(docs): escape Jinja examples in admin API docs * refactor(memory): simplify description renderer inputs |
||
|
|
e04f4360df |
feat(metric): export ragfs metrics to python observability system (#5041)
* feat(metric): export ragfs metrics to python observability system * feat(metric): adjust scrape ttl second config |
||
|
|
18fa9573ce |
feat: support remote vikingdb glob (#4586)
* feat: support remote vikingdb glob Co-authored-by: TRAE CLI <traecli@bytedance.com> * Add glob benchmark and default to fs Co-authored-by: TRAE CLI <traecli@bytedance.com> * fix: address remote glob review feedback Co-authored-by: TRAE CLI <traecli@bytedance.com> --------- Co-authored-by: TRAE CLI <traecli@bytedance.com> |
||
|
|
a9d908e131 |
fix(storage): 缩小写锁范围并跳过父目录刷新冲突 (#5014)
* fix(storage): 避免批量写入阻塞同目录下的无关文件 * test(storage): 精简批量写入的测试改动 * fix(storage): 父目录刷新冲突不阻断文件写入 保留变化文件的语义和索引处理,父目录忙时跳过聚合;目录摘要未落盘时不发布目录向量。 |
||
|
|
8f20fbff02 |
fix(docker): keep relative workspaces inside the persistent mount (#4961)
Co-authored-by: r266-tech <r266-tech@users.noreply.github.com> |
||
|
|
35c984c449 |
feat(pathlock): pathlock support cache runtime (#4918)
* feat(pathlock): pathlock support cache runtime * perf(pathlock): shard Redis lock hashes by account Route global, system, and account paths to separate HASH keys while retaining namespace slot affinity. Reject cross-scope batches and preserve lock lifecycle behavior within each scope. * fix(pathlock): fix batch Redis stale token deletions too many results to unpack problem * fix(test): update pathlock cache provider docs * fix(pathlock): After the Redis lock times out, the incremental update reverts to an incorrect lock scope. |
||
|
|
7df74141c0 |
fix(queuefs): make completion status backend-owned (#5000)
Co-authored-by: zhourunyun <zhourunyun@bytedance.com> |
||
|
|
9eeeda4e1b | 更新下 skills 相关的doc (#5003) | ||
|
|
4c014ab346 |
docs: 清理显式等待的调用示例 (#4998)
* docs: 清理显式等待的调用示例 * docs: 省略 Go 示例中默认关闭的等待参数 --------- Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com> |
||
|
|
2d774d835c | docs: refresh WeChat group QR code (#4995) | ||
|
|
9b5f43d828 |
fix(parse): 保留 Understanding 上传文件的已识别扩展名 (#4981)
* fix(parse): preserve detected extensions in Understanding uploads * test(parse): trim upload filename regression coverage --------- Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com> |
||
|
|
fafccb1c2b |
feat(skills): 支持服务端导入 Git 技能 (#4957)
Co-authored-by: qin-ptr <222410531+qin-ptr@users.noreply.github.com> |
||
|
|
9a408aa5ef |
docs: regroup integration tables and rename helper section (#4951)
* docs: regroup integration tables and rename helper section * docs: shrink integration cell captions * docs: restore caption size, shorten Hermes caption * docs: normalize integration logo sizes, tighten pi logo viewBox * docs: add normalized integration logo set * docs: refresh studio hero with light and dark variants * docs: use OpenAI mark for the Codex entry * docs: add DSH and Doubao Work to the integration table |
||
|
|
a11a1cde81 | docs: refresh WeChat group QR code (#4950) | ||
|
|
b3104fbd0c |
fix(parse): preserve source filenames in Understanding uploads (#4947)
Pass prepared source identity through every local upload entry point so HTTP temporary basenames do not leak into parser artifacts. |
||
|
|
b1c0bef43a |
feat(reset-context): on session commit clear, reset context without changing session identity (#4937)
* fix(openclaw): clear reset context without changing session identity * fix(sessions): unblock commits after reset boundary write failure * refactor(sessions): read reset boundary from .done and skip duplicate empty resets - _is_context_reset_archive reads the context_reset property directly instead of inferring it from a missing overview - reset on an already-reset empty session returns confirmation without appending another empty archive directory * refactor(sessions): reset boundary archive holds only .done Terminal archives never have messages.jsonl read and a missing overview already reads as empty, so the empty placeholder files were unused. |
||
|
|
4f430f65fe |
fix(cli): 统一 add-skill 与 skills add 的导入流程 (#4939)
复用已有 Git 技能发现、选择和上传能力,避免两个 CLI 入口行为分叉。 移除技能导入命令的 timeout 选项,保留现有服务端处理及 wait 语义。 |
||
|
|
4375f5e9ac |
docs(readme): add integration logos and fix publication formatting (#4929)
* docs(readme): fix publication citation line breaks * docs(readme): present agent integrations in a logo table * docs(readme): align integration logos across each row * docs(readme): shorten Claude label and link integration details * docs(readme): add DeerFlow and general integration logos * docs(readme): keep integration captions and mode names on one line * docs(readme): prevent CJK MCP labels from wrapping * docs(readme): remove redundant MCP integration note |
||
|
|
bf3062975a | docs: refresh WeChat group QR code (#4930) | ||
|
|
f2832b7b9b |
Feat/configurable executor workers (#4913)
* feat(server): configure asyncio executor workers Co-authored-by: TRAE CLI <traecli@bytedance.com> * refactor: clarify executor thread configuration Co-authored-by: TRAE CLI <traecli@bytedance.com> --------- Co-authored-by: TRAE CLI <traecli@bytedance.com> |
||
|
|
a57d7f941a |
feat(tasks): show persisted execution events in Studio (#4864)
* feat(tasks): persist bounded execution events in task snapshots * feat(tasks): expose execution history on detail requests * test(tasks): cover persisted event ordering and durability * test(tasks): verify event API ownership and opt-in responses * feat(studio): define and format recorded task events * fix(studio): replace synthetic logs with persisted task events * chore(studio): add the task event query option * feat(studio): localize execution events in English * feat(studio): localize execution events in Chinese * docs(tasks): document persisted execution events * docs(tasks): document execution events in Chinese |
||
|
|
b50431770e |
feat(auth): batch register trusted identities (#4861)
Co-authored-by: TRAE CLI <traecli@bytedance.com> |
||
|
|
a386269d38 |
fix(resources): 明确等待超时语义并改用任务轮询示例 (#4888)
等待超时返回任务 ID 和状态查询提示,避免误认为后台导入失败;主要文档和示例改为提交后查询任务状态。 |
||
|
|
98f7dfe0e3 |
docs: remove duplicate CLI API key block (#4878)
Co-authored-by: linweiye <linweiye.voph@bytedance.com> |
||
|
|
708dba60bd |
feat(plugins): configurable regex input filters for recall queries and captured turns (#4858)
* feat(plugins): ordered regex input filters for plugin input The memory plugins feed the raw user prompt into recall and the raw transcript into capture. Nothing between the harness and the server lets an operator shape that text: a thinking-keyword prefix (`ultrathink ...`) goes into the search query verbatim, a slash command becomes a memory, and a token pasted into a prompt is stored as it was typed. The only user-configurable pattern today is `bypassSessionPatterns`, which matches a session id or a cwd, never the text; every text rule -- `ACK_RE`, `SLASH_COMMAND_RE`, `sanitizeCapturedText` -- is hard-coded. `lib/input-filters.mjs` adds the missing layer: an ordered list of sed-style rules, `s` to substitute, `d` to drop the text on a match and `k` to keep it only on a match, each optionally scoped to one role. The rules are plain strings so they survive the existing string-list config coercion (env CSV, `ovcli.conf` array) without a new config type. Nothing in the module throws: an unparsable rule, an unknown flag or a pattern RegExp rejects becomes an entry in `errors` and is skipped, because a typo in a config file must not take a hook down. Compilation is memoised per rule list so a long-lived host compiles once, and rule count and pattern length are capped. The time budget is deliberately advisory -- `applyInputFilters` reports `slow`/`elapsedMs` and runs every rule anyway, since skipping the tail of the list would skip exactly the redaction rule an operator added. `capture-utils.mjs` grows the capture-side seam. `filterCaptureParts` prunes blank text parts as before, then takes one drop verdict per turn on the aggregate of its text parts and rewrites the individual parts with the substitutions only, so a rule can never both keep and drop the same turn; tool call/result payloads are carried through unmatched, though a turn dropped on its text takes them with it. `shouldCaptureText` gains the same step for the text path, behind a `filters` option that `extractCaptureTurns` turns off when parts are on the wire -- one application per stored string, and `turn.text` stays faithful for callers that scan it for trigger words. No loader supplies either knob yet, so this commit changes no behaviour: with `captureFilters` unset every path is the identity it was. * feat(claude-code): recallQueryFilters / captureFilters Wires the shared filter engine into the Claude Code plugin. Both knobs are string lists shaped exactly like `bypassSessionPatterns`: an env CSV replaces the configured array wholesale, entries are trimmed and non-strings dropped, and a rule that needs a literal comma has to come from the `ovcli.conf` array because the env value is split on one. Recall filters run in `auto-recall.mjs` immediately above the `minQueryLength` gate, so a prompt whose only content was a stripped prefix reports `short_query` rather than searching for the empty string, and a dropped prompt gets its own `last-recall.json` reason, `query_filtered` -- distinct from `filtered_out`, which is the score threshold. The statusline only tests for `ok`, so it needs no change. Capture filters run at the send site rather than in the extractor, because the cursor CC persists is an index into the extracted turn list: dropping a turn earlier would leave `capturedTurnCount` describing a list the next run does not reproduce. `sanitizePartsForSend` in `auto-capture.mjs` and the equivalent in `subagent-stop.mjs` are the two places every payload passes through, and both now delegate their blank- part pruning to `filterCaptureParts`, which keeps that behaviour when no rules are configured. `ov-memory-doctor` lists the active rules per knob and warns, with the exact parse or RegExp error, about the ones it could not compile -- config errors have to be visible somewhere other than a debug log, because a skipped rule is silent by design. The README gains an "Input filters" section with the grammar, six worked examples (every one of them asserted in the shared engine test), an ovcli.conf sample, and the caveats that actually bite: the env comma split, that drops win over substitutions, that filters run before the built-in ack/slash heuristics, that a mid-session drop rule shortens the turn list and reads as a transcript rewrite, and that nothing already stored is rewritten. * feat(codex): recallQueryFilters / captureFilters The same two knobs on the Codex side. The loader gains the string-list helper the harness did not have yet -- an env CSV replaces the configured array wholesale, both paths trim and drop non-strings -- and reads `cx.recallQueryFilters` / `cx.captureFilters`, so `ovcli.conf` `plugin.codex`, `plugin` and the legacy `ov.conf` `codex` block all work without further change. Recall filters go in `auto-recall.mjs` immediately above the `minQueryLength` gate: a dropped prompt emits the empty envelope before the health check, so a filtered turn costs no request at all. Capture needs no edit here. Codex's four write hooks funnel through `catchUpTurns` into the shared `extractCaptureTurns`, which is where the filters already run -- and it has to be there rather than at the send site, because this cursor advances by payloads durably sent: dropping a turn later would leave the cursor short and replay the surrounding turns on the next hook. The test covers exactly that, running the same transcript twice and asserting the second pass finds nothing new. `ov-memory-doctor` reports the active rules and the compile errors, and the README documents the grammar, both env examples, the ovcli.conf array for rules that need a literal comma, and the ordering and mid-session caveats. * docs(agent-integrations): input filter env vars The Claude Code integration guide's env table is where most people look first, so the two new knobs belong there next to the bypass patterns they resemble. The grammar and the worked examples stay in the plugin README, which the table already links to. * refactor(plugins): drop machinery the input filters do not need A review pass against the actual requirement — let an operator configure a regex over plugin input — found five constructions that solve problems this feature does not have. All of them go: - The compile memo cache, its FIFO eviction and the test-only reset. It was justified as "a long-lived host compiles once", but the only hosts that supply these knobs are the Claude Code and Codex hooks, which are short-lived subprocesses; the per-turn capture path is the one repeat caller, and compiling five rules measures 3 microseconds. The cache cost more code than it saved work. - `maxRules` / `maxPatternLength` as injectable options nothing passed, and the caps themselves. Rules come from the operator's own config, so a 33rd rule or a 600-character pattern is self-inflicted and harmless, and `bypassSessionPatterns` — the same shape of knob, also operator regexes — has never capped anything. - The advisory time budget: `slow` / `elapsedMs` and the injectable `now` whose only non-default caller was the test that made it look slow. The pattern that actually hurts is one that backtracks, and that hangs rather than reporting a number. - The try/catch around rule application. Every hook already ends in `main().catch(...)` that logs and approves, so an exception here was never going to take a session down. The try/catch around `new RegExp` stays: that one parses operator input, and the doctors render its message. - The warnings channel, which existed to carry one advisory string. Stripping `g` from a `d`/`k` rule is load-bearing — `.test()` on a global regex advances `lastIndex` and would alternate between calls — but it needs no announcement, since the rule then means exactly what it looked like it meant. `filterCaptureParts` also returned `ruleIndex` / `op` / `slow` that no caller read, and `extractCaptureTurns` tested `decision.reason === "filtered"` inside a condition where that disjunct could never be the deciding one: `filters` is passed as `shaped.parts.length === 0`, so the reason can only appear when the other half is already true. Docs lose the retired caps and a duplicated note; the Claude Code recall test loses the case Codex's suite already covers. Engine 303 -> 218 lines, behaviour identical for every rule an operator can write. * docs(agent-integrations): point the filter knobs at their grammar The env-var rows added to the Claude Code guide named the two knobs but said nothing about how to write a rule, and described only the comma-separated environment form — which reads as if that were the only way to configure them. Both rows now link to the plugin README's Input filters section, and a note under each table says the knobs also live in `ovcli.conf` under `plugin.<harness>.<key>` or the shared `plugin.<key>` as a JSON array, which is the better form: the env vars are split on commas, so a rule containing a literal comma can only be written in the array. The grammar itself stays in the plugin READMEs, where these guides already send readers for the full env-var list. The Codex guide had no mention of the feature at all; it gets the same two rows, the same note, and a link to its own README. The capability reference carried counts this feature invalidated: `memory-plugin-shared/lib/` is 24 modules rather than 23, the hook set `sync.mjs` gives every hook-driven plugin is 13 rather than 12, and every per-target total shifts with it (dsh 15, opencode 17, zcode 19, claude-code and codex 22 each). pi's entry was already wrong before this change — it takes `batch-send` as well as `setup-wizard`, so 15, not 13 — and is corrected in the same sentence. `input-filters.mjs` joins the module table, and the `capture-utils.mjs` row now says "built-in capture heuristics" so the two rows do not both read as "capture filtering". * docs(configuration): document the ovcli.conf plugin section `docs/*/configuration/02-client.md` is titled "ovcli Configuration" and covers connection, command behaviour, upload filters and the workspace layers — but never described the `plugin` section itself. It appeared only twice in passing: two rows of the workspace precedence table, and one sentence about `peerSource`. The nearest thing to a general explanation was buried in the agent-integrations overview, inside a discussion of recall latency. That gap is why the input filter note added in the previous commit had to explain the section's shape inline for two knobs, which reads as an oddity rather than a reference. The section is now documented once, where someone editing `ovcli.conf` would look: what `plugin` and `plugin.<harness>` mean, that keys are the camelCase counterparts of the `OPENVIKING_*` tuning variables and that the mapping does not go both ways, that list-valued knobs are JSON arrays here while their environment counterparts are comma-separated, the full resolution order, when an edit takes effect versus when the agent has to restart, that only Claude Code and Codex read it today, and that `ov-memory-doctor` flags unrecognised keys. The per-knob lists stay in the plugin READMEs, which the section links. The two integration guides now point at it in one line instead of restating it, the workspace precedence table links the section it already named, and the complete example grows a `plugin` block. |
||
|
|
7eed9adf75 |
refactor(compile): 改用 instruction 并兼容 reason 参数 (#4852)
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com> |
||
|
|
e41b2467f0 |
fix(vlm): decouple reasoning effort from OpenAI model defaults (#4690)
* fix(vlm): route configured reasoning_effort through extra_body for non-OpenAI models _is_reasoning_model() gates the native reasoning_effort kwarg on the gpt-5/o-series prefixes, so for any other model name a configured vlm.reasoning_effort was silently dropped from the request. OpenAI- compatible providers then ran at their server-side default; for the GLM-5.3 family that default is max, turning session-commit extraction into minutes-long calls (#4686). Route an explicitly configured reasoning_effort through extra_body when the model is not an OpenAI reasoning family: - only injects when the user actually set the field (a defaulted value is never invented), so backends that never configure it see no change - an explicit extra_request_body.reasoning_effort always wins - DashScope endpoints keep the enable_thinking mechanism untouched - gpt-5/o-series keep the native top-level kwarg GLMVLM inherits the routing (issue MRE now sees the value reach the API). Fixes #4686 * refactor(vlm): simplify reasoning effort routing Use mutually exclusive provider handling and setdefault for explicit body precedence, reading configuration presence without a duplicate flag. * refactor(vlm): separate configured reasoning effort from OpenAI defaults Build shared completion parameters once for text and vision, forwarding explicit reasoning effort independently of model prefixes and thinking. Preserve OpenAI defaults and extra-body precedence, and consolidate the contract coverage into existing tests. --------- Co-authored-by: now-ing <now-ing@users.noreply.github.com> Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com> |