* 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>
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.
* 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>
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.
parse() resolves SecretRef values to plain strings before returning, but the
parsed type still inherited string | OpenVikingSecretRef from the input type,
breaking tsc -p tsconfig.build.json (release prepack) since #3618.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Derive the statusline recall count from unique viking:// references in the injected context and bump the plugin to 0.4.4.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Keep retention options when retryable Claude session commits are queued and replayed. Add coverage for retryable storage conflicts.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Treat messages.jsonl as the materialization boundary for session-aware recall,
repair partial session roots during the existing authoritative append path,
and preserve Claude capture cursors when writes never reach the server.
Also replay explicitly retryable storage conflicts across memory plugins.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(retrieval): honor tier ceilings and stop cooling unserved recalls
Follow-up to #3534, from its post-merge review round.
- The abstract-to-overview substitute now applies only to categories whose
stored abstract is the whole file body. A resource or skill whose abstract is
missing (`processing_mode=vectors_only`) or over the per-entry cap read its
body and returned an overview instead, which for a short file is the body
almost verbatim — crossing the opt-in deepening boundary those categories are
documented to have, and doing it even under an explicit `detail="abstract"`.
They now degrade to a bare URI and their body is never read.
- A digest reporting `no_relevant` blanks `rendered`, so the client injects
nothing, yet those URIs still entered the dedup ledger and were cooled for
`dedup_turns` turns. That contradicted the ledger's own bare-URI grace rule
and held memories back from the later turn they were relevant to.
- Flat retrieval reaches built-in memory types outside the four named ones
(`cases`, `patterns`, `tools`, `trajectories`, skill-usage memories) and
reported them as an undeclared `memories` category that no tier or penalty
table covered, so other-peer hits skipped the score penalty and callers could
not pin their tier. The catch-all is now a declared category with both; it
stays out of `quotas`, whose buckets it would overlap. Skill-usage memories
also stop being misread as the `skills` category.
- ZCode, OpenCode and pi own an OV session id but did not forward it, so their
recalls silently ran without query expansion or cross-turn dedup.
- The context-request deadline covered only the server's 30s rewrite fuse, but
the pipeline is serial: expansion, retrieval and budgeting all precede it.
45s covers both fuses and the work between them.
- `plugin` config scope and the `/recall` successor example now match what the
code actually does.
* fix(retrieval): make the context deadline and expansion opt-out reachable
Forwarding a session id turns on server-side query expansion, an LLM call with
its own 5s fuse, but neither the deadline that was supposed to cover it nor the
switch that turns it off reached the two harnesses this PR newly enabled it for.
- `contextRequestTimeoutMs()` now derives the deadline from the request body
rather than from `cfg` plus a rewrite flag. The body is what states which
server stages will run: a session takes the expansion fuse, `rewrite` takes
the digest fuse, and a bare retrieval takes neither and keeps the caller's own
budget. Reading `cfg` alone could not tell those apart.
- OpenCode pinned `timeoutMs: 5000` after spreading the helper's options and pi
ignored them entirely, so the helper's deadline was dead code in both. Their
own budgets are now defaults rather than ceilings. OpenCode's 5s in particular
was shorter than the expansion fuse it had just enabled, so a legal request
would have been aborted client-side and dropped back to the path with neither
dedup nor expansion.
- OpenCode and pi read `OPENVIKING_RECALL_QUERY_EXPANSION` (and
`recallQueryExpansion` in their own config files) and set the `configured`
flag the shared body builder requires, so the documented opt-out exists where
the cost was introduced.
- The integration overview no longer implies every harness reads the same
environment knobs, and describes the deadline as per-stage rather than
rewrite-only.
* feat(retrieval): assemble auto-recall context server-side via /search mode="context"
Auto-recall assembly lived in every harness plugin: each one searched per
memory type, read hits back one by one, and stitched a context block with its
own budget and degradation rules. The implementations drifted, and the shared
weaknesses showed up in production injections — roughly half of the entries
degraded to a bare URI plus a score, character budgets distorted up to 6x on
CJK text, and adjacent turns re-injected the same memories.
This moves assembly into the server as one round trip. /find stays an unchanged
stateless primitive. /search gains mode="context" (mode="list" is the default
and byte-identical to before), and /recall becomes a thin preset over the same
kernel with its v1 field names folded onto the new contract.
New assembly kernel under openviking/retrieve/context_assembler/:
- Token budgeting with a CJK-aware estimate replaces the character budget.
- detail="auto" fills breadth-first then deepens: every candidate gets a
readable floor, then overview, then full for high-scoring entries. An
oversized tier falls back to the previous one instead of being truncated,
bounded by max_tokens / candidates * 2 per entry.
- Overview extraction dispatches by source: memory files use their leading
Summary section, code files reuse code_outline signatures, long documents use
a heading tree plus first paragraph.
- Directory hits start at overview and read their .overview.md sidecar, since
directories carry no stored abstract; their full tier stays capped at
overview. v1 injected the sidecar as if it were a whole file.
- Quotas generalize beyond memory types to resources and skills, with purpose
presets supplying ratios when quotas are absent.
- dedup_turns keeps a per-session ledger at {session_uri}/.recall_log.json so
every harness inherits cross-turn dedup; exclude_uris remains as the
stateless fallback.
- Rendering flattens to one <memory uri=... type=... score=... detail=...>
element per entry. Every tier carries its URI, so the model can always drill
down through the MCP read tool.
- Query expansion and digest rewriting are opt-in and fail closed: both have
timeout fuses, and a failed rewrite still returns the unrewritten block.
Retrieval failures are counted into stats rather than silently yielding an
empty block.
Plugins now send one context request, falling back to /recall and then to raw
find on older deployments, and cache that outcome so only the first turn pays
for the probe. The tri-state recallRewrite knob chooses between local host-CLI
compression and the server digest, and client-side settings move to a plugin
section in ovcli.conf.
* refactor(retrieval): give context tiers a per-category default
The tier ladder assumed `abstract` is a cheap summary. For memory files it
is not: the memory writer stores the whole stripped body in that scalar
because it doubles as the embedding text, so `abstract` costs the same as
`full` and the ladder runs `uri < overview < abstract = full`. Two of the
model's properties fell out of that: exempting `abstract` from the per-entry
cap let a single entry eat several times the budget, and `detail` — which
only ever set a ceiling — collapsed to two distinguishable behaviours across
its four values, since `auto` already allowed `full` for memory.
Tiers now come from a per-category constant table that treats the storage
shape as a given: `events` starts at overview (the one memory type whose
`# Summary` extraction is a real compression) and may deepen to full on
leftover budget; every other category is served at `abstract`, which for
memory already is the complete file at zero read cost and for resources and
skills is the generated 256-char summary. The table carries the note to move
`events` back to `abstract` once the writer stores a separate summary scalar.
Falling out of that: prefetch now reads only the candidates whose planned
tier needs a body rather than every candidate, `detail` becomes a real pin
(start and ceiling) and additionally accepts a per-category map, and
`full_score_threshold` is gone — leftover budget is spent in score order
instead of behind an absolute threshold the observed score band cannot
support. `auto` is still accepted on the wire as a synonym for "unset".
Assembly fixes found alongside:
- Removing the abstract cap exemption would turn an oversized abstract into
a bare URI, so it now falls back to overview first — for memory that is a
cheaper substitute, not a step up.
- Rewrite timeouts were reported as failures on Python 3.10, where
`asyncio.TimeoutError` is a separate class from the builtin.
- `stats.rewrite_usage` read `token_tracker` off `VLMConfig`, which has no
such attribute; usage was structurally always null. It now reads the model
instance's tracker and reports only when the call count moved by exactly
one, since that tracker is shared.
- A single malformed ledger record made every deduped recall in that session
fail, and the file was never rewritten, so it could not heal. Records are
now coerced on read and dropped on the next write, along with records left
ahead of the clock by an archive rotation.
- Entries served as a bare URI no longer enter the dedup cooldown: they lost
to budget pressure, not to the reader having already seen them.
- The render envelope only neutralised a literal `</memory>`, so a body could
forge a sibling entry with its own uri, type and score.
- Flat-mode gathering re-derived the category from the URI, reading
`viking://resources/backup/memories/events/log.md` as an event.
- Cooled and excluded URIs are compensated with extra rows, so a fully cooled
bucket falls through to the next-best hits instead of coming back empty.
- `/recall` quotas overlay the v1 bucket defaults again; `{"events": 5}` had
started dropping the other three buckets.
- The MCP `recall` signature sent its own defaults as if the caller had, which
resolved a different profile than `POST /recall`; an unknown `detail` value
raised `KeyError` through the whole call instead of degrading.
* feat(codex): inject profile context on session start
Reuse the shared profile builder for startup, clear, and resume hooks while preserving archive injection and orphan-session status output.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(retrieval): raise rewrite timeout default to 30s
* docs(agents): document low-latency recall settings
* fix(codex): prefer luna as recall compressor fallback
* refactor(plugins): unify recall compression setting
* feat(plugins): enable recall compression by default
* docs(agents): use absolute links in image docs
* fix(retrieval): address context assembly review feedback
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* test: trim redundant context assembly coverage
* fix(retrieval): address second-round context assembly review
- Drop the backticked `/search` from the deprecated-recall row in both API
overviews. The reference checker scans the whole row after the method cell
for backticked paths, so it read the description as a route named
`POST /search` and Build Docs failed on an unknown, undocumented route.
- Accept ovcli.conf's full field set in both Python readers. The file's schema
belongs to the Rust CLI, which writes `root_api_key`, `output`,
`echo_command`, `show_progress` and `verbose` and ignores unknown keys; the
two Python readers had drifted into stricter subsets, so the shipped example
already failed to load in both. Adding the new `plugin` section to a working
ovcli.conf would have broken `ov doctor` and every SDK client the same way.
- Return 400 from `mode="context"` for a request `mode="list"` also rejects.
Retrieval validates query and image_url before searching, and the gather
fuse swallowed that rejection along with genuine scope failures, so a body
of `{"mode":"context"}` came back 200 with an empty block instead of the
documented parameter error. Runtime failures still degrade into
`stats.retrieval_errors`.
- Let a context request that asks for a server-side digest outlast the
server's rewrite fuse. The plugin's ordinary 15s request timeout is shorter
than the 30s fuse, so a rewrite that finished inside its own budget was
aborted client-side, discarding the whole response — including the
uncompressed block the server returns when a rewrite fails — and falling
back to `/recall`. The deadline is only extended when the body actually
requests a rewrite, and `OPENVIKING_RECALL_CONTEXT_TIMEOUT_MS` /
`plugin.recallContextTimeoutMs` pins it.
* chore(plugins): sync shared modules into the zcode snapshot
* fix(retrieval): align context quotas and plugin defaults
Restore cross-domain coding recall, reuse authoritative actor resource
scopes, and make bucket quotas the sole width control in purpose mode.
Keep plugin defaults server-owned while preserving explicit legacy limit
settings through quota conversion.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* fix(retrieval): preserve recall compatibility
Restore the deprecated recall threshold default, distinguish successful empty rewrites from compressor failures, and document legacy quota floors across coding-agent plugins.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
---------
Co-authored-by: TRAE CLI <noreply@bytedance.com>
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>
Use ZCode rollout logs as the authoritative incremental source, advance capture state only for the acknowledged prefix, and persist host turn identity with the OpenViking turn_id contract.
Detach Stop writes, package ZCode in the TOS marketplace artifact, add end-to-end regressions, and move the integration docs under community plugins.
Co-authored-by: TRAE CLI <noreply@bytedance.com>
* feat(integrations): add ZCode memory plugin
Add examples/zcode-memory-plugin — a thin ZCode lifecycle adapter that reuses
the shared memory-plugin-shared runtime for recall, capture, commit, and MCP
proxy. No memory logic is duplicated.
Key design decisions (see docs/design/zcode-memory-plugin-design.md):
- Vendor shared runtime into scripts/shared/ via sync.mjs (self-contained plugin)
- 4 hook events only (SessionStart, UserPromptSubmit, PreToolUse, Stop) —
ZCode does not support PreCompact/SessionEnd/SubagentStart/SubagentStop
- Output schema: ZCode-canonical keys only (no Claude-Code 'decision: approve')
- Config-driven install: hooks + MCP merged into ~/.zcode/cli/config.json
- install.sh wiring: detection, TUI, validation, install, uninstall
Verified locally:
- 22/22 node:test cases pass (turns parser + hook output schema)
- sync.test.mjs passes
- install → uninstall cycle: hooks/MCP correctly written and cleaned
- URI guard denies viking:// paths with MCP redirect
- Capture writes to OV session with zc- prefix
Closes#3127
Related: #3442, #3544
* chore: remove non-essential files from PR, add .scratch to .gitignore
- Remove .scratch/ working notes (local ticket files, not codebase artifacts)
- Remove package.json and .gitignore from plugin dir (TRAE/Cursor don't have them)
- Add .scratch/ to root .gitignore
* fix(zcode): use verified ZCode field names + rollout fallback for capture
- Update zcode-turns.mjs to probe responseText/responsePreview (verified
from ZCode reverse-engineering in #3127 by @quinn-zenith) instead of
the TRAE-inferred last_assistant_message
- Add rollout file fallback: when stdin payload lacks user content (the
known ZCode limitation), read ~/.zcode/cli/rollout/model-io-sess-*.jsonl
to extract the last user+assistant pair from request.messages+response
- Fix concurrent session isolation: normalize sessionId→session_id in
zcode-hook.mjs before resolveNativeSessionId to prevent cwd-fallback
collision when two ZCode windows run in the same directory
- Add 2 new test cases for rollout fallback (14 turns tests total, 24 total)
- All 24 tests pass
* fix(zcode): address maintainer review blockers (config safety, MCP ownership, turnId)
Addresses 3 blockers from @huangruiteng's review (CHANGES_REQUESTED):
1. Config safety: distinguish ENOENT from parse errors — malformed
config.json now aborts instead of overwriting. Use backup+tmp+rename
for atomic writes.
2. MCP ownership: only replace/delete mcp.servers.openviking entries
tagged as openviking-memory. User-managed entries with the same name
are preserved on install and untouched on uninstall.
3. TurnId-based dedup: rollout entries carry monotonic turnId — now used
as the primary dedup key (capturedTurnIds set) instead of stableHash.
extractUnseenRolloutTurns scans ALL unseen entries since lastTurnId,
not just the last row — recovers missed turns after hook failure.
Fail-closed when no turns are found.
Also updates DESIGN.md to reflect verified field names (responseText/
responsePreview) and the turnId contract.
27/27 tests pass (was 24). Added 3 new rollout tests: incremental
capture with lastTurnId, multi-entry scan, turnId propagation.
* docs(zcode): update stale field name references in design spec
Update test case descriptions to match verified field names
(responseText/responsePreview instead of last_assistant_message)
and add rollout fallback + turnId test coverage descriptions.
* fix(zcode): dedup key includes role + first-capture returns all turns
Fix two bugs found in code review pass 2:
1. Assistant turns silently dropped: user and assistant from the same
rollout entry shared a turnId, so dedup via capturedTurnIds dropped
the assistant. Fix: dedup key is now ${turnId}:${role}, not turnId
alone. Regression test added.
2. First-capture data loss: when no lastKnownTurnId was set, only the
last rollout entry was returned, losing prior turns. Fix: first-time
capture now returns ALL entries.
Also: add backup step to config atomic write (copyFileSync before tmp+rename),
fix line width in zcode-turns.mjs, add 2 lifecycle tests (missed Stop
recovery, user+assistant same turnId).
29/29 tests pass (was 27).
* test(zcode): add concurrent session isolation tests
Two new test cases addressing maintainer criterion 4 (concurrent sessions):
1. Two sessions read their own rollout files — verifies session A cannot
see session B's content and vice versa (sentinel-based assertion)
2. Independent lastTurnId state per session — verifies incremental capture
progresses independently when one session has prior state and another
is fresh
31/31 tests pass (was 29).
* fix(zcode): correct rollout file path pattern (model-io-<sessionId>)
The rollout path used model-io-sess-${sessionId} but ZCode filenames are
model-io-<sessionId> where sessionId already includes the sess_ prefix.
This caused the rollout fallback to always miss the file and return empty,
defeating capture entirely in production.
Verified on live two-session ZCode setup:
- Session A (sess_8c6ce483): 2 messages, 2 commits
- Session B (sess_74759710): 2 messages, 2 commits
- No cross-contamination between sessions
31/31 tests pass. Updated all test rollout filename patterns.
* docs(zcode): fix stale rollout path in comments and DESIGN.md
Comments referenced model-io-sess-<sessionId> but actual pattern is
model-io-<sessionId> (fixed in code already, comments were stale).
---------
Co-authored-by: woshiguanxiaoliang <woshiguanxiaoliang@noreply.gitcode.com>
* feat: add audio and video understanding via VLM
* docs: design media resource guards
* fix: bound media staging concurrency
* fix: cap unknown-size media staging
* test: stage media in routing fake
* test: exercise media staging callbacks
* test: trim media understanding coverage
* chore: 清理实现计划文档
* fix: 修复多凭证切换问题
---------
Co-authored-by: Qin Haojie <qinhaojie.exe@bytedance.com>
- add storage.agfs.pathlock.lock_timeout_secs
- use pathlock default timeout instead of hardcoded zero in wrapper
- map legacy storage.transaction.lock_timeout when new config is unset
- remote redolog by using persistent `session_commit` queue.
Issue #3522 — the OpenClaw plugin's `config.apiKey` only accepted a plain
string, resolved through local `${ENV_VAR}` interpolation. `INSTALL*.md`
documented this as a known limitation: users who store their other OpenClaw
provider credentials (LLM, TTS, MCP servers) through the standard
`{source, id[, provider]}` SecretRef mechanism (env / file mount /
exec-backed vault such as 1Password, Vault, gopass) had to keep the
OpenViking key as cleartext inside `openclaw.json`.
### config.ts — `string | OpenVikingSecretRef` widening
* Add `OpenVikingSecretRef = "env"|"file"|"exec"` discriminated union type,
matching the shape OpenClaw core uses for its own credential fields
(`env` + `file` implemented in-plugin, `exec` forwarded to `child_process`
so providers like `@transmitt0r/openclaw-plugin-onepassword` can manage
the OpenViking key without SDK coupling).
* Add `resolveSecret()` resolver with explicit, actionable errors:
- env: unset var = throw, no silent empty fallback
- file: `~` expanded, UTF-8 read, whitespace trimmed; unreadable file
rethrows with the OpenViking field name prefixed so config misconfigs
surface with a clear label and path
- exec: lazy `require("node:child_process").execFileSync(provider,[id])`,
stdout trimmed, 15s timeout; errors prefixed with provider + id
- unknown source / missing id / missing exec provider = explicit throw
* `memoryOpenVikingConfigSchema.parse()` widens `rawApiKey` to
`string | OpenVikingSecretRef`, then passes it through
`resolveSecret(rawApiKey, "config.apiKey")` *before* the existing
`resolveEnvVars` pass. Plain strings transparently fall through
`resolveSecret` unchanged, so `${ENV_VAR}` interpolation is preserved
100% backward-compatibly.
* `OPENVIKING_API_KEY` env fallback is unchanged and triggers only when the
`apiKey` config key is absent — a user who deliberately sets `apiKey: ""`
still gets "" (explicitly unauthenticated), not the env fallback.
* `uiHints.apiKey.help` documents the SecretRef shape and recommends it.
### openclaw.plugin.json — widening schema + UI hints
* `configSchema.properties.apiKey` becomes `oneOf: [string, env ref, file ref, exec ref]`.
Each object variant has a `title`, `additionalProperties: false`,
`required`, and explicit description per field, so OpenClaw's config UI
can render them individually instead of showing a generic JSON object blob.
* `uiHints.apiKey.help` matches the new config.ts wording.
### INSTALL.md / INSTALL-ZH.md — SecretRef usage tables
Replace the old "plaintext / chmod 0600" caveat bullet with a 3-row table
(env / file / exec) showing example JSON + notes (Kubernetes secretKeyRef
mount for `file`, 1Password `op://` URL convention for `exec`). The
backward-compat string path is retained at the end of the new bullet so
existing deployments that haven't migrated yet still get the old permission
advice — no surprise behaviour for upgrading users.
### tests/ut/config.test.ts — SecretRef regression suite (10 new cases)
Under a new `describe("… SecretRef (#3522)")`:
1. Backward compat: `${OV_KEY}` interpolation still resolves.
2. env source — happy path with a fresh env var.
3. env source — unset var throws, no silent fallback.
4. file source — real `mkdtemp`-created file, trimmed whitespace. Cleanup
in `afterEach`.
5. file source — missing-path error message contains readable label + path.
6. exec source — `vi.spyOn(child_process.execFileSync)` asserts provider +
args, stdout trimmed.
7. exec source — missing `provider` field errors.
8. Schema validation — unknown `source` and missing `id` each throw with
error messages that name the problem.
9. Env fallback boundary — explicit `apiKey: ""` is NOT overridden by
OPENVIKING_API_KEY, but `apiKey` absent IS (backward-compat behaviour
contract pinned with a test so future refactors can't regress).
Covers every branch inside `resolveSecret()`, plus the backward-compat
contracts issue #3522 called out.
* feat: implement server-resolved OpenViking Assets manifests
Add the openviking-assets/1 declaration flow with server-owned configuration parsing and native Rust CLI execution.
- Resolve one flat Manifest against one Catalog through an authenticated server endpoint with strict schema and Git semantic validation.
- Reject recursive includes and unsafe clone URLs; return a resolved plan without submitting resources or running server-side batches.
- Keep local credential aliases, manifest state, dry-run, failure isolation, and per-asset create/sync orchestration in the CLI.
- Generate normalized stable asset identities on the server and remove the CLI direct SHA-1 dependency.
- Update flat examples and add server resolver/API plus Rust CLI coverage.
* feat: implement server-resolved OpenViking Assets manifests
* feat: implement server-resolved OpenViking Assets manifests
* fix(pathlock): tolerate missing lock token after recursive delete
* feat: implement server-resolved OpenViking Assets manifests
* feat: implement server-resolved OpenViking Assets manifests
* feat(connector): support more git like platform
* feat(connector): support more git like platform
* feat(connector): support more git like platform
* feat(connector): support more git like platform
* feat(connector): support more git like platform
Every harness memory plugin sent OpenViking requests under Node's default
`user-agent: node`, so server-side and gateway logs could not attribute
traffic to a harness or a plugin version.
Send `openviking-memory-<harness>/<version>` from all six harnesses
(claude-code, codex, opencode, pi, cursor, trae) on both the data plane
and the MCP proxy. Versions come from each harness's own manifest, or
from OPENVIKING_INTEGRATION_VERSION for cursor/trae; an unreadable
manifest degrades to 0.0.0 rather than throwing inside a short-lived hook.
The header is purely informational — neither the open-source server nor
the commercial gateway consumes it, and no auth or identity header
changes.
* feat(parse): add large image processing for image parser
- Add large_image_processor.py: detect large images (>10MB or >4096px),
create low-res previews, split into grid tiles, and generate grid
overlay images with tile labels
- Refactor ImageParser.parse() to integrate large image processing pipeline
- Enable SVG-to-PNG conversion in utils.py (cairosvg/wand)
- Rename ImageConfig.max_dimension to preview_max_dimension and add new
config fields: max_file_size_mb, max_tile_size_mb, max_tile_dimension_px,
tile_overlap_px, large_image_threshold_dimension
- Update ov.conf.example with new image config options
* fix(parse): correct tile dimension comment from 1024px to 2048px
* fix(parse): fix tile label path in grid overlay to include tiles/ directory
* fix(parse): register missing image extensions for ImageParser
TIFF, ICO, DIB, ICNS, SGI, JP2 were not in IMAGE_EXTENSIONS, causing
them to fallback to TextParser. All are supported by PIL.
* fix(parse): preserve PNG format for tiles instead of always converting to JPEG
* fix(parse): address review feedback for large image processing
- Wire config.image to ImageParser in ParserRegistry (was missing)
- Remove unnecessary preview creation for small images (broke LA mode PNG)
- Enforce max_tile_size_mb on tiles with quality reduction and resize fallback
- Remove 64-tile hard cap that conflicted with max_tile_dimension_px
- Add comment explaining why original file is not saved for large images
* refactor(parse): remove max_tile_size_mb as it is a soft suggestion
max_tile_size_mb was a soft constraint that was not enforced
consistently. Remove it from config, constants, and all enforcement
logic. Tile dimension (max_tile_dimension_px) remains the sole constraint.
* fix(parse): use CJK-capable font for grid overlay labels
The old font loading only tried macOS-specific paths and fell back to
PIL's default bitmap font, which cannot render CJK characters in
filenames. Add a cross-platform CJK font lookup that covers Linux
(Noto/Droid/WQY/DejaVu), macOS (PingFang), and Windows (MSYH/SimSun).
* fix(parse): convert non-VLM-supported image formats to PNG on save
Image formats like TIFF, ICO, DIB, ICNS, SGI, JP2 are not recognized
by VLM backends (OpenAI/LiteLLM/VolcEngine only support PNG/JPEG/GIF/
WebP/BMP) or by embedding_utils for image vectorization. When a file
with one of these extensions is parsed, convert it to PNG and use a
.png extension so that downstream pipelines see consistent data.
SVG files (already PNG-converted via cairosvg) also get the .png
extension for the same reason.
* fix(parse): import io for SVG conversion
---------
Co-authored-by: qin-ctx <qinhaojie.exe@bytedance.com>