* ci: persist BuildKit mount caches across runs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: key mount caches on native-build inputs, drop npm/uv from cache-map
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
DSH marks session-backed subagents with a durable origin field, while the OpenViking plugin previously enrolled every session in profile, recall, capture, and teardown commit. Add one opt-in boundary at the plugin hooks so operators can exclude those child sessions without changing existing installations.
Constraint: DSH classifies supported child sessions through SessionHeader.origin=subagent
Rejected: Independent capture and recall filters | teardown commit semantics would require per-state policy and a broader runtime refactor
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not classify from parentSession because human-driven forks may also carry lineage
Tested: Node 22.19 and Node 24 plugin checks; 341-pass memory-plugin matrix; npm pack dry-run
Not-tested: Real DSH CLI to OpenViking server E2E; unclassified timer or cron sessions
Co-authored-by: czyyyy <255856754+kwistzzqq-byte@users.noreply.github.com>
ZCode parses config-file hooks with strict zod rules (matcher:string().min(1),
groups .strict()). Empty-string matchers fail validation and safeParse drops
the ENTIRE user hook source silently — hooks never fire while every component
looks healthy. Omitted matcher matches all events, preserving semantics.
Incident reproduction and bundle-extraction provenance documented in operator
workspace: projects/zcode-ov-dsh-plugin/docs/post-install-verification.md
Co-authored-by: SearXNG Agent <agent@searxng.local>
* feat(admin): support wildcard matching in list-accounts and list-users
Add fnmatch-based wildcard filtering to the admin list-accounts operation,
matching the existing semantics of list-users (case-sensitive, substring
via *x*). Both operations now accept an optional name filter and limit.
- server: get_accounts(name_filter, limit) using fnmatch; list_accounts
router exposes `name` and `limit` query params
- ov_cli: add --name/--limit flags to list-accounts
- SDKs (python/go/typescript): thread filter params through admin clients
- docs: document the new filtering params for the admin API
Also fix two pre-existing, unrelated test failures (test-only):
- test_legacy_cleanup_removes_only_legacy_namespaces: update to the
per-agent-id cleanup contract introduced in 0102a48c
- test_remove_user: raise _wait_for_task budget to accommodate the
~2s AGFS recursive cleanup during user deletion
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* refactor(admin): drop limit param from list-accounts, keep name filter
The account list is consumed by callers that fetch the full set and scan
it (vikingbot root-key namespace-policy lookup, config-wizard account
menu, migration registry checks). Defaulting the HTTP route to limit=100
would silently truncate those consumers once an install has >100
accounts. The original requirement was only wildcard name matching, so
remove the limit param from the account path across route, CLI, SDKs and
manager, keeping only the fnmatch `name` filter. list-users is unchanged
(its limit predates this work and its consumers pass explicit large caps).
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* feat(admin): add opt-in limit/page pagination to list-accounts and list-users
Both endpoints now return results in lexicographic order of ID and accept
optional limit/page query params. Pagination is opt-in: omitting limit
returns the full set, so internal full-set consumers stay unaffected.
Co-authored-by: TRAE CLI <traecli@bytedance.com>
---------
Co-authored-by: TRAE CLI <traecli@bytedance.com>
* fix: web url deny error code
* feat: support feishu_app_id in args
* feat: connector task add internal task tag
* fix: remove connector extra meta info
* feat: watch api add source_type
* fix: terminate Git preflight process groups on timeout
* fix: internal task meta
* feat(memory-plugin): add ov-memory-doctor skill and diagnostics script for Claude Code and Codex
* docs(memory-plugin): link docs and mark the Volcengine-hosted service in the doctor skill
* feat(memory-plugin): add a Server health section to the doctor for local deployments
When the resolved url is loopback the doctor now inspects the server side:
ov.conf startup blockers (plugin-only keys the server rejects, dev mode on a
non-loopback bind, empty root_api_key, port mismatch, relative workspace,
unexpanded $VAR secrets, provider credential rules), the server process and
port owner (pid file, lsof/ss, docker container and its /app/.openviking
mount), the vector index's recorded embedding vs the configured one, the
server log when log.output is a file, and GET /ready. Remote servers get the
/ready probe only. The docker pending_initialization stub is recognised in
the Connection section. Skills, references and READMEs describe the new
section; provider-level validation stays with openviking-server doctor.
* refactor(memory-plugin): trim the doctor's Server health section to the port, plugin-only ov.conf keys and /ready
The section replicated the server's own config validation (top-level and
server.* key allowlists, provider credential rules, vlm, workers) and inspected
the pid file, docker mounts, systemd, the vector collection metadata and the
server log. All of that is what openviking-server reports itself at startup or
what `openviking-server doctor` covers, and the allowlists would drift with
every new config field. Keep what the server cannot tell the client: whether
anything listens on the port, the plugin-only ov.conf keys the server refuses
to start on, and GET /ready.
doctor-core.mjs is now synced only to the plugins that ship a doctor script;
the opencode and zcode copies were never imported.
Rewrite of #2319 against current main. The original patch no longer
applies: mcp_endpoint.py grew from 977 to 1471 lines, the upload route
was renamed from temp_upload_signed to temp_upload?token=, and
map_bind_host_to_loopback landed after that PR's base. The
_is_loopback_authority helper is taken verbatim from the original; the
branch replacement, docstring, and test assertions were redone against
main.
Co-authored-by: hinotoi-agent <paperlantern.agent@gmail.com>
The async-path test asserted the parent hook exits within a hard-coded
650ms wall-clock budget. On loaded CI runners the node cold start plus
ESM module-graph load alone can exceed that budget (observed 4.8s on a
contended runner), making the test flaky without any real regression.
Drop the wall-clock latency budget and keep the two assertions that
actually pin the async contract:
- completedResponses === 0: every server response is delayed 700ms, so
a parent that exits having completed none provably never awaited the
network. A synchronous fallback completes both before exiting, so the
degenerate path is still caught (verified by forcing maybeDetach to
return false).
- elapsed < OPENVIKING_TIMEOUT_MS: retained as a hang guard only.
Also raise the detached-worker waitFor budget from 5s to 20s: on a slow
runner the worker needs cold start + two 700ms-delayed responses + state
writes, which can exceed the old 5s default.
Co-authored-by: mac <bishopapril850965@yahoo.com>
Route MCP requests through the same request-context checks as REST so root API keys cannot bypass data-plane restrictions via /mcp while user keys continue to work.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Normalize bare-array query planner payloads before reading query metadata so valid planner output does not crash semantic search.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
findVikingUri() checked the path-like keys and then swept every remaining
argument value, so a local write or edit whose CONTENT merely mentioned a
viking URI was denied and no file was created:
write { file_path: "/home/me/notes.md",
content: "docs say viking://user/default/ is virtual" } -> deny
The sweep still runs — it is what catches an unusual or nested path key — but
it now skips arguments that carry content rather than a location
(content, new_string, old_string, file_text, ...). A URI in file_path, path,
uri, an unknown nested path key, or a bash command still denies.
Vendored copies regenerated with examples/memory-plugin-shared/sync.mjs.
Use a stable Node command for the DSH stdio MCP proxy so Electron desktop hosts do not try to spawn their own app binary as the proxy runtime.
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
* fix(plugin): honor explicit recall context timeout
Let operator-configured recallContextTimeoutMs apply even when context recall skips rewrite and query expansion, so low-latency configs can still extend the request deadline explicitly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(plugin): sync recall timeout override
Keep the explicit recall context timeout behavior in the shared plugin source so generated plugin copies stay synchronized.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#4058): [Bug]: Codex memory plugin replays historical turns after resume or transcript compaction
Fixes#4058
Ref: https://github.com/volcengine/OpenViking/issues/4058
* fix(codex): retire committed cursors and keep activity-based concurrency
Preserving the transcript cursor after a commit stops the replay, but it also
means nothing deletes state files any more: clearState() lost its last caller,
so every codex session — including ones that never captured a turn — leaves a
file behind, and listStates() reads all of them on every SessionStart.
The sweep now retires cursor-only states in the same pass: a real cursor is
kept for resume until OPENVIKING_CODEX_COMMITTED_TTL_MS (default 30 days, past
the life of the codex rollout it indexes), and a state that never captured
anything goes on the idle schedule, which is what the old sweep did with it.
Releasing ovSessionId also wrote lastUpdatedAt, making a committed session look
freshly active; saveState() takes touch:false so the field keeps meaning "last
transcript activity" for both the active window and retention.
Requiring a live ovSessionId to count as recently-active made the heuristic
miss sessions PreCompact had just committed, which can still be running: the
count is back on activity alone, and only a state with a live session is
committed.
Also name the shrink predicate: role === "user" covers tool results too
(normalizeCaptureRole maps them onto the user role), so findLastHumanTurnIndex
requires a text part, and the no-human-turn fallback to a full replay is now
visible in the log instead of silent.
---------
Co-authored-by: 7487 <1042653432@qq.com>
updateStatus() called ctx.ui.setStatus(status) with a single argument,
but the pi extension API signature is setStatus(key, text). With a
single argument the status string becomes the key and the text is
undefined, which clears the status entry instead of setting it -- so
the OpenViking status line never shows on pi 0.84.x, silently.
Pass "openviking" as the key so the status text actually renders.
Co-authored-by: veryvideo <veryvideo@users.noreply.github.com>
* fix(rerank): send top_n in OpenAI-compatible rerank requests
Some providers (e.g. Scaleway) require top_n and otherwise return 400;
_rerank_scores then silently falls back to vector-only ordering.
Align with CohereRerankClient by sending top_n=len(documents).
Fixes#4027
* test(rerank): assert OpenAI flat body includes top_n
* fix(rerank): send top_n in OpenAI flat request body
Fixes#4027
* test(rerank): assert flat OpenAI body includes top_n
* fix(rerank): send top_n in OpenAI flat request body
Fixes#4027
* test(rerank): assert flat OpenAI body includes top_n
---------
Co-authored-by: ktz03 <ktzhe036@gmail.com>
* docs: add anydoc office converter design
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(parse): add AnydocConfig and anydoc adapter skeleton
Register anydoc parser config with firecrawl-anydoc dependency and a small
attribute adapter for binding name normalization ahead of converter wiring.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(parse): add anydoc conversion core
Serialize anydoc documents to GFM while preserving embedded images through the existing storage media pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(parse): address Task 2 review findings
Move inline-code backslash escaping out of the f-string expression for
Python 3.10/3.11 compatibility, and stop tracking the SDD task report
in the product tree.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(parse): wire Word and legacy Doc to anydoc
Route Word and real OLE documents through the shared converter while preserving configurable legacy fallbacks and OOXML disguise handling.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(parse): honor anydoc config and safe fallbacks
Wire application parser settings into the default registry and prevent unsupported ODT/RTF files from reaching python-docx.
* feat(parse): wire PowerPoint and EPUB to anydoc
Route supported presentation and EPUB formats through the shared converter while preserving safe format-specific legacy fallbacks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(parse): wire Excel parser to anydoc
Route modern spreadsheet formats through anydoc with safe row truncation while preserving legacy process-pool conversion when anydoc is disabled.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(parse): preserve Excel ingestion options
Forward directory parse settings through the anydoc path and make skipped row truncation observable.
* docs(parse): document anydoc Office support
Reflect the expanded Office and EPUB format coverage while keeping PDF behavior explicitly unchanged.
* fix(parse): address final anydoc review findings
Resolve signatureless CSV conversion and preserve ingestion options across Office parsers while documenting and testing XLSB behavior.
* feat: unify office parsing with anydoc
* refactor: simplify anydoc renderer organization
* fix(parse): align anydoc parser config switch
* fix(anydoc): preserve legacy parser compatibility
* fix(anydoc): restore legacy safeguards
* refactor(anydoc): keep Office parsing on the unified path
* fix(anydoc): preserve config and benchmark compatibility
* fix(markdown): isolate link rewrite state per parse
---------
Co-authored-by: 张剑锋 <zhangjianfeng@ydjdev.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
* feat(cli): download directories as zip archives
* fix(download): cap directory archives
* fix(download): bound directory archives while they are built
The archive size cap was only enforced by `os.path.getsize()` after the
whole ZIP had been written, and `actual_total` counts file payload bytes
only. A tree made of empty directories or empty files therefore adds
per-entry ZIP headers that no check sees until the temp file is already
complete: with the limit set to 1 KiB, a 20k-entry tree writes 1.9 MB to
disk before being rejected.
Check the live write offset after every member so the temp archive stays
within the limit as it grows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CneCiyRjLaeDRYSWKngcJ
* docs(download): make the KG snippet re-runnable and sync the API catalog
The new knowledge-graph snippet extracts with a plain `unzip`, but the
note above it only tells the reader to delete the archive. Re-running it
leaves the previously extracted `./journal-kg/` in place, so `unzip`
stops at an overwrite prompt — and in a non-interactive shell it exits 1
without extracting anything. Use `unzip -o` and say what the note
actually has to cover.
Also update the endpoint catalog in api/01-overview.md, which still
described /content/download as file-bytes only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CneCiyRjLaeDRYSWKngcJ
* fix(download): build directory archives in memory, not in a temp file
The temp archive is handed to FileResponse with a BackgroundTask that
unlinks it, but starlette runs `background` only after a successful
send. Both Range-header error branches (starlette/responses.py:370,373)
`return await PlainTextResponse(...)` before reaching it, so a malformed
or unsatisfiable Range leaks the archive permanently — 22 such requests
leak 22 files in a local repro, up to 10 MiB each, with nothing to
reclaim them. asyncio.CancelledError misses the `except Exception`
cleanup for the same reason.
Since the archive is capped at 10 MiB anyway, build it in a BytesIO and
return it as a plain Response, exactly like the single-file branch. That
drops the temp file, the cleanup callback, and the tempfile/os/
FileResponse/BackgroundTask imports, and gives both branches the same
`Content-Disposition: attachment; filename*=UTF-8''...` form instead of
two different ones.
Directory downloads no longer honour Range. They never usefully did:
the archive is rebuilt per request and zipfile stamps time.localtime()
into every member, so resuming a range spliced two different archives.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CneCiyRjLaeDRYSWKngcJ
* fix(download): return 413 for oversized directory archives
RESOURCE_EXHAUSTED maps to 429, which tells clients the request is
rate-limited and worth retrying after a backoff. An archive over the
10 MiB cap fails because of the directory's own size, so every retry
re-walks the tree and re-zips it before failing again.
Add PAYLOAD_TOO_LARGE / 413 and raise it from the archive size check.
The code is plumbed through both status<->code maps (server app and
utils), the client's code->exception table, and the Rust CLI's status
mapping, so an over-cap `ov get` still surfaces a typed error rather
than falling through to INTERNAL.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CneCiyRjLaeDRYSWKngcJ
* feat(cli): write directory downloads as a named .zip in a target directory
`ov get viking://resources/myfolder ./myfolder` wrote the ZIP bytes to a
path named `myfolder` with no suffix: a regular file wearing a folder's
name, which `cd` rejects and `file` reports as ZIP data. The local path
was always used verbatim, so only the docs' hard-coded `./project.zip`
form produced a sane result.
Treat a target that is an existing directory — or omitted, meaning the
current directory — as the destination *directory*, and name the file
after the resource, appending `.zip` when the response came back as
`application/zip`. An explicit non-directory path is still used
verbatim, so `ov get <uri> ./explicit.zip` is unchanged. Nothing is
extracted; the archive is what lands.
get_bytes_with_type exposes the response Content-Type, which is how the
caller tells a raw file apart from a directory served as a ZIP;
get_bytes keeps its old signature for the TUI and its existing test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CneCiyRjLaeDRYSWKngcJ
* fix(download): bound archive entries and preflight targets
* fix(cli): preflight existing symlink targets
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>