Commit Graph
2352 Commits
Author SHA1 Message Date
chenxiaobin-monkeyandchenxiaobin.monkey 5e0754f371 feat: support local file to external-queue (#4412)
* feat: support local file to external-queue

* fix: update external parser

* fix: supply internal_task

---------

Co-authored-by: chenxiaobin.monkey <chenxiaobin.monkey@bytedance.com>
2026-08-28 16:19:42 +08:00
Hao Zhe e7f2fe5190 fix(studio): prevent retrieval description label overlap (#4431) v0.4.17 2026-08-28 16:12:23 +08:00
Zayn JarvisandClaude Fable 5 013b4a042e ci: persist BuildKit mount caches across runs (#4421)
* 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>
2026-08-28 12:54:17 +08:00
czyyyyandczyyyy 66c1656813 feat(dsh): prevent delegated sessions from contaminating user memory (#4382)
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>
2026-08-28 12:41:20 +08:00
AlmazandSearXNG Agent 028c2cb503 fix(zcode): omit empty-string matchers from hooks template (#4391)
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>
2026-08-28 12:41:00 +08:00
Zayn JarvisandZayn 22c6b21924 docs: document ov get raw download (#4124)
Co-authored-by: Zayn <zayn@users.noreply.github.com>
2026-08-28 11:47:01 +08:00
Jiahui ZhouandTRAE CLI 805633d5b8 Feat/memory extraction operation metrics (#4410)
* fix(metrics): measure volcengine embedding call latency

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

* feat(metrics): classify model call outcomes

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

* feat(metrics): expose model call error codes

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

* feat(metrics): tag memory extraction by type

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

---------

Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-08-28 11:44:43 +08:00
dingbenandTRAE CLI 953d0bb9f1 feat(admin): support wildcard matching in list-accounts and list-users (#4409)
* 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>
2026-08-28 11:26:23 +08:00
zihengli b8c89099be Fix/add resource (#4344)
* 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
2026-08-27 20:58:29 +08:00
zgy 248dab7d95 fix(embedding): report missing gemini dependency (#4402) 2026-08-27 19:52:36 +08:00
zgy e1bdd6c283 fix(doctor): validate vlm credentials config (#4406) 2026-08-27 19:52:07 +08:00
Axiomoth cf5cc308ca fix(codex): avoid stale actor peer in MCP proxy (#4400)
Signed-off-by: Axiomoth <alearner@splrad.com>
2026-08-27 18:33:20 +08:00
t0saki 206054cf15 feat(memory-plugin): add ov-memory-doctor skill and diagnostics script for Claude Code and Codex (#4389)
* 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.
2026-08-27 16:58:44 +08:00
t0saki 3b1db2082f fix(memory-plugin): setup wizard first-run path, proxy hint, config source reporting (#4387) 2026-08-27 16:31:20 +08:00
Kchenandchenpengfei c05e5bb987 fix(parse): 避免 no_split 重复创建临时目录 (#4385)
Co-authored-by: chenpengfei <chenpengfei@bytedance.com>
2026-08-27 13:36:42 +08:00
Zayn Jarvis 05ed1c94b7 Revert "fix(mcp): stop trusting request headers for signed upload URLs (#4379)" (#4384)
This reverts commit c775adc45a.
2026-08-27 12:53:12 +08:00
Zayn Jarvisandhinotoi-agent c775adc45a fix(mcp): stop trusting request headers for signed upload URLs (#4379)
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>
2026-08-27 12:46:32 +08:00
now-ingandmac 90405e2619 fix(zcode): make async stop test robust on slow CI runners (#4363)
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>
2026-08-27 11:25:48 +08:00
Onefly 4738df667e fix(web-studio): make context tree keyboard-operable (#4301) 2026-08-27 10:19:17 +08:00
Jiahui Zhou 9952ce1ec6 fix: create missing files on content write (#4360) 2026-08-26 22:18:14 +08:00
Jiahui ZhouandTRAE CLI b1780a4d39 feat(metrics): track memory extraction operation outcomes (#4358)
Co-authored-by: TRAE CLI <traecli@bytedance.com>
2026-08-26 21:04:47 +08:00
Qin Haojie fd35c06f0b fix(parser): reject empty Understanding files (#4356) 2026-08-26 20:47:49 +08:00
zgy 6e0aa34fab docs(resources): clarify add-resource target semantics (#4357) 2026-08-26 20:41:16 +08:00
Axiomoth f926353e3e fix(i18n): 完善 Studio 简体中文界面文案 (#4297)
* fix(i18n): complete Simplified Chinese Studio copy

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(i18n): localize upload failure fallbacks

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(studio): preserve localized task errors

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(i18n): refine Studio labels and status output

Signed-off-by: Axiomoth <alearner@splrad.com>

* docs: refine Web Studio internationalization guide

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(studio): address localization review feedback

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(studio): translate retrieval labels

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(studio): address maintainer review feedback

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(web-studio): use theme-aware error colors

Signed-off-by: Axiomoth <alearner@splrad.com>

---------

Signed-off-by: Axiomoth <alearner@splrad.com>
2026-08-26 18:27:08 +08:00
Qin Haojie afc54b01e8 fix(session): 避免 Session 锁阻塞归档写入 (#4350) 2026-08-26 16:19:04 +08:00
agent 1bb629461b fix(agent-evolution): recognize namespaced MCP reads (#4349) 2026-08-26 16:13:45 +08:00
Axiomoth cd9d4fa9b7 fix(docker): 补充 Studio 校验诊断信息 (#4348)
Signed-off-by: Axiomoth <alearner@splrad.com>
2026-08-26 16:05:51 +08:00
Onefly e1c8dceffe fix(privacy): avoid replacing bare values in prose (#3905) 2026-08-26 14:50:25 +08:00
Yu ZhangandClaude Sonnet 4.6 noreply@anthropic.com 96ca75326b fix(mcp): apply api-key context checks (#4247)
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
2026-08-26 14:49:57 +08:00
Axiomoth 124ea380f2 fix(docker): 在运行时校验 Studio 安装包 (#4340)
* fix(docker): 在运行时校验 Studio 资源

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(docker): 使用模块规范定位 Studio 资源

Signed-off-by: Axiomoth <alearner@splrad.com>

* fix(docker): 显式校验 Studio 包目录

Signed-off-by: Axiomoth <alearner@splrad.com>

---------

Signed-off-by: Axiomoth <alearner@splrad.com>
2026-08-26 14:40:21 +08:00
Wu Shuwen cf553ff748 fix(memory): index generated directory overviews (#4300) 2026-08-26 14:38:45 +08:00
Yu ZhangandClaude Sonnet 4.6 noreply@anthropic.com 0536518229 fix(retrieve): accept array intent planner responses (#4250)
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
2026-08-26 14:37:01 +08:00
Zayn Jarvis ea717aa05b fix(uri): resolve home alias for root requests (#4342) 2026-08-26 14:26:35 +08:00
Zayn Jarvis 028d34a0c8 Revert "fix(dsh): launch MCP proxy with node command (#4263)" (#4343)
* Revert "fix(dsh): launch MCP proxy with node command (#4263)"

* Revert "fix(dsh): launch MCP proxy with node command (#4263)"
2026-08-26 14:04:49 +08:00
Nguyen Thanh Dat 24185a0848 fix(memory-plugin): stop the uri-guard from reading file content as a path (#4188) (#4233)
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.
2026-08-26 12:33:59 +08:00
Yu ZhangandClaude Sonnet 4.6 noreply@anthropic.com 26aae04a85 fix(dsh): launch MCP proxy with node command (#4263)
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
2026-08-26 12:05:23 +08:00
Yu ZhangandClaude Sonnet 4.6 5356ced5ba fix(plugin): honor explicit recall context timeout (#4256)
* 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>
2026-08-26 12:01:07 +08:00
Zayn Jarvis 193d137ba8 fix(observer): ignore drained queue errors in health (#4327) 2026-08-26 11:47:06 +08:00
t0sakiand7487 a61c57bf59 fix(codex): preserve transcript cursors across commit and compaction (#4191)
* 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>
2026-08-26 11:44:40 +08:00
GengCongandveryvideo 1f7294a1b7 fix(pi): pass status key to setStatus so the status line renders (#4331)
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>
2026-08-26 11:29:05 +08:00
Axiomoth d933667c7e fix(docker): 显式构建并校验 Studio (#4305)
Signed-off-by: Axiomoth <alearner@splrad.com>
2026-08-26 11:20:50 +08:00
Wu Shuwen 0f68a12dd6 fix(sdk/go): forward remote sources on Windows (#4285) 2026-08-26 11:20:27 +08:00
ktz03andktz03 687167f1ac fix(rerank): send top_n in OpenAI-compatible rerank requests (#4040)
* 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>
2026-08-26 11:19:21 +08:00
Balachandar Ramakrishnan 3964d8685c fix(memory): preserve extracted replace fields (#4206) 2026-08-25 21:46:11 +08:00
zgy 80f4ba1412 fix(sdk): align compat client with SDK behavior (#4316)
* fix(sdk): normalize compat message parts

* fix(sdk): inherit normalized add_message in compat client

* fix(sdk): reuse base HTTP initialization in compat client
2026-08-25 21:08:54 +08:00
yufeng c3f3fb499e fix: normalize agent logos and guide rendering (#4325) 2026-08-25 21:01:43 +08:00
yufeng 156de74caf docs: add DeepSeek Harness, WorkBuddy, and Doubao guides (#4321) 2026-08-25 20:05:52 +08:00
Zayn Jarvis 9b7bb49634 Revert "feat(cli): download directories as zip archives (#4262)" (#4320)
This reverts commit 18d6805b81.
2026-08-25 18:39:59 +08:00
7ee7561121 refactor(parser):Unify Office Document Parsing with AnyDoc and Restore Compatibility Safeguards (#4279)
* 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>
2026-08-25 18:28:54 +08:00
Zayn JarvisandClaude Fable 5 18d6805b81 feat(cli): download directories as zip archives (#4262)
* 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>
2026-08-25 18:16:34 +08:00