549 Commits
Author SHA1 Message Date
t0sakiandTRAE CLI 276dfffc80 fix(codex-plugin): exclude host startup context from captured sessions (#5392)
* fix(codex-plugin): skip host startup context during capture

* fix(codex-plugin): handle legacy AGENTS startup headers

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-25 17:33:57 +08:00
t0saki 3e02c6ac29 fix(dsh-plugin): drop the plugin group from the bundle patch (#5390)
DSH's plugin manager lists every bundle row, group rows included, but
resolves row state and toggles through the running-plugin inventory,
which skips group entries. The outer `@deepseek-ai/cordis-plugin-group`
row therefore always showed as disabled, and enabling it failed with
`unknown-plugin`.

The group only isolated the `openvikingMemory` service, which nothing
consumes, so the bundle now inserts the runtime row directly under the
same row id. Desktop toggles and id-targeted overrides keep applying,
and the docs show the id-targeted override form, which also applies to
the old nested layout.
2026-09-25 16:13:46 +08:00
z1gonandstarship-s cef8be2294 fix(hermes-plugin): bind settings to initialized profile (#5374)
* fix(hermes-plugin): bind settings to initialized profile

* fix(hermes-plugin): keep launch secrets scoped across routed profiles

* fix(hermes-plugin): retain frozen launch secrets under multiplex

---------

Co-authored-by: starship-s <45587122+starship-s@users.noreply.github.com>
2026-09-25 14:58:34 +08:00
z1gon 4e5c873a46 fix(plugins): align Kimi manifest version with integration (#5376) 2026-09-25 14:45:37 +08:00
Johnny 597ef15750 feat(dsh-plugin): add the plugin card's icon and localized descriptions (#5362)
The card resolves its text and icon through the package `exports` map, so a
manifest exporting only `.` left it with neither: export `./package.json` and
`./locale/*.json`, declare `icon: ./icon.svg`, ship both in `files`, and add
the English and Chinese dictionaries. The mark draws in one ink per colour
scheme (black on light surfaces, white on dark) rather than a fixed gradient,
so it stays legible on the card's dark background.

The peer ranges also admit the `0.1.7-rc` series from `rc.2` on, a pre-release
series semver otherwise rejects.

Version 0.5.7, as the repository's version-bump check requires.
2026-09-25 14:01:24 +08:00
t0sakiandTRAE CLI 4e5136e440 docs(dsh): clarify recall budgets and category quotas (#5381)
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-25 12:02:58 +08:00
t0sakiandTRAE CLI 7fcb42377c fix(dsh): resolve workspace peer settings per session (#5380)
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-25 12:02:40 +08:00
z1gon 4edc30b068 fix(plugins): filter hook capture before truncation (#5375)
* fix(capture): filter hook-host turns before truncation

(cherry picked from commit 6b05eb47a529eb49b53f4a2bdf9ee574453ad2cf)

* chore(plugins): bump versions for shared capture change
2026-09-24 23:39:57 +08:00
z1gonandhemingzhe 92dcf00f83 fix(plugins): share capture filtering across adapters (#5359)
* fix(pi): honor conversation capture filters in faithful mode

Apply the shared turn filter before capture decisions so configured rules cannot be bypassed by takeover or attached tool parts, while preserving tool values and behavior without applicable rules.

(cherry picked from commit e8fb6c9476)
Signed-off-by: Hao Zhe <haozhe4547@gmail.com>

* fix(opencode): wire captureFilters through config and the capture path

captureFilters (sed-style redaction rules, e.g. s/sk-.../[REDACTED-SK]/g)
was defined in the shared capture-utils library but never reached the
OpenCode capture path: lib/config.mjs had no mapping for the key and
memory-session's buildCapturePayload applied no filtering, so secrets
typed into OpenCode sessions were captured verbatim. The Codex plugin
applies the identical library correctly (issue #4984).

- map captureFilters in config (file key, OPENVIKING_CAPTURE_FILTERS env
  override as JSON, non-string entries dropped)
- apply filterCaptureParts() to extracted parts in buildCapturePayload,
  mirroring the Codex reference flow (role-level drops skip the turn;
  an empty filtered result gates capture)
- cover config mapping, env override and validation in tests

(cherry picked from commit b5bd21930d)
Signed-off-by: Hao Zhe <haozhe4547@gmail.com>

* refactor(capture): share sanitized filter path across adapters

* test(capture): preserve mixed tool messages and cover OpenCode v2

* fix(capture): cap mixed text and preserve dated logs

* fix(capture): use shared shaping in Claude Code hooks

* fix(capture): sanitize fallback text once

---------

Signed-off-by: Hao Zhe <haozhe4547@gmail.com>
Co-authored-by: hemingzhe <hehesmilett@163.com>
2026-09-24 22:46:20 +08:00
z1gon 53360209b9 fix(hermes): rotate read-only session state for recall (#5372) 2026-09-24 22:21:27 +08:00
HMYDK 3b9928ee40 fix(hermes): skip writes for non-primary agent contexts (#5353)
The provider ignored the host-provided agent_context, so sessions started
for scheduled cron jobs, delegated subagents, and flush forks recorded
turns and committed into OpenViking exactly like interactive sessions.
Fixed-prompt output landed in the memory bank as user context and every
scheduled run paid summarization plus extraction.

Read agent_context in initialize() (default "primary", so hosts that
predate the keyword keep the previous behavior) and skip sync_turn,
on_session_end, on_session_switch, and on_memory_write for the
non-primary set. Recall and prefetch are unchanged.

Fixes #5345
2026-09-24 17:23:11 +08:00
ligjnandligjn fc0916bf3e fix(opencode-plugin): run session inject and recall in parallel on chat.message (#5149)
opencode awaits chat.message before persisting/broadcasting the user
message, so the serial session-inject -> recall waterfall stacks remote
latency onto message display (3-5s on the first message of a session
with a remote embedding endpoint; #5148). The two injections are
independent after hook entry; overlap them the way the dsh plugin
does (#4643).

Co-authored-by: ligjn <ligjn@users.noreply.github.com>
2026-09-24 17:22:34 +08:00
dvd233 37390e6d71 fix(dsh): respect broad MCP recall scope (#5346) 2026-09-24 17:21:27 +08:00
dvd233 c63785b60a fix(opencode-plugin): discard captured message parts from state snapshots (#5179) 2026-09-24 16:22:56 +08:00
t0sakiandTrent Telfer 12076389da feat(opencode-plugin): support OpenCode v2 (#5341)
* feat(opencode-plugin): support the OpenCode v2 plugin API

OpenCode 2 does not run v1 hook plugins. Keep the existing server()
entrypoint and add setup() so one package serves both, adapting MCP,
recall, capture, and lifecycle events to the v2 shapes.

Refs #5226

* fix(opencode-plugin): align v2 lifecycle handling

* docs(opencode-plugin): document v2 support

* fix(opencode-plugin): preserve capture across v2 compaction

* fix(opencode-plugin): drop reasoning and keep state on failed v2 executions

v2 capture turned reasoning blocks into assistant text, so chain-of-thought
reached memory extraction and inflated pending tokens. v1 parts and the shared
capture filter drop reasoning; v2 now does the same.

A failed v2 execution ends one turn, not the session. Mapping it to v1's
session.error committed and deleted the session state, so a later capture
without a cursor resent every earlier turn. Failed executions now take the
same idle path as succeeded and interrupted ones.

* fix(opencode-plugin): scope v2 capture to the plugin location and persist its cursor

One OpenCode v2 service runs a plugin instance per location, and the plugin
event stream carries every location's events. Each instance therefore
captured and committed every session, storing each message once per open
project. Handle only lifecycle events whose session belongs to this
location, resolved from session.created or ctx.session.get because execution
events carry no envelope location.

The capture cursor lived only in memory, and the shared session state file
is rewritten by every instance, so a reloaded or evicted instance could
resend the transcript since the last compaction. Keep the cursor in plugin
storage and remove it when the session is deleted.

* docs(opencode-plugin): describe v2 commit points and location scoping

---------

Co-authored-by: Trent Telfer <4094016+ttelfer@users.noreply.github.com>
2026-09-24 15:35:07 +08:00
Zayn Jarvis 9ecb2863a7 fix(openclaw): stop offering the legacy person peer role at install time (#5355)
Setup (--peer-role, the interactive prompt) and the installer
(--peer-role, OPENVIKING_PEER_ROLE, the interactive prompt) now reject
"person" and say it was renamed to "sender". Existing configs that still say
peer_role=person keep working: config parsing, the plugin manifest enum and
reads of the current config are unchanged.
2026-09-24 11:44:11 +08:00
Zayn Jarvis 8a0d362647 fix(openclaw): widen to unscoped recall when peer_role=sender has no sender (#5347)
* fix(openclaw): widen to unscoped recall when peer_role=sender has no sender

OpenClaw does not pass runtimeContext to context-engine assemble(), and cron,
heartbeat and webchat turns carry no sender. With peer_role=sender the plugin
threw "requires a sender identity" there, so auto-recall failed every turn and
tools without a sender errored.

Peer scoping is soft isolation: a missing sender now warns and continues as an
unscoped request (no X-OpenViking-Actor-Peer), like the MCP proxy (#5132).
memory_forget refuses in that state, since an unscoped search could pick and
delete another sender's memory. Capture already stored no peer_id when the
sender is missing and is unchanged.

* fix(openclaw): let memory_forget widen like find when the sender is missing

Peer scoping is soft isolation; forget follows the same unscoped fallback
as recall and find instead of refusing.
2026-09-24 11:43:50 +08:00
Zayn Jarvisandsomewhere1994 20a97022d5 fix(agent-hook): stamp the effective peer into captured messages (#5343)
Cursor, TRAE, ZCode and Kimi Code capture through addAgentMessages and sent
their peer only as the X-OpenViking-Actor-Peer header. Session routes never
read that header (they use get_session_request_context), so messages landed
without a peer and their memories went to the user-level layer instead of
peers/<peer>. Claude Code and Codex already put peer_id in the body.

addAgentMessages now takes the peer and stamps it on payloads that do not
name one; the hook passes the same effective peer the header carries. When
peer mode is off the peer is empty and nothing is stamped.

Co-authored-by: somewhere1994 <108641179+somewhere1994@users.noreply.github.com>
2026-09-24 11:42:23 +08:00
ydflowandydflow de1c5c4954 fix(dsh-plugin): pass recallExcludeUris so subtrees can be excluded from recall (#5312)
`recall-core.mjs` reads `options.excludeUris` and forwards it as the search
request's `exclude_uris`, but the DSH runtime built its recall options without
that key, so no configuration could stop a subtree from being recalled. The
generated per-directory context files (`viking://user/<space>/skills`,
`viking://user/<space>/resources`, `viking://agent/skills`) came back as ordinary
hits and carry only boilerplate text — on a vague prompt, 3 of 7 returned entries
were these files. The only remedy was deleting the data.

Add a `recallExcludeUris` list knob to the shared config schema and pass
`cfg.recallExcludeUris` through as `excludeUris` from the DSH recall call, which
is the single place that builds those options. The schema entry lands in
`memory-plugin-shared/lib` and is propagated to the claude-code and codex copies
by `sync.mjs`; those two plugins are marked `committed: true` there because a
host installs them from a directory in this repository, so their vendored copies
belong in git. `recall-core.mjs` already caps the forwarded list at 200 entries
and omits the field entirely when the list is empty, so the default behaviour and
the request body are unchanged.

Validation, from `examples/dsh-memory-plugin` after
`node ../memory-plugin-shared/sync.mjs` and `npm install`:
`node --test *.test.mjs` 77 tests, 76 passed, 0 failed, 1 skipped. The new
`recallExcludeUris reaches the search request` failed before the change with
`exclude_uris` undefined in the request body and passes after it; the companion
case asserts no `exclude_uris` field is sent when the knob is unset.
`node --check` passes on all three changed source files.

`examples/claude-code-memory-plugin` fails 6 tests in
`scripts/auto-capture.test.mjs` on this Windows machine. Those tests spawn a real
subprocess that talks to a mock server on 127.0.0.1; they fail identically with
this change stashed and with a pristine checkout, so they are pre-existing and
environmental rather than caused by this change.

Co-authored-by: ydflow <314143294+ydflow@users.noreply.github.com>
2026-09-23 22:56:07 +08:00
wangyuandwangyu134 14a7b8126e fix(plugins): find the installer runtime in the fetched checkout (#5280)
ensure_checkout's SRC_ROOT assignment dies with the command substitution that
calls it, so install_lib_dir never sees the checkout under REPO_DIR. OpenCode
is the only host that reads the installer's JavaScript from there, so it alone
failed with "Installer runtime not found"; REPO_DIR is now a candidate.

Co-authored-by: wangyu134 <wangyu134@58.com>
2026-09-23 22:47:57 +08:00
t0sakiandcocolord 4196550942 fix(pi): make takeover archive-safe and recoverable (#5321)
* fix(pi): keep covered system messages during takeover

transformContext() sliced covered system messages off with the user
turns, dropping the model's tool declarations and instructions on
pi >= 0.86, where the leading system message carries the base prompt and
tools and later system messages carry mid-conversation tool changes,
section updates and appended instructions. The overview stands in for
the archived conversation only, never for the system state.

Keep every system message in [0, boundaryIdx) in front of the overview
in original order; leave a system message inside the retained tail where
it is. On 0.80.3 there are no system messages in the branch, so this
preserves nothing and the behaviour is unchanged. On 0.87 the host
reconciles declared tools against the executable set each request, so
keeping the declarations introduces no duplicate.

Regression tests assert tool loadout and system prompt across pi's real
merge (@earendil-works/pi-ai getCurrentTools/getCurrentSystemPrompt),
resolved from any pi install and skipped on pi < 0.86 where there is
nothing to preserve.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(pi): align takeover archives with trim boundaries

Sync and drain the current branch before committing, calculate keep_recent_count from the actual capture projection, and freeze the exact boundary while asynchronous archive summaries are pending. Only a successful archive with its own non-empty overview may advance context trimming.

Persist capture gaps caused by permanent delivery failures, block takeover across those gaps, and make native compaction fail open while retaining Pi’s first-kept boundary.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* feat(pi): add takeover archive recovery hints

Persist the exact archive and history URIs and append a recovery footer after the bounded overview. Only advertise list/read when both tools are active; grep remains optional and captured history is not described as the full raw transcript.

Document the delivery and pending-archive behavior and bump the stable Pi extension to 0.4.1.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* fix(pi): anchor the takeover boundary on a pi entry id

The boundary was frozen on getBranch() entries as a user-turn count plus
the fingerprint of the entry in front of it, then applied to the context
hook's messages. getBranch() also holds takeover's own ov-takeover state
entries, which sit in front of every next user turn, model changes and,
after a pi compaction, the compacted-away prefix; the context messages
hold none of them. The fingerprint disagreed on the very next request and
the boundary reset, so takeover committed archives but never trimmed.
After a pi compaction the turn count overshot the context as well.

Make the boundary the id of the entry the covered prefix ends at. Freeze
and apply it on pi's context projection of the branch (compaction-aware,
context edits applied) and map the first kept user entry onto the hook's
messages by timestamp. Messages between the boundary and that turn - a
run that went on after a keepRecentTurns 0 commit, a branch summary /tree
left at the boundary - are covered by no archive and stay. A boundary
outside the active context sends the full context and is kept for a
return to that branch. 0.4.0 state is adopted on the first context hook,
and the pending archive shrinks to the archive and its boundary entry.

Persist takeover state on transitions and at shutdown instead of every
turn, since each entry carries the overview.

With takeover on, startup drained the current session and never replayed
other sessions' queued entries. Drain this session first, then hand the
rest to the generic replay once none of this session's entries remain for
it to drop untracked.

tests/takeover-session-manager.test.mjs drives the core against pi's real
SessionManager. It fails on the previous head; it passes on pi 0.87.0,
and its state-entry, compaction and tree-navigation checks also pass on
0.80.3 and 0.86.1.

* fix(pi): keep takeover inside the host's handler budget

pi hosts cap every extension event handler at 30s; omp logs "handler
timed out after 30000ms", drops the result and lets the handler run on
(#5275). Takeover polled the archive summary inside turn_end - 15 reads
2s apart, 28s before any real work - and a summary that never came
(phase 2 failed, or Working Memory disabled on the server) left a pending
archive that repeated that wait on every later turn.

Nothing waits for a summary in turn_end any more. The commit's overview
is read once; a pending archive is read once per later turn_end and once
in before_agent_start, so a summary finished between prompts or pi -p
processes trims the next request. When the commit's task has ended, or
the server no longer knows it, and a last read still finds nothing, the
pending archive is dropped and its frozen token pressure spent. A pending
archive whose boundary left the active context is dropped without a
read.

Each handler gives takeover a 25s deadline from its start. The drain gets
the time the commit does not need, the commit gets the time one read does
not need, and with less than 10s left the commit waits for a later turn.
The compaction handler still polls, because pi needs its summary now,
but only until the deadline; when it hands compaction back to pi it no
longer resets the boundary, since pi may yet cancel or fail its own
compaction. The drain's default budget drops from 60s to 10s, as startup
replay runs inside before_agent_start too.

Refer to boundaries saved by 0.4.1 and earlier as count-based, since the
released 0.4.1 still writes them, and bump the extension to 0.4.2.

* fix(pi): tell a finished archive by the server's own markers

A pending archive is dropped once no summary can come any more. That was
decided through GET /api/v1/tasks/{id}, but task records expire and a
server replica may not know another's task. The server keeps an archive's
terminal state in the archive itself instead: .done, written last once
commit phase 2 completed (after the Working Memory when that is enabled,
recording working_memory_enabled=false when it is not), and .failed.json
once phase 2 failed for good. Read those markers, as #5320 does to decide
completion, and drop the task lookup.

Co-authored-by: cocolord <17559402+cocolord@users.noreply.github.com>

---------

Co-authored-by: cocolord <17559402+cocolord@users.noreply.github.com>
2026-09-23 22:47:51 +08:00
z1gon c2c6407314 fix(hermes): mirror native memory replacements and removals (#5281)
* fix(hermes): mirror native memory replacements and removals

Port the provider implementation from NousResearch/hermes-agent#100187 at 32f75a9e6728a9a3d2f50a870dab3715a1f34fd7, which continues #85860. Keep the existing profile and connection-generation fixes, use relative imports and context-preserving workers, and cover the external loader and native-memory bridge.

* test(hermes): retain native memory mirror regression coverage

* fix(hermes): require committed entry identity and report indexing failures

* fix(hermes): clarify asynchronous add indexing status
2026-09-23 17:36:05 +08:00
t0saki 12748051a6 chore(dsh-plugin): bump to 0.5.2 to publish the format v4 fix (#5335) 2026-09-23 17:33:04 +08:00
dvd233 914078f7cc fix(dsh): use producer-owned source kind (#5318) 2026-09-23 17:05:38 +08:00
z1gonandliuhao1024 9f21385993 feat(hermes): add gateway memory presets and sender attribution (#5293)
* fix(hermes): retain gateway sender in captured messages

Port the provider change from Hermes commit 16b3f04a9a5be50e7d6c5bafe8273dfb1bfe51a1 (PR #98506, included in #105812). Keep the sender snapshot on the current upload object, alongside its existing client and assistant identity.

* feat(hermes): add sender-scoped gateway recall

Adapt Hermes PR #105812 to current per-turn author hooks and the standalone provider. Preserve configured recall by default; add explicit shared and peer modes with scoped fallbacks.

* feat(hermes): restore personal and shared setup presets

Restore the original Hermes gateway setup choices and peer-ID encoding. Keep prior recall behavior when unset, preserve per-turn capture and scoped compression, and test profile saves and session boundaries.

* fix(hermes): keep shared setup warning visible in TUI

* fix(hermes): clarify personal setup preserves session sharing

* test(hermes): assert setup completion notices

---------

Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
2026-09-23 16:20:20 +08:00
梧桐雨andzhengxiao.wu 03391bae43 feat(plugins): add Kimi Code CLI memory plugin (#4787)
* feat(plugins): add Kimi Code memory integration

* fix(plugins): read Kimi UserPromptSubmit prompt field

Kimi Code passes the submitted prompt as `prompt` (camelToSnake of
inputData { prompt, isSteer }), not `input`, so recall and first-prompt
profile injection never ran. Align tests and DESIGN.md, and point the
host references at the MoonshotAI/kimi-code sources.

---------

Co-authored-by: zhengxiao.wu <zhengxiao.wu@bytedance.com>
2026-09-23 14:37:39 +08:00
t0sakiandTRAE CLI bbf2e37f88 feat(pi): mirror the server's MCP tool surface (#5272)
* feat(pi): mirror the server's MCP tool surface

The pi extension defined seven viking_* tools over its REST client and had
drifted from the MCP harnesses: no grep, glob, tree, write, edit, no watch
tools, no health, and no context mode on search. pi has no MCP client, so
the extension now drives the shared stdio->HTTP proxy core in process --
never calling its start(), just handleMessage over an injected sink -- and
republishes every descriptor from the server's tools/list as a native pi
tool named openviking_<tool>. Nothing in the extension enumerates tools, so
a server that gains or drops one changes pi's surface at the next session
with no plugin release. No new dependency and no subprocess.

viking_* is removed outright with no alias period. The server's usage
attribution already recognises openviking_* but never recognised viking_*,
so pi's retrieval and reads now count toward Experience usage and lineage.

A failed handshake does not fail startup: recall, session capture and
takeover keep working, the status line and /viking report the failure, and
a later turn retries, so a server started after pi is picked up without a
restart. The shared mcpEnabled key now applies to pi as well.

pi validates tool arguments locally, which the other harnesses do not, so a
small schema-driven repair pass runs before validation. It only erases that
extra local strictness -- dropping explicit nulls on optional fields,
wrapping a scalar where the schema wants an array, and parsing a JSON
string the way FastMCP's pre_parse_json does -- and deliberately leaves
everything the server itself would reject.

Recall, session sync, profile injection and takeover stay on REST. The
experimental context-management fork keeps its own tools; only its
coexistence probe learns the new name, plus a globalThis marker for the
case where a 403 leaves the stable extension with no tools but a live
session.

* refactor(pi): replace custom MCP bridge with official client

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* test(pi): cover SDK calls, reload cleanup and dependency installs

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(pi): align MCP architecture descriptions

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-22 18:20:02 +08:00
t0saki b7d0415c24 feat(plugins): skill catalog and openviking-skills for the memory plugins (#5161)
* refactor(skills): install skills through one shared helper

POST /api/v1/skills kept its whole install loop (source resolution, per-skill
install, source metadata, list_only) inline in the route. Move it into
openviking/server/skill_ingest.py:install_skills so the MCP add_skill tool
and signed skill uploads can reuse the exact same code path. The REST
route's behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(mcp): add an add_skill tool

MCP clients had no way to create a skill: write refuses the skills/
subtree (_USER_MANAGED_SUBTREES) and add_resource validates its target as
a resource. Agents that should keep skills in OpenViking could read them
but never add one.

add_skill takes either the full SKILL.md text (data) or a path. A Git or
GitHub tree URL installs through the same source resolution as REST, with
skills=[...] to pick from a multi-skill repository and list_only to
preview it. A local SKILL.md, directory, or zip gets the add_resource
treatment: the tool mints a one-time upload token, now tagged kind="skill"
with the target root, selection and list_only, and the signed temp_upload
installs the file as skills instead of ingesting it as a resource.
target_uri="viking://agent/skills" shares the skill with the account.

All three paths (REST, MCP inline/Git, signed upload) go through
skill_ingest.install_skills. The tool count in the server log, app
comment, docs, and the Codex plugin's REAL_MCP_TOOLS moves to 16.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): search shared skills in find(context_type="skill")

Without a target_uri, find resolved the generic default targets, which
stop at the caller's user root, so a skill search never reached the
account-shared viking://agent/skills. REST /skills/find and the context
search already cover both roots. When context_type resolves to skill only
and no target_uri is given, the MCP tool now targets
default_target_directories(ctx, context_type=SKILL): the user's own skills
plus viking://agent/skills. REST find semantics are unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): print directory abstracts in tree(include_abstract=true)

The tree tool skipped to the next entry right after printing a directory,
and only printed abstracts for files, but the storage layer only fills
abstracts for directories (files always come back empty). The flag
therefore never printed anything. Print the abstract after either kind
of entry and ask for up to 1024 characters, enough for a full skill
description, so tree(uri="viking://~/skills", level_limit=1,
include_abstract=true) lists every skill with its description.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(skills): honor node_limit in GET /api/v1/skills

list_skills declared node_limit but always listed each skill root with a
hardcoded 1000. Pass it through per root; 0 keeps the default so the CLI's
accepted range (-n 0) still lists everything.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): point skill hits in find/search at their SKILL.md

A skill is indexed through its directory's .abstract.md, so find and
list-mode search printed hits like viking://agent/skills/x/.abstract.md.
Following the "use the read tool to expand a URI" advice returned only
the frontmatter, and read_content inlined the same stub. Skill hits now
show <dir>/SKILL.md, and read_content reads that file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): validate add_skill targets and sources before minting an upload

Review findings on the add_skill tool:

- target_uri passed the content-kind check for any path under a skills
  root (viking://~/skills/pdf) and, for ROOT, for another user's root,
  but the installer only accepts the caller's own skills root or
  viking://agent/skills. On the local-path branch the tool minted a
  one-time upload token anyway, and the upload failed with 400 after the
  token was spent. The target is now resolved with the installer's own
  rule first; shared subpaths map to viking://agent/skills, the rest fail
  at once, and the error names both allowed roots.
- Non-Git remote sources such as tos:// were treated as remote, then
  refused as "direct host filesystem paths". add_skill now decides Git
  with the same prefixes resolve_skill_source uses (shared as
  GIT_SKILL_SOURCE_PREFIXES) and reports other schemes as unsupported.
- With list_only, the upload instructions still said the skill would be
  installed and that no further call was needed; they now say the upload
  only lists the source's skills.
- The zip example packaged hidden files, so .git and .env files went
  into the stored skill. It now excludes VCS data, .env files,
  node_modules and .DS_Store, starting from a fresh archive.
- tree(include_abstract=true) printed the "abstract is not ready"
  placeholder for directories that never get an abstract, such as a
  skill's scripts/. Those placeholders are skipped.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(mcp): say that write only refuses the user's own skills subtree

The capability reference claimed MCP write refuses every skill URI. It
refuses the user's own skills/ subtree, but under viking://agent/skills
it writes a plain file that skips skill installation. State that, and
point shared skills at add_skill as well.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* style(skills): format skill_processor.py

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): inject an <available-skills> catalog at session start

Agents on every harness learned about memories at session start but had
no idea which skills OpenViking held, so a stored skill was only found if
a later recall happened to surface it.

buildProfileBlock() now takes the caller's resolved config as a fourth
argument and, when skillCatalog is on (default), adds <available-skills>
after <available-memories>: one GET /api/v1/skills lists the user's own
skills first, then account-shared ones, dropping a shared skill the user
shadows by name. Descriptions are cut to about 40 tokens and envelope
tags in them are escaped, since the shared root is written by anyone on
the account. The block has its own budget (skillCatalogTokenBudget,
default 1200) and degrades from descriptions to names to a one-line
count; with no skills, or a server without the endpoint, it is omitted.

The shared formatListing now gives entries back so its "+N more" tail
fits: a greedily filled listing never left room for it, so a cut listing
ended silently. This also applies to <available-memories>.

All six callers (claude-code, codex, opencode, dsh, pi, and the thin hook
runtime for cursor/trae/zcode) pass their config through.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): recall account-shared skills and flag skill entries

The server context face already mixes both skill roots into per-prompt
recall, but the plugins' last-resort ranked find only searched
viking://~/memories and viking://~/skills, so on servers without the
context face a shared skill in viking://agent/skills could never be
recalled. Add it as a third source, and name each skill hit by its
directory rather than the .abstract.md it was indexed through, matching
the context face and the session-start catalog.

When an injected recall block carries a skill (a type="skills" entry, or
a [skill] line from the fallback), its header gains one line telling the
agent to read the skill's SKILL.md before following it. Turns without a
skill are unchanged.

The openclaw plugin's vendored recall-core copy is regenerated with it.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): point skill writes at add_skill in the URI guard

A local Write or Edit aimed at viking://.../skills/... was denied with a
hint to use MCP write or edit instead, but the server refuses both under
the skills subtree, so the hint led straight into a second error. Hints
may now depend on the URI: for a skill URI (viking://~/skills,
viking://user/<id>/skills, viking://agent/skills) the default table and
dsh's bridged table name add_skill with an add_skill(data="<the full
SKILL.md text>") example. Other URIs keep their hints.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): bundle an openviking-skills skill

Nothing told an agent how to work with skills stored in OpenViking: how
to load one from the catalog, run its helper files, create one through
add_skill, install from Git or a local folder, share it with the account,
or move the user's existing local skills over.

examples/skills/openviking-skills covers all of that, including a
user-triggered, one-time migration of ~/.claude/skills, ~/.agents/skills
and ~/.cursor/skills that keeps environment-bound skills local (shipped
by a plugin or marketplace, symlinked in by a CLI installer such as
lark-cli, or needing a local binary) and uploads only what the user
approves skill by skill. It passes strict server validation.

sync.mjs ships it wherever add_skill is a real tool and a bundled skill
loads: the codex, claude-code, cursor and dsh plugins. openviking-memory
now points to it for skill work. A new sync test keeps synced skills
flat, since copySkill copies a flat file list and a subdirectory would
crash it; the marketplace tests pin the packaged copies, and dsh's
provider test expects both bundled skills.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* chore(plugins): bump versions for the skill integration

claude-code 0.6.0, codex 0.10.0, agent-hook (cursor/trae/zcode) 0.4.0 and
dsh 0.5.0 gain the skill catalog and, except trae/zcode, the bundled
openviking-skills skill; opencode 0.3.3 and pi 0.3.3 gain the catalog.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(codex): recall shared skills and flag skill entries in Codex too

Codex builds its recall block itself instead of through recall-core's
wrappers, so the previous commit's changes never reached it: its raw
search still skipped viking://agent/skills, its digest carried no skill
hint, and its post-processing kept only level-2 leaves, which silently
dropped every skill hit (skills are found through their directory's
level-0 abstract), including the viking://~/skills search it already ran.

recall-core now exports skillEntryHint() and skillHitUri(). The hint is
added when a block carries a type="skills" entry or cites any skill URI,
which also covers digests that only keep URIs. Codex searches the shared
skill root as a third bucket, labels skill hits "skills" under their
directory URI, lets them through post-processing, and adds the same hint
line to its envelope.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(cursor): install openviking-skills next to openviking-memory

sync.mjs puts openviking-skills into hosts/cursor/skills, but the
installer copied only openviking-memory into ~/.cursor/skills, so Cursor
never saw the new skill. Install, uninstall, the post-install check and
the doctor's file list now cover both skills. The install test also moves
to the agent-hook plugin's new 0.4.0 version string.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(dsh): keep PLUGIN_VERSION in step with package.json

The version bump moved package.json to 0.5.0 but left the PLUGIN_VERSION
constant at 0.4.3, which bundle.test.mjs and npm run check:version
compare against the manifest.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(skills): match ov-skills commands to the real ov CLI flags

The ov-skills skill documented flags the CLI never had (--json, and a
--limit that is only a hidden alias), a raw-content "ov skills add -"
form that sends a literal "-", and ov resources subcommands that do not
exist. Every command line now follows the clap definitions: -o json for
JSON, -n/--node-limit, -p/--uri on read commands and
-p/--parent-auto-create on add, -s/--skill as a comma list, show
--format, and validate's --strict-only body-length warning. ov add-skill
is documented as the same command as ov skills add.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(plugins): document the skill catalog, openviking-skills, and skill-aware recall

The integration pages (Claude Code, Codex, Cursor, TRAE, opencode, pi,
dsh), the capability reference, the plugin development guide, and the
plugin READMEs now describe the <available-skills> session-start block,
its skillCatalog / skillCatalogTokenBudget knobs, the bundled
openviking-skills skill where it ships, recall reaching
viking://agent/skills with the skill-entry hint, and the URI guard
sending skill writes to add_skill. en and zh pages carry the same facts.

Stale statements fixed on the lines touched: thin hook hosts use the
same 10000-token profile budget as the rest, the claude-code and codex
plugins ship four skills, dsh mounts the server MCP surface, and the
opencode install guide lists openviking_add_skill once.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): let the user's own skills use the catalog budget the shared group leaves

Review findings on <available-skills>:

- Each group got at most its even share of the listing budget, with
  unused tokens passing only forward, so the user's own skills (always
  first) never got more than half. Twenty own skills and one shared skill
  fell back to names only while most of the 1200 tokens went unused. A
  group now takes its even share or everything the later groups leave
  when listed in full, whichever is larger.
- When a group's share could not hold its header plus the "+N more"
  tail, formatListing gave back every entry and printed a bare header,
  which reads as an empty directory. It now prints the one-line
  "N entries, budget too tight" stub instead (memory listings too).
- A budget too small for even the one-line count now injects nothing
  rather than overrunning it.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): rank skill hits like memory leaves in the recall fallbacks

Naming a skill hit by its directory instead of its .abstract.md cost it
the 0.12 leaf boost, since the boost keyed on a ".md" URI, so a skill
that main would recall lost to ten slightly weaker memory leaves. In
Codex, skills were also never picked while enough memory leaves passed
the threshold (leaves are picked first), and a hit the server labeled
with another category lost its "skills" label. Skill hits now count as
leaves in ranking and picking in both recall-core and Codex, and Codex
always labels them "skills".

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(plugins): send shared-skill edits back to the shared root in the URI guard

The guard's add_skill example carried no target_uri, and add_skill
without one installs into the caller's own root. Fixing a shared skill
that way left the team copy unchanged and created a private copy that
the catalog then shows instead. For viking://agent/skills URIs the
example now passes target_uri="viking://agent/skills", and a helper file
(anything below a skill's SKILL.md) points to a folder upload through
add_skill(path=...) rather than SKILL.md text. addSkillExample() builds
the example for both the default table and dsh's.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(skills): tighten openviking-skills where review found unsafe steps

- The catalog is a snapshot that drops descriptions or entries with many
  skills, so a name missing from it does not prove the name is free.
  Check <root>/<name>/SKILL.md, and confirm with the user before
  replacing an existing skill, since add_skill replaces silently.
- Updating a shared skill must pass target_uri="viking://agent/skills"
  after the user confirms; otherwise add_skill creates a private copy
  that shadows it.
- Every file in an uploaded folder is stored with the skill: zip without
  .git, .env files, node_modules and .DS_Store, and delete the archive
  afterwards. The migration now inspects the whole folder, hidden files
  included, for secrets.
- Migration flags frontmatter keys OpenViking drops (for example
  disable-model-invocation or context), and fixes a missing name or
  description in a temporary copy, never in the user's file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(plugins): keep the session-start block under the host's inline limit

Claude Code saves hook context over 10,000 characters to a file and
shows the model a 2 KB preview; Codex and trae-cli spill past about
10,000 bytes; ZCode drops stdout over 32 KB. The session-start block
(profile at a 10,000-token budget, memory index, skill catalog, and the
archive on resume) routinely ran 25-40 KB, so on these hosts the model
saw only the start of the profile, and the catalog appended at the end
never reached it.

sessionStartMaxBytes caps the whole block in UTF-8 bytes: 9500 for
claude-code and codex, 20000 for zcode, no cap elsewhere. Under the cap
buildProfileBlock shrinks its token budgets (about 4 bytes per estimated
token); if the block still does not fit it drops the memory index, then
the catalog. On resume/compact the archive takes up to half, truncated
on a line with a pointer to viking://~/sessions/<id>/history/.

On resume, claude-code and codex skip the profile block when it matches
the one this session already received, since the restored history holds
it; a changed block is injected again.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): return one hit per skill package in find

#5045 made a skill index as a whole package, so an item-level find now
returns one hit per file inside it. Route skill-only find through
SearchService.find_skills, which keeps the best hit per package, and
resolve every skill hit to its package's SKILL.md instead of only
rewriting the two index sidecars.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): point skill changes at add_skill in the tool descriptions

The server keeps accepting write/edit under viking://agent/skills, and
forget still removes a skill directory, so the constraint lives in the
tool descriptions: add_skill is the one entry point for creating and
updating a skill, and removal goes through ov skills remove or Studio.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): describe a skill hit by its own abstract

A package hit can be any file inside the skill, whose abstract describes
that file and not the skill, so find would list a skill under a helper
script's summary. Read the package's abstract for those hits, the way
GET /skills/find already does. Keep a filter-only skill query on the
generic find, which find_skills does not serve.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): document package-level skill retrieval in find

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* feat(agent-plugins): ship the openviking-skills skill

Agent Plugins has no hooks, so no session-start catalog: without this skill
the model never learns that the account's skills exist. The skill's own text
now reaches for find(context_type="skill") first and treats
<available-skills> as something only some harnesses inject, so one copy
reads correctly in both kinds of harness.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(plugins): name the skill package a recall hit came from

Since a skill is indexed as a whole package, a hit can be any file inside
it, not just the two index sidecars the old rewrite stripped. Derive the
package root the way the server's skill_root_uri does, drop the internal
update backups, and keep one entry per package at its best score.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(plugins): let the skill catalog use both roots' full listings

The server already caps each skill root at node_limit, so a second cap over
the merged list only bites once the private root alone fills it — and then
it drops the shared root whole while reporting nothing dropped. The token
budget is what should decide, and it already does.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(plugins): say node_limit caps each skill root, not the merged list

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): do not paste an unready abstract over a skill hit's own summary

fs.abstract returns a placeholder string rather than raising when a package
has no usable .abstract.md, so the substitution replaced a useful file
summary with a diagnostic line. Reject the same placeholders tree already
rejects, and bound the per-package reads the way read_content is bounded.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): correct how search reports skill hits, and refresh the tool table

find and search both render one line per skill package, so the earlier
wording — that search returns several hits per package — contradicted the
code. Say what actually differs: search still spends a limit slot per
matching file and keeps that file's summary. Also point forget and
add_resource at add_skill where an agent would look for them, name the REST
delete alongside the CLI, and bring the capability table's line citations
back in step with mcp_endpoint.py.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(agent-plugins): list add_skill among the tools the package exposes

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* refactor(mcp): drop guards and prose no caller can reach

install_skills only ever returns a dict, add_skill always fills root_uri,
and a source with no SKILL.md raises before it gets here, so the
isinstance, empty-list and missing-uri branches were unreachable.
fs.abstract only returns the directory placeholder. One skill package
resolves to one rendered item, so the pending map holds one each. In the
docstrings, drop what Args already says and the one removal path an MCP
caller cannot take.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(plugins): drop a comment about a branch formatListing cannot take

A listing left with only its header returns the stub above, so it never
reaches the silent close the comment described. Also name
sessionStartMaxBytes in buildProfileBlock's options type, where the .d.mts
already has it.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* revert(plugins): drop the skill changes in the recall fallback

Reverts the recall half of the skill integration: 992215bfb, 553416200,
5e838a0a0 and 62a456c08. The seven files they touched go back to their
state at aa77061c1, the merge base with main.

Those four commits taught the local recall assembly about skills: a third
source for viking://agent/skills, skillHitUri naming a hit's package,
dedupeSkillHits keeping one entry per package, rankItem scoring a skill
like a memory leaf, and a header line telling the model to read SKILL.md.
Four of the five only ever ran in the raw-find fallback. recallForPeer
calls buildServerAssembledBlock first and returns as soon as it answers,
so searchAllSources, rankItem and the fallback block builder are reached
only on a deployment whose server has no context face. The fifth, the
header line in wrapContext, did run on the main path, but the server
already reports each entry's type and the URI it wants read, so the line
restates what the block carries.

Skills still reach the model on the path that runs: the context face
searches both skill roots, returns them as entries with type="skills",
and the session-start <available-skills> catalog lists every skill with
its description. Both stay.

The documentation that described the fallback behaviour goes with it. The
statements that survive are the ones the context face makes true on its
own, such as recall covering the skills shared with the account.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* chore(plugins): bump opencode and pi past main's releases

Both were 0.3.3 on this branch and main has since shipped 0.3.3 of its
own, so the version a host installs by no longer moves for the skill
catalog they now carry through the shared library.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): say what list-mode search actually reports for a skill hit

The search row claimed a skill package's summary is the matching file's.
It is not: _format_search_result rewrites every skill hit onto the
package's SKILL.md, keeps the best-scored one per package, and
_describe_skills_by_package replaces the summary with the package's own
abstract. What is true is that limit applies during retrieval, before
that merge, so a package matching several files still spends several
slots and fewer than limit results come back.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW
2026-09-22 15:09:44 +08:00
t0saki 6b127eb92c feat(mcp): add an add_skill tool and make skills findable over MCP (#5160)
* refactor(skills): install skills through one shared helper

POST /api/v1/skills kept its whole install loop (source resolution, per-skill
install, source metadata, list_only) inline in the route. Move it into
openviking/server/skill_ingest.py:install_skills so the MCP add_skill tool
and signed skill uploads can reuse the exact same code path. The REST
route's behavior is unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* feat(mcp): add an add_skill tool

MCP clients had no way to create a skill: write refuses the skills/
subtree (_USER_MANAGED_SUBTREES) and add_resource validates its target as
a resource. Agents that should keep skills in OpenViking could read them
but never add one.

add_skill takes either the full SKILL.md text (data) or a path. A Git or
GitHub tree URL installs through the same source resolution as REST, with
skills=[...] to pick from a multi-skill repository and list_only to
preview it. A local SKILL.md, directory, or zip gets the add_resource
treatment: the tool mints a one-time upload token, now tagged kind="skill"
with the target root, selection and list_only, and the signed temp_upload
installs the file as skills instead of ingesting it as a resource.
target_uri="viking://agent/skills" shares the skill with the account.

All three paths (REST, MCP inline/Git, signed upload) go through
skill_ingest.install_skills. The tool count in the server log, app
comment, docs, and the Codex plugin's REAL_MCP_TOOLS moves to 16.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): search shared skills in find(context_type="skill")

Without a target_uri, find resolved the generic default targets, which
stop at the caller's user root, so a skill search never reached the
account-shared viking://agent/skills. REST /skills/find and the context
search already cover both roots. When context_type resolves to skill only
and no target_uri is given, the MCP tool now targets
default_target_directories(ctx, context_type=SKILL): the user's own skills
plus viking://agent/skills. REST find semantics are unchanged.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): print directory abstracts in tree(include_abstract=true)

The tree tool skipped to the next entry right after printing a directory,
and only printed abstracts for files, but the storage layer only fills
abstracts for directories (files always come back empty). The flag
therefore never printed anything. Print the abstract after either kind
of entry and ask for up to 1024 characters, enough for a full skill
description, so tree(uri="viking://~/skills", level_limit=1,
include_abstract=true) lists every skill with its description.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(skills): honor node_limit in GET /api/v1/skills

list_skills declared node_limit but always listed each skill root with a
hardcoded 1000. Pass it through per root; 0 keeps the default so the CLI's
accepted range (-n 0) still lists everything.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): point skill hits in find/search at their SKILL.md

A skill is indexed through its directory's .abstract.md, so find and
list-mode search printed hits like viking://agent/skills/x/.abstract.md.
Following the "use the read tool to expand a URI" advice returned only
the frontmatter, and read_content inlined the same stub. Skill hits now
show <dir>/SKILL.md, and read_content reads that file.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): validate add_skill targets and sources before minting an upload

Review findings on the add_skill tool:

- target_uri passed the content-kind check for any path under a skills
  root (viking://~/skills/pdf) and, for ROOT, for another user's root,
  but the installer only accepts the caller's own skills root or
  viking://agent/skills. On the local-path branch the tool minted a
  one-time upload token anyway, and the upload failed with 400 after the
  token was spent. The target is now resolved with the installer's own
  rule first; shared subpaths map to viking://agent/skills, the rest fail
  at once, and the error names both allowed roots.
- Non-Git remote sources such as tos:// were treated as remote, then
  refused as "direct host filesystem paths". add_skill now decides Git
  with the same prefixes resolve_skill_source uses (shared as
  GIT_SKILL_SOURCE_PREFIXES) and reports other schemes as unsupported.
- With list_only, the upload instructions still said the skill would be
  installed and that no further call was needed; they now say the upload
  only lists the source's skills.
- The zip example packaged hidden files, so .git and .env files went
  into the stored skill. It now excludes VCS data, .env files,
  node_modules and .DS_Store, starting from a fresh archive.
- tree(include_abstract=true) printed the "abstract is not ready"
  placeholder for directories that never get an abstract, such as a
  skill's scripts/. Those placeholders are skipped.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* docs(mcp): say that write only refuses the user's own skills subtree

The capability reference claimed MCP write refuses every skill URI. It
refuses the user's own skills/ subtree, but under viking://agent/skills
it writes a plain file that skips skill installation. State that, and
point shared skills at add_skill as well.

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* style(skills): format skill_processor.py

Claude-Session: https://claude.ai/code/session_01ECVkFufAU2LKe4g83cxt28

* fix(mcp): return one hit per skill package in find

#5045 made a skill index as a whole package, so an item-level find now
returns one hit per file inside it. Route skill-only find through
SearchService.find_skills, which keeps the best hit per package, and
resolve every skill hit to its package's SKILL.md instead of only
rewriting the two index sidecars.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): point skill changes at add_skill in the tool descriptions

The server keeps accepting write/edit under viking://agent/skills, and
forget still removes a skill directory, so the constraint lives in the
tool descriptions: add_skill is the one entry point for creating and
updating a skill, and removal goes through ov skills remove or Studio.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): describe a skill hit by its own abstract

A package hit can be any file inside the skill, whose abstract describes
that file and not the skill, so find would list a skill under a helper
script's summary. Read the package's abstract for those hits, the way
GET /skills/find already does. Keep a filter-only skill query on the
generic find, which find_skills does not serve.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): document package-level skill retrieval in find

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* fix(mcp): do not paste an unready abstract over a skill hit's own summary

fs.abstract returns a placeholder string rather than raising when a package
has no usable .abstract.md, so the substitution replaced a useful file
summary with a diagnostic line. Reject the same placeholders tree already
rejects, and bound the per-package reads the way read_content is bounded.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): correct how search reports skill hits, and refresh the tool table

find and search both render one line per skill package, so the earlier
wording — that search returns several hits per package — contradicted the
code. Say what actually differs: search still spends a limit slot per
matching file and keeps that file's summary. Also point forget and
add_resource at add_skill where an agent would look for them, name the REST
delete alongside the CLI, and bring the capability table's line citations
back in step with mcp_endpoint.py.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* refactor(mcp): drop guards and prose no caller can reach

install_skills only ever returns a dict, add_skill always fills root_uri,
and a source with no SKILL.md raises before it gets here, so the
isinstance, empty-list and missing-uri branches were unreachable.
fs.abstract only returns the directory placeholder. One skill package
resolves to one rendered item, so the pending map holds one each. In the
docstrings, drop what Args already says and the one removal path an MCP
caller cannot take.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW

* docs(mcp): say what list-mode search actually reports for a skill hit

The search row claimed a skill package's summary is the matching file's.
It is not: _format_search_result rewrites every skill hit onto the
package's SKILL.md, keeps the best-scored one per package, and
_describe_skills_by_package replaces the summary with the package's own
abstract. What is true is that limit applies during retrieval, before
that merge, so a package matching several files still spends several
slots and fewer than limit results come back.

Claude-Session: https://claude.ai/code/session_01CrZadR75kCueyZyoiGUFBW
2026-09-22 14:49:20 +08:00
z1gonandKoNit-K 6b067ca801 fix(hermes): commit active memory sessions by token threshold (#5278)
* fix(memory): commit live OpenViking sessions

Ported from NousResearch/hermes-agent commit 8b06175633020abaf89cb9d1ae42452883416300. Adapt the original regression tests to the standalone external-loader fixture.

* fix(hermes): commit active sessions at a configurable token threshold

* fix(hermes): isolate commit state across connection reloads

---------

Co-authored-by: KoNit-K <konit.block@protonmail.com>
2026-09-22 13:37:05 +08:00
z1gonandgaoanze888 5854383b5f fix(hermes): save provider settings in the requested profile (#5262)
* fix(openviking): honor explicit home when saving config

Ported from NousResearch/hermes-agent commit 47e2d4532d64fd4ceafb1e9320bc3271a3b58be8.
Adapt the regression to the standalone plugin external-loader fixture.

* test(hermes): preserve profile scope after a failed config save

---------

Co-authored-by: gaoanze888 <gaoanze888@gmail.com>
2026-09-22 11:32:30 +08:00
Hao Zheandnftpoetrist 172c105071 fix(hermes): isolate cached user identities across reloads (#5261)
* fix(memory): key OpenViking user-space cache on the resolving client's own snapshot

Background writers (on_memory_write, sync_turn) freeze a client via
_new_client() on one thread and resolve/use it later. If a config reload
swaps self._conn_snapshot in between, _user_space() previously read the
live self._conn_snapshot instead of the snapshot the passed-in client was
actually built from, and published the OLD client's resolved identity
under the NEW connection's cache key — poisoning every subsequent lookup
for the new connection with the stale user until the next reload.

_new_client() now stamps each client with the snapshot it was built from,
and _user_space() keys/publishes the cache against that stamped snapshot
when an explicit client is passed, instead of the live self._conn_snapshot.

Ported from NousResearch/hermes-agent commit e079fb6b32e5ef7ba04a533d59d84041fffa5d61.
Adapt provider paths and the regression test to the standalone external loader.

* fix(hermes): bind identity for all client construction paths

Bind the cache key in the client constructor so retained active clients are also covered. Avoid cache use for unbound clients. Replace the stamped-stub regression with external-loader tests that exercise real background and active-client construction paths.

---------

Co-authored-by: nftpoetrist <264138787+nftpoetrist@users.noreply.github.com>
2026-09-21 19:28:13 +08:00
ec13c5b113 retrieval: add Jev rerank provider (#5247)
* retrieval: add Jev rerank provider

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* retrieval: log Jev rerank payloads

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* retrieval: make rerank payload logging configurable

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* retrieval: support Jev through Vercel gateway

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* retrieval: route Jev through Vercel's TypeSafe-compatible endpoint (#5256)

Vercel AI Gateway exposes https://ai-gateway.vercel.sh/typesafe, which
accepts TypeSafe's own System One request/response shapes. Drop the
Vercel-specific branch (undocumented /v4/ai/evaluation-model path and
SDK-internal ai-gateway-* headers) so the adapter speaks one protocol;
Vercel is now just a different api_base and model id.

Auto-detect: any api_base containing "typesafe" resolves to jev.
Docs: point Vercel config at the /typesafe base and note the long-lived
API key and credit-card requirements.

Verified live against Vercel with a real gateway key.

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
Co-authored-by: Zayn Jarvis <zaynjarvis@gmail.com>
2026-09-21 19:05:52 +08:00
t0saki 14dd4e9e61 docs: explain the startup hook-trust step for Codex and TraeCode CLI 2.0 (#5257)
* docs: explain the startup hook-trust step for Codex and TraeCode CLI 2.0

The Codex-format plugin registers six hooks, and both Codex and TraeCode
CLI 2.0 keep them behind a startup trust prompt. Answering "Continue
without trusting" — or scrolling past the prompt — leaves recall and
capture silently dead while the MCP tools keep working, and every later
update that touches a hook asks for trust again.

Quote the prompt verbatim in the Codex and TRAE integration docs, name
the option to pick, and describe recovery as two independent switches:
/hooks for hook trust and on/off state, /plugins for the plugin's own
enabled state. The troubleshooting rows now cover "installed but nothing
fires", not just the literal `6 hooks need review` banner.

* docs: refresh the Codex onboarding cards for the hook-trust prompt

The CDN-served onboarding pages still told people to run /hooks after
launch and listed four hooks; the plugin has registered six since
PreToolUse and SessionEnd were added, and trust is now asked for at
startup.

Quote the startup prompt, list all six events, and note that /hooks and
/plugins are independent switches. An older Codex may still show fewer
events, so say so rather than promising six.
2026-09-21 17:31:40 +08:00
Hao Zheandzhiheng.liu ec08630541 fix(hermes): validate OpenViking forget URIs (#5185)
* fix(hermes): validate OpenViking forget URI ownership

Accept the supported self alias, reject removed uid-less user paths, and prevent explicit deletes for a different resolved user.

* fix(hermes): fail closed on unverified forget identity

* fix(hermes): reject dot segments in forget URIs

* fix(hermes): bind forget verification to connection

---------

Co-authored-by: zhiheng.liu <zhiheng.liu@bytedance.com>
2026-09-21 17:26:21 +08:00
Zayn Jarvis e44ea6e11a fix(plugins): honor cloud recall compression across harnesses (#5240)
* fix(plugins): honor cloud recall compression across harnesses

* chore(plugins): bump versions for cloud recall support

* fix(dsh): align runtime and package versions

* test(plugins): derive installed version from the manifest

* refactor(plugins): route Codex recall through the shared pipeline
2026-09-21 15:43:04 +08:00
t0saki 9b0ce3dea5 fix(doctor): read /health with credentials where the gateway gates it (#5088)
The doctor probes /health once without credentials on purpose: it reports
version and auth_mode before judging the key. OpenViking Cloud authenticates
/health at the gateway, so that probe answers 401 and the report stopped
right there with "the server answered but not like OpenViking" — discarding
the system/status, fs/ls and /mcp probes it had already taken, and skipping
/ready — while the configured key was valid all along.

When the credential-less /health answers 401/403, fall back to the
authenticated probe for version, auth_mode and identity and keep the ladder
going; send credentials on /ready as well. A key the gateway really rejects
is now reported as a rejected key instead of a wrong url.
2026-09-21 15:19:50 +08:00
MaojiaSheng aa77061c14 chore: remove useless docs, mv some code to better places (#5244) 2026-09-21 14:02:00 +08:00
Jiahui ZhouandTRAE CLI 336f2173b4 perf(resources): optimize incremental resource ingestion (#5175)
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-21 14:00:51 +08:00
auyua9andauyua9 b5202d2929 fix(openclaw-plugin): pin mode and captureMode to allowed values (#5234)
The OpenClaw plugin manifest defined `mode` and `captureMode` as bare
`type: "string"`, allowing the runtime to accept any value at
config-validation time and only discover the typo at the first
config-init failure.

Tighten the JSON Schema to:

- `mode` enum `["local", "remote"]` (the existing description
  already notes only 'remote' is supported, so pinning matches the
  documented behaviour).
- `captureMode` enum `["semantic", "keyword"]`, matching the
  existing two retrieval modes.

Single-file change scoped to
`examples/openclaw-plugin/openclaw.plugin.json`. No public API change.

Co-authored-by: auyua9 <auyua9@users.noreply.github.com>
2026-09-21 11:21:30 +08:00
YohanesandYohanes 241dc8b3e0 fix(knowledge-graph): style recommended module entities (#4435)
Co-authored-by: Yohanes <CryoThrust@users.noreply.github.com>
2026-09-20 20:49:45 +08:00
t0saki 3c6e3d456b feat(agent-plugins): ship the ov-experience-memory skill (#5172)
Sync examples/skills/ov-experience-memory into agent-plugins/skills so
Agent Plugins clients can retrieve and apply Experience through the find,
search, and read MCP tools. The package has no hooks and no session
capture, so the copy is retrieval-only; the README and the Agent Plugins
integration docs say so.
2026-09-18 19:14:08 +08:00
Hao Zhe 3fca257752 feat(hermes): import standalone OpenViking memory provider (#5152)
* feat(hermes): import standalone OpenViking memory provider

* docs(hermes): consolidate handoff notes in plugin README

* Revert "docs(hermes): consolidate handoff notes in plugin README"

This reverts commit e14f71cc52.

* docs(hermes): clarify plugin installation and maintenance

* ci(hermes): defer dedicated plugin workflow

* docs(hermes): retain bundled setup during migration testing
2026-09-18 15:16:45 +08:00
Qin Haojie f316f27569 fix(storage): 用文件锁替代本地存储 PID 检查 (#5159)
避免残留 PID 和并发启动导致误判,仅保护 local/cuvs 后端;远程向量后端自动跳过,保留显式跳过开关并删除旧 PID 兼容逻辑。
2026-09-18 15:10:13 +08:00
t0saki 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.
2026-09-17 19:38:18 +08:00
t0saki 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
2026-09-17 17:25:20 +08:00
Qin Haojie 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
2026-09-17 11:57:16 +08:00
t0saki 8941acc864 docs: suggest the volces registry mirror when ghcr.io is hard to reach (#5099) 2026-09-16 17:42:22 +08:00
Evoandr266-tech c809fec2be fix(dsh): avoid watching bundled skills during plugin upgrades (#5074)
* fix(dsh): avoid watching bundled skills during plugin upgrades

* test(dsh): read current manifest version in shared config checks

---------

Co-authored-by: r266-tech <r266-tech@users.noreply.github.com>
2026-09-16 14:40:48 +08:00
yorickandTRAE CLI 87ae94031b feat(memory-plugin): support OPENVIKING_EXTRA_HEADERS for private gateways (#5065)
* feat(memory-plugin): support OPENVIKING_EXTRA_HEADERS for private gateways

Some private OpenViking deployments sit behind gateways that require
custom tenant/vault headers on every request (e.g. `openviking_name`).
The stdio MCP proxy hard-coded its header set, so those deployments
returned HTTP 400 before the initialize handshake could run.

- Parse OPENVIKING_EXTRA_HEADERS (JSON object of scalars) into
  proxyConfig.extraHeaders via buildMcpProxyConfig.
- Merge extras first in headersForRequest so proxy-owned headers
  (Authorization, Mcp-Session-Id, MCP-Protocol-Version, identity)
  always win on the wire.
- Drop reserved header names at parse time with a stderr warning.
- Whitelist the env var in codex-memory-plugin/.mcp.json and document
  the escape hatch in its README.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

* docs(codex): remove tenant-shaped example values from OPENVIKING_EXTRA_HEADERS

Reviewer feedback: the `openviking_name`, vault id and region examples
read like internal-deployment specifics. Replace them with generic
`<header-name>/<header-value>` placeholders and refer operators to their
gateway docs for the actual names.

Co-authored-by: TRAE CLI <traecli@bytedance.com>

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-09-15 22:07:51 +08:00