* feat: add OpenViking memory integration for TRAE CLI
Add TRAE CLI lifecycle hooks and MCP proxy support, wire the integration into the shared installer, and cover idempotent install and uninstall behavior.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(trae-cli): cover archive installs and hook payload aliases
* fix: keep TRAE CLI installation explicit
Leave TRAE Desktop detection unchanged and avoid auto-selecting TRAE CLI. TRAE CLI remains available through an explicit harness selection or --harness trae-cli.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(trae-cli): auto-select installed CLI commands
Detect traecli and traex only when they are available in PATH, then mark and select the TRAE CLI harness automatically.
---------
Co-authored-by: “bianhaonan” <“bianhaonan@bytedance.com”>
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* feat(pdf): refactor MinerU parsing to the official file_parse API
* feat(pdf): remove mineru_api_key from configuration and examples
* feat(pdf): preflight MinerU /health during service initialization
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* feat(plugins): add OpenViking memory for DSH
* feat(dsh-plugin): graft review items — source whitelist, dsh constructors, live recall gate
Applies the #task-65 review verdict's graft list from #3991 onto the
#3993 base:
- capture whitelist: drop every plugin-sourced user message (any plugin,
not just this one) so injected context never mirrors into memory as
human input; recall queries keep their existing scope
- pre-step: register with prepend so this listener sees the final
claimed batch, and short-circuit on signal.aborted around each await
- adopt dsh constructors behind exact-pinned peers (devDependencies
mirror the pins): tools flow through @deepseek-ai/dsh-tools defineTool
(declarative parameters, output schema/render, presentCall per tool),
plugin messages through @deepseek-ai/dsh-llm createUserMessage; a
registration-shape test makes a future rc pin bump fail CI instead of
a user install when the ToolDefinition contract moves
- live-recall.test.mjs: opt-in (OPENVIKING_E2E=1) real-backend gate —
store a sentinel via session commit, wait for extraction, assert
recall returns it; passed against a live OpenViking server in 124s
(note: commit with the default keep_recent_count=10 extracts nothing
from short sessions — the test pins keepRecentCount 0)
- README: why injection is pre-step user messages, not the system
prompt (complete:true personas silently drop prompt assembly), plus
peer-pin rationale and a Testing section
Tests: 15 pass + 1 env-gated (node --test), requires npm ci for the
pinned dsh devDependencies — CI step lands separately (workflow scope).
* ci(pr): install DSH plugin deps before running memory plugin tests
* fix(dsh-plugin): finalize neutral plugin integration
Remove product-specific identifiers from the DSH plugin surface and harden its lifecycle, HTTP contracts, archive tooling, and ordered offline delivery.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
---------
Co-authored-by: Zayn Jarvis <zaynjarvis@gmail.com>
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* feat(plugins): add Agent Plugins 1.0 portable package
Add agent-plugins/, an Agent Plugins 1.0 conformant package
(https://agent-plugins.org/specification) that any conforming client can
load: plugin.json manifest, an openviking-memory skill teaching the
hook-less recall + persist loop, and an mcp.json stdio entry running a
stdio -> streamable-HTTP proxy that resolves credentials from
OPENVIKING_* env -> ~/.openviking/ovcli.conf -> ~/.openviking/ov.conf,
same as the ov CLI.
servers/shared/* are generated copies of memory-plugin-shared/lib, wired
into sync.mjs / sync.test.mjs TARGETS so they cannot drift silently.
config.mjs / debug-log.mjs / mcp-proxy.mjs are adapted from
claude-code-memory-plugin with the hook-tuning knobs dropped.
plugin.test.mjs validates spec conformance (schema URLs and matching
spec versions, name rules, closed manifest root, semver, skill
frontmatter, referenced files staying inside the plugin root, node
--check on all .mjs) and runs in CI via pr.yml.
The skill treats tree/write/edit as optional, since they only exist on
servers that carry #3936.
Docs: docs/{en,zh}/agent-integrations/15-agent-plugins.md, registered in
the VitePress sidebar and the integration overview tables, plus a link
from the three root READMEs. The docs recommend the per-client plugin
whenever the harness has hooks, with the shared installer one-liner.
Based on #3994 by @ZaynJarvis.
Co-Authored-By: Zayn Jarvis <zaynjarvis@gmail.com>
* docs(agent-plugins): pluralize README title
---------
Co-authored-by: Zayn Jarvis <zaynjarvis@gmail.com>
* fix(vikingdb): normalize all date_time range filters in API key client
OpenViking compiles TimeRange down to the internal `range` DSL, but the
commercial VikingDB data plane (Bearer API-key auth) expects `time_range`
for date_time fields and `range` only for numeric fields. The API-key
client does not run the local engine's filter conversion, so `range`
nodes on date_time fields were sent verbatim and mis-handled.
Normalize `range` -> `time_range` for every schema date_time field by
reusing the canonical VALID_TIME_FIELDS constant, covering both
`created_at` and `updated_at` instead of hardcoding a single field name.
Numeric `range` nodes and nested boolean filter structure are preserved,
and filters already emitted as `time_range` pass through unchanged. Only
the request body `filter` is rewritten; upsert/update data is untouched.
Add regression tests covering the converted created_at/updated_at date
filters, an unchanged numeric filter, and time_range idempotency.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(vikingdb): normalize date_time filters in AK/SK client
The API-key client already rewrites `range` filter nodes on date_time
fields to VikingDB's `time_range` operator, but the AK/SK-signed
`VolcengineCollection` shares the same commercial data-plane endpoints
and had the identical latent bug: `TimeRange` expressions compile down
to the internal `range` DSL, which the commercial API only accepts for
numeric fields.
Mirror the API-key fix in `VolcengineCollection._data_post` so both
auth modes normalize `range` -> `time_range` for `created_at`/`updated_at`
while leaving numeric `range` nodes untouched. Add AK/SK coverage for
both date_time fields and for idempotency of already-`time_range` input.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Preserve result.trace_id across plugin HTTP wrappers, include it in commit success and failure logs, and surface it in user-visible commit confirmations where supported.
Brings the docs onto the widget's v2 contract, and restores the push layout
the site had before the migration to the shared widget.
The navbar trigger is gone. The widget renders its own bottom entry pill and
right-edge toggle, which is now the single entry point across the marketing
site, the docs and the blog instead of a per-site invention — and it lets a
reader start typing before the panel exists. VikingBotAssistant.vue existed
only to be that button, so it goes with it.
The loader is a plain module rather than a component: with no trigger to
render there is nothing for Vue to own. It mounts once per page load on an
idle callback (2.5s timeout fallback for Safari < 16.4) — the widget is an
assistive affordance, not documentation, and must not compete with the page
for bandwidth. Locale comes from <html lang>, which survives a configured
`base`, unlike matching the pathname against '/zh'; a locale switch is a
client-side route change, so onAfterRouteChanged remounts the widget in the
language the reader just picked.
custom.css grows the host half of the push contract, ported from the layout
this site had before #3939 — `.Layout` and `.VPNav` inset by `--ov-rail`, the
navbar title released from its sidebar-anchored absolute position, doc padding
retuned. Two differences from the old recipe:
- The breakpoint moves 768px -> 900px, matching the widget's own sheet
breakpoint. Below it the panel is a bottom sheet and there is no rail.
- The old recipe hid navbar controls between 768 and 1279 to make room for the
VikingBot button. There is no button now, so those rules are dropped; only
the space-driven ones (hide sidebar and aside) remain.
Screenshots at 900/1000/1280/1440 also earned one new rule: at 1280 the
sidebar returns, and sidebar + aside + rail left the prose at ~300px. Between
1280 and 1439 the aside is hidden instead, which roughly doubles the reading
column.
`--ovw-*` are mapped from `--vp-c-*`, so the panel is painted in the docs
palette and follows the appearance toggle — custom properties are not reset by
the widget's `all: initial`, so they pierce the shadow root with no bundle
change. `--ovw-rail-top: 64px` keeps the rail below the fixed VPNav.
Verified on a production build against a mocked embed backend, both locales,
900/1000/1280/1440/720px: rail publishes and retracts, layout squeezes and
springs back, panel sits below the navbar in docs colours and in the reader's
language, sheet takes over below 900px with no squeeze. Theme tests 13/13.
* feat(reindex): support tag updates
Add replace and append tag modes to reindex vector rebuilds, preserve omission-aware behavior, and propagate options through background tasks and namespace rebuilds. Align Python, TypeScript, Go, and CLI interfaces with tests and documentation.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(reindex): lock file targets exactly
Use an exact path lock for existing file targets while retaining tree locks for directories and prune-orphans scopes. Add a regression test for single-file reindex.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(reindex): handle prune file targets
Use exact locks for existing file targets in prune-orphans mode while retaining tree scope for missing targets. Document the existing Go ReindexOptions wait semantics and add lock regression coverage.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(embedding): downsample oversized image inputs
Keep imported image resources unchanged while avoiding provider-side multimodal embedding failures for oversized images. The embedding path now builds a temporary downsampled image data URI when image bytes exceed the shared large-image limits.
Move reusable image size thresholds into media_limits so both parser-side large image handling and embedding-side input preparation depend on a common utility instead of embedding_utils importing parser internals.
Add vectorize_file coverage confirming large image embedding inputs are resized and the stored resource bytes are preserved.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(media): downsample oversized image model inputs
Keep imported image resources unchanged while avoiding provider-side multimodal failures for oversized images. Shared image input preparation now builds temporary downsampled bytes for model requests when image bytes exceed the configured large-image limits.
Apply the model-input downsampling to both semantic image summary generation and embedding image data URI construction, so directory and code repository imports can preserve original images while sending provider-compatible inputs.
Move reusable image size thresholds into media_limits so parser-side large image handling, VLM summary generation, and embedding preparation share common limits without embedding_utils importing parser internals.
Always convert downsampled model images to RGB before JPEG encoding so Pillow-openable modes such as LA or I;16 do not fall back to the original oversized bytes.
Add coverage confirming VLM image summaries, vectorize_file embedding inputs, and JPEG-incompatible image modes are resized while stored resource bytes are preserved.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* feat(docs): load VikingBot from the shared embed widget
The docs assistant was a copy of the playground chat panel: its own
transport, markdown renderer and ~700 lines of panel CSS, all talking to
a hardcoded gateway. Replace it with a thin loader so the shared embed
widget owns the conversation UI.
- VikingBotAssistant.vue keeps only the navbar trigger; the first click
lazy-loads the widget script and mounts it with the docs locale, later
clicks toggle the panel.
- Widget URL defaults to the stable alias and can be overridden with
VITE_VIKINGBOT_WIDGET_URL for local development.
- A failed script load surfaces a small hint next to the button and
leaves the docs untouched; the next click retries.
- Drop vikingbot-api.ts, vikingbot-markdown.ts and their tests, the
panel styles, and the markdown-it/xss dependencies they needed.
* fix(docs): let the trigger button own the widget click
mount({trigger: el}) makes the widget bind its own click->toggle on that
element. Passing the navbar button while this component also toggles on
@click meant every click after the first toggled twice: the panel stayed
put while aria-expanded flipped, so the button announced the wrong state.
Mount with trigger: 'none' instead — the widget then neither renders its
floating launcher nor binds a listener, leaving this component the single
owner of the click while open/close events keep aria-expanded honest.
* fix(docs): stop double-writing the trigger's expanded state
toggle() emits open/close synchronously, so onWidgetEvent has already
updated isOpen by the time the click handler returns. The extra manual
flip inverted it: after the second click the panel was closed while the
button still reported aria-expanded=true. The post-mount assignment was
redundant for the same reason (mount({open:true}) emits 'open').
Widget events are now the single writer of isOpen.
* fix(storage): keep non-memory appends free of memory trailers
ContentWriteCoordinator._write_in_place routed every append through
MemoryFileUtils, which strips the existing trailing newline and appends
a reserved MEMORY_FIELDS metadata trailer, even for resource/skill files
where MEMORY_FIELDS is not a reserved format (see content_visibility).
Append to non-memory files now concatenates raw content instead, matching
POSIX append semantics and the documented visibility rules.
* feat(mcp): add write tool with exact-string edit support
Agents could not use viking:// as a working directory through MCP: no
tool could create or update file content. Add a write tool covering full
writes (mode=replace as create-or-overwrite, append, strict create) and
targeted edits (a list of {old_string, new_string, replace_all}
exact-string replacements applied in order, all-or-nothing), following
the Write/Edit conventions of common agent harnesses.
Edits read via read_visible and write back through the content-write
coordinator, so memory metadata trailers are preserved and semantic /
vector re-indexing triggers as with any other write. Parent directories
are created automatically by the storage layer. Descriptions spell out
writable scopes (resources, user memories/resources, agent) and the
wait=true knob for read-after-write search consistency.
Also update the stale tool-count comment in app.py and the MCP tool
tables in the en/zh guides (13 -> 14 tools).
* feat(mcp): add tree tool, split targeted edits into edit tool
tree renders the recursive directory tree under a viking:// URI,
indented by depth with file sizes, for whole-layout orientation;
level_limit/node_limit bound the output and include_abstract adds
per-file summaries. Missing directories report "(nothing under ...)"
instead of an error, matching the read tool's convention.
edit(uri, old_string, new_string, replace_all) takes over the targeted
exact-string replacement that previously lived in write's edits array,
matching the classic Edit tool signature harnesses already train on.
write now only does full-content writes (content + mode), removing the
mutually-exclusive content/edits schema ambiguity. Edits still read via
read_visible and write back through the content-write coordinator, so
memory metadata trailers are preserved and re-indexing triggers as with
any other write.
* test(plugin): update canonical MCP tool list for tree/write/edit
The marketplace test pins the server-registered MCP tool list; add the
new tree, write, and edit tools to fix plugin-tests CI.
* feat(storage): support plain files at the user scope root
Agents treating viking:// as a working directory naturally drop files
like viking://user/zeus-persona.md at the user root, but the write
coordinator only accepted the memories/ and resources/ subtrees.
Two changes make that work:
- Namespace shorthand: a dotted first segment under viking://user/ is a
file name, not a user id (canonical user ids are dot-free by
convention), so viking://user/zeus-persona.md now canonicalizes to
viking://user/<current-user>/zeus-persona.md, matching how the
reserved memories/resources/skills segments already shorthand.
Dot-free segments still address an explicit user, and an exact match
with the current user id still wins.
- Coordinator: plain files directly under the user root (or in
non-managed subdirectories) anchor their semantic refresh at the
parent directory. The managed subtrees skills/, peers/, privacy/ and
sessions/ remain read-only with an actionable error message.
* fix(namespace): narrow user-root shorthand to text-file extensions
Review on #3936 (codex /review-pr) flagged that treating any dotted
segment as a user-root file shorthand would silently re-route canonical
URIs for valid dotted user ids (e.g. alice.smith) into the current
user space. Shorthand now triggers only when the first segment ends
in a common text-file extension; dotted or email-style user ids keep
resolving as canonical user ids. Adds regression tests pinning both
behaviors.
* fix(mcp): resolve user URIs against current user
* test(mcp): pin plain-file writes directly at the user root
The user-root shorthand exists so an agent can drop viking://user/persona.md
into its workspace, but every new test went through an intermediate directory
(viking://user/project/zeus-persona.md), leaving the no-directory shape — the
one that anchors the write coordinator's refresh at the user root itself —
uncovered. Add the missing case.
Also correct the write tool docstring: the create-extension allowlist applies
to any newly created file, including one created by mode="replace" falling
back to create, not only to an explicit mode="create".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Replace the fixed 50 ms retry loop with bounded exponential backoff and jitter while preserving the original first-retry latency floor. Clamp each delay to the remaining timeout so retry sleeps do not add avoidable timeout overshoot.
Add deterministic interval and polling-reduction checks plus a four-waiter contention regression. The 10-second worst-case schedule drops retry probes from 200 to 29 while all waiters still acquire after the holder releases.
Tests: cargo test -p ragfs --lib lock:: -- --nocapture
* fix(pathlock): treat localfs read ENOENT as NotFound
Map ENOENT from localfs read back to NotFound when a file disappears
between metadata() and fs::read().
This avoids misclassifying lock-file delete races as plugin I/O errors
in pathlock token reads, so missing .path.ovlock is handled as an
expected absence instead of a fatal lock I/O failure.
* fix(pathlock): treat localfs read ENOENT as NotFound
Map ENOENT from localfs read back to NotFound when a file disappears
between metadata() and fs::read().
Coding-agent plugins capped a tool part's `tool_output` at 2000 chars before
POSTing it to `/api/v1/sessions/{id}/messages`. That cap sits below the server's
own externalization threshold (`tool_output_externalization.threshold_chars`,
default 20000), so output in the 2k-20k band was destroyed for no reason and
anything larger never reached `ToolResultStore` - leaving `tool_output_ref`
permanently empty and the `/tool-results` read-back path unusable.
Raise the `captureToolMaxChars` default to 1000000 (a guard against pathological
payloads, not a truncation policy) and lift the opencode/pi clamps that would
otherwise pin it back to 20000. claude-code had no knob at all - two hardcoded
`TOOL_OUTPUT_PART_MAX_CHARS = 2000` constants - so it gains the same config
entry and both capture scripts now read it.
Also stop pi from sending tool output twice: for a tool-only payload the
rawText-derived text part re-rendered the same output the tool part carries.
Frontmatter was parsed into ParseResult.meta and removed from the body, but
that metadata is never persisted, so every ingested markdown file lost its
frontmatter with no way to read the fields back.
Parse frontmatter into meta unconditionally (it still drives doc_title) and
only remove it from the stored body when explicitly configured; that removal
is now off by default.