Commit Graph
44 Commits
Author SHA1 Message Date
t0saki 37ef554bb2 refactor(plugins): converge the harness forks back onto the shared library (#4594)
* refactor(plugins): ship each harness only the shared modules it imports

`sync.mjs` grouped its lists by how they had grown rather than by what each target imports, so three modules travelled to plugins that never load them: `setup-wizard.mjs` reached dsh and zcode, neither of which ships a setup entry point, and `async-writer.mjs` reached opencode, which its host imports in-process and so has no hook subprocess to detach a write from. Split the groups by capability — the hook set, the wizard, the stdio proxy pair, the batch sender, the async write path — and give pi its own list, so a target's entry says which capabilities it has.

`sync.test.mjs` kept its own copy of those lists, and the copy had drifted: it was missing `plugin-config`, `retryable`, `recall-compress-core` and `mcp-proxy-config`, so a stale vendored copy of any of them would have passed CI. Import the lists from `sync.mjs` instead, guarding the sync behind an entrypoint check, and add the check the duplicate could never make: every module in `lib/` is claimed by some target, and no target holds a banner-carrying file the sync no longer ships.

* refactor(zcode): capture and shape the proxy config through the shared modules

zcode sent every turn it parsed straight to the server: no length cap, no acknowledgement filter, no slash-command or injected-status guard — the four things `shouldCaptureText` does for every other harness. It re-derived the proxy config object by hand too, which is how it came to watch a narrower set of credential files than `buildMcpProxyConfig` watches.

Route both through the shared modules. The dedup key stays keyed on the raw turn, so raising the cap later never resends a turn the server already holds in truncated form.

* refactor(pi): log through the shared JSON Lines logger

pi carried two copies of a hand-written `debugLog` — one in `index.ts`, one in `sync.ts` — that appended `<ISO timestamp> <message>` lines, read `OV_DEBUG_LOG` directly, and could only be turned on through the environment. Both are the shared `debug-log.mjs` with the structure taken out: no stage field, no JSON payload, no config knob, and a spelling of the variable no other harness uses.

Use `createLogger` in both places, add a `debugLogPath` config key so the log can be turned on the way every other pi setting is, and read `OPENVIKING_DEBUG_LOG` with `OV_DEBUG_LOG` kept as a deprecated alias so existing setups keep logging.

* fix(dsh): honor syncTurns on every write path, not just capture

`syncTurns: false` gated `capture()` alone, so a read-only session still committed on `turn/end`, still committed again on dispose, and still replayed whatever an earlier session had queued. The toggle promised no writes and made three.

Gate the commit paths and the replay on it too, and document it — the README and the integration page never mentioned the key at all. A backlog queued while capture was on stays on the queue for a session that still writes.

* chore(plugins): bump the zcode and dsh plugin versions

Both changed behavior in this branch — zcode now filters and truncates what it captures and watches the full credential set, dsh now writes nothing when `syncTurns` is off — and installed copies are keyed by version.

* fix(plugins): make the installer and the plugin test matrix work in a git worktree

`resolve_self_checkout` looked for `.git` as a directory. A linked worktree keeps it as a file pointing at the real gitdir, so `CHECKOUT_DIR` stayed empty there: `--source dev` resolved the marketplace to `/examples` and failed outright, and every other path fell through to `remote`, which clones from GitHub. On this machine that turned six of the thirteen installer tests red and made `release-marketplace.test.mjs` hang for twenty minutes on the network — long enough that the CPU starvation failed an unrelated recall timing assertion too. Test for existence instead of for a directory.

Three test files were never run by CI, so nothing noticed that one of them had gone stale: the pi wiring assertion still required `new RecallManager(...)` to end at the session-id getter, which stopped being true when the recall ledger was added a fourth argument. Assert only the getter, and register all three files in `pr.yml` — the matrix now covers every `*.test.mjs` under `examples/`.
2026-09-04 13:59:29 +08:00
t0saki 1d89f8d465 feat(plugins): derive the workspace peer from git, and let a repository carry its own config (#4595)
* fix(plugins): stop dropping ovcli.conf's plugin section, and unrot the sync test

`ov config add|edit`, the config wizard and `ov config switch` all rebuild
ovcli.conf from the `Config` struct, which has no `plugin` field and no
catch-all — so every write silently deleted the whole `plugin` section the
memory plugins own. `write_config_file` now carries over the top-level keys
`Config` does not model, `save_edited_config` reads them from the old name on a
rename, and `activate_config` keeps the active file's. Modeled keys are
deliberately not carried over: one that is `None` was cleared on purpose.
`KNOWN_CONFIG_KEYS` decides what counts as modeled, guarded by a test that
fails when a struct field is added without listing it.

sync.test.mjs kept its own copies of the target lists and they had drifted —
dsh, opencode and agent-plugins were each missing modules the sync ships, so a
stale vendored file passed CI. It now imports the lists from sync.mjs (whose
`main()` moved behind an entrypoint guard) and additionally fails on a module
no target claims or a banner-carrying orphan no target lists.

Also in this hygiene pass:

- postRecall dropped `peer_scope` on any 400/422, silently widening recall from
  the caller's own peer to the whole user root. It now retries only on an
  unknown-field rejection, remembers the downgrade so every turn stops paying
  for a rejected request, and both doctors warn while that memo is live.
- The session peer pins (Claude Code's `ws-peer-*.json`, Codex's
  `workspacePeerId`) carry a version, so a pin written under one derivation
  rule cannot outlive it. Derivation is unchanged, so today they re-derive to
  the same value.
- recall-session-wiring.test.mjs was never registered in CI and had rotted
  against a fourth RecallManager argument; assertion fixed and registered.

* feat(plugins): layered workspace configuration

A workspace can now carry `<root>/.openviking/config.json`, which a team
commits, and `config.local.json`, which stays private; a per-machine registry
under `~/.openviking/workspaces/` sits above both so the user keeps the last
word over any repository they clone. All three share one schema and one merge,
and they slot in exactly where ovcli.conf's `plugin` section already does, so
`OPENVIKING_*` still wins over everything.

Three modules, synced to all seven plugin targets:

- workspace-identity.mjs finds the workspace root and reads git's own idea of
  what the repository is called, using only filesystem reads. No `git`
  subprocess: hooks are fresh Node processes on prompt-level paths with budgets
  as tight as Codex's 3s SessionEnd, and this keeps working where git is absent
  from PATH or would refuse the repo over dubious ownership. Worktrees converge
  through `commondir`; submodules stay separate; `$HOME` and `/` are never
  workspace roots.
- workspace-config.mjs discovers, parses, filters and merges the layers, and
  records per-key provenance — which layer won and what it covered up.
- workspace-registry.mjs keeps one file per workspace rather than one listing
  them all, so concurrent hooks cannot lose each other's writes, and treats a
  path whose git identity has changed as a miss rather than inheriting the
  previous repository's peer.

These files are trusted without a prompt, because a hook is non-interactive and
any approval gate degrades into "run one command per workspace first". What is
refused instead is structural and costs nobody anything: connection and
credential keys are stripped loudly, `${VAR}` is never expanded, and
`cli_config_profile` — which decides which credentials reach which server — is
registry-only and name-only. What a committed file switches off is announced in
doctor rather than blocked.

An adversarial review pass over these three modules found 18 defects, all fixed
here and each now covered by a test. The one that mattered: `JSON.parse` keeps
`__proto__` as an own property, so a 128-byte committed file could write
straight into `Object.prototype`, and since `process.env` reads through the
prototype chain and the environment outranks ovcli.conf, that set
`OPENVIKING_URL` and `OPENVIKING_API_KEY` for the whole process — silently
shipping the user's real API key to an attacker's host. Prototype keys are now
dropped with a warning in both the strip and the merge. The rest: unbounded
recursion (a 4KB file could take out every sibling layer), the identity cache
storing a remote's embedded token at 0644, worktrees under a directory named
`modules` misread as submodules, the registry's negative-evidence check being
inert in its only caller, `Number(null)` pinning cost knobs to a bound, and
provenance lying when two layers disagree about a key's type.

`.gitignore` no longer ignores all of `.openviking/`, which would have stopped
a team's config.json from ever being committed; doctor warns when a workspace
still does. The schema maps `capture.commit_token_threshold`, matching the knob
the loaders actually read — the RFC's example named a turn-based one that does
not exist.

* feat(plugins): derive the workspace peer from git, configurably

The peer a workspace writes its memories under was the working directory with
every non-alphanumeric byte turned into a dash. That made the identity an
accident of where the repository happened to sit: a clone on another machine, a
rename, a worktree, or simply `cd examples/` each minted a separate, empty
namespace, and there is no server-side rename or merge to recover from it.

The default is now git's own idea of the repository. `peer.source` decides the
rule and reads from every layer — `OPENVIKING_PEER_SOURCE`, ovcli.conf's
`plugin.peerSource`, or `peer.source` in a workspace file:

- `git` (new default) ≡ `["{git_remote}", "{git_root}", "{cwd}"]` — the
  normalized origin, else the repository root, else the working directory. No
  preset adds a prefix; a path-derived id already starts with `-` on POSIX, so
  it cannot collide with a remote-derived one.
- `cwd` — the old rule, byte for byte.
- `none` — send no peer. `OPENVIKING_WORKSPACE_PEER=0` still means this.
- Any template, or a list tried in order, over `{git_remote}` `{git_root}`
  `{cwd}` `{dir}`. Substitution is all-or-nothing: an empty variable falls
  through to the next template rather than leaving a half-formed shared id.

So `/Users/x/Dev/OpenViking/examples/codex-memory-plugin` with origin
`git@github.com:volcengine/OpenViking.git` is `github.com-volcengine-openviking`
from any subdirectory, worktree, machine or clone. Every clone of one repository
shares one peer; a fork has a different origin and stays separate, and
`gh pr checkout` of someone else's PR does not change origin, so reviewing does
not move a session's memory.

Nobody has to migrate. The pre-git id is always recomputable locally, so
`resolveEffectivePeerId` returns it alongside the effective one and recall still
reaches it: under the default `peer_scope: "all"` the server's cross-peer sweep
already covers it for free, and under `"actor"` — where that sweep is off by
definition — the plugin asks that peer separately, as itself, which is cheaper
and reaches more than a bare cross-peer read would. There is no deadline on
this. Wired through all five recall paths; doctor names the previous peer and
says which of the two is carrying it.

`source` keeps its three values because five call sites compare it against the
literal `"workspace"` to decide whether a session pin may be reused; the new
`origin` field names the template that actually produced the id, and doctor
prints it. Both session pins bump their version, so a pin frozen under the old
rule cannot outlive it.

* docs: the workspace peer comes from git, and a workspace can carry config

Every page that described the peer as the working directory with its
non-alphanumerics dashed now describes `peer.source` and the git default, with
the presets, the template variables, the clone-vs-fork identity semantics, and
why no migration is required. The capability reference gains the three new
shared modules; the client configuration page gains a Workspace Configuration
section covering the two workspace files, the per-machine registry, the
precedence table, the v1 schema and what a workspace file may not set; the
Claude Code and Codex integration pages, which never mentioned peer derivation
at all, each gain a short section. All zh mirrors follow.

`examples/schemas/workspace-config-v1.json` is the schema the `$schema` key in
a workspace file points at, and `examples/workspace-config.example.json` is a
file to copy. `ovcli.conf.example` shows `plugin.peerSource`.

Two facts worth stating plainly, both verified against the loaders rather than
assumed: `OPENVIKING_PEER_SOURCE` and the workspace-file layer are read only by
the Claude Code and Codex plugins today, so the other harnesses run on the
default and their pages document the config key rather than an env var that
would be inert; and pi and dsh compute their legacy id from the process cwd,
so their pages promise dual-read only under the default `peer_scope: "all"`.

* fix(mcp): stop the proxies from guessing a peer out of their launch directory

Three MCP proxies keyed the actor peer off `process.cwd()`, which for a
long-lived server started from a static MCP config is the directory the harness
happened to launch from — often the plugin's own. The Codex proxy already
refused this and had a test forbidding it; the rule now holds for all of them
through the same `resolveMcpActorPeerId`.

The plan called for the parent process to inject `OPENVIKING_PEER_ID` at launch
instead, following dsh's `mcp.mjs:27`. That only works for dsh: Claude Code and
agent-plugins are launched from a static `.mcp.json`/`mcp.json` with no
environment block, and OpenCode's `createOpenVikingMcpConfig` builds a command
and args with nowhere to put one. So the fix is to stop guessing rather than to
guess better — a proxy sends no actor peer, which is broad recall, the default.

`resolveMcpActorPeerId` now warns and widens where it used to throw. Refusing to
start took away every memory tool because a scope preference could not be
honoured, which costs the user far more than the wider search does; the warning
says which two settings would scope it.

* test(pi): follow the git-derived peer default rather than pinning the cwd id

* fix(plugins): warn on the camelCase spelling of a connection key too

The projection into harness knobs is an allowlist, so `apiKey` in a workspace
file could never take effect — but it vanished without a word, which reads as
acceptance. It is refused by name now, like its snake_case twin.

* feat(cli): ov workspace show, and ov peer link|migrate|forget-previous

`ov workspace show` answers "which layer actually set this" the way
`git config --show-origin --show-scope` does: the workspace root and how it was
found, the git remote, every template variable, the effective peer and the
template that produced it, each config layer with whether it applied, and per
key the effective value plus everything it shadowed.

That question matters here because three languages read this configuration and
each could drift. So the Rust reader is not a paraphrase of the JS one — the two
were run side by side over the identity helpers, the merge with full provenance
trees, the file-read rules and the registry's raw bytes, and made byte-identical.
That comparison paid for itself: it caught `serde_json::Map::remove` being a
swap remove under `preserve_order`, which reshuffled a registry file the JS half
reads on every rewrite.

It also caught the divergence that would have made the command a liar. ovcli.conf's
`plugin` section speaks the flat knob names a harness loader reads (`peerSource`,
`recallLimit`); a workspace file spells the same settings nested (`peer.source`,
`recall.max_items`). Both are one chain in `loadPluginSettings`, and the port had
merged the flat file into the nested tree, so `plugin.peerSource: "cwd"` in
ovcli.conf left `ov workspace show` reporting the git-derived peer while every
plugin sent the cwd-derived one.

`ov peer link <id>` pins a peer for this workspace in the registry — the way out
of a fork that should share the upstream's memory, or a legacy id worth keeping.
`ov peer migrate` moves a peer's memories and resources with the fs mv API,
reporting the plan by default and requiring `--apply`; the server has no merge
semantics, so a collision is refused with the colliding path rather than
overwritten, and a listing that fills its limit aborts rather than planning from
a truncated view that could hide one. `ov peer forget-previous` clears the
recorded ids.

`workspace show`, `peer link` and `peer forget-previous` are local and do not
require ovcli.conf; `peer migrate` talks to the server and does. Both config
gates and the hand-rendered help are registered, with a test pinning the gates
against each other.

A test now reads FORBIDDEN_KEYS, REGISTRY_ONLY_KEYS and FREE_FORM_SECTIONS out
of the JS module and compares them to the Rust constants, because those lists
are what someone fixing a bug in one language edits — and they had already
drifted once while this was being written.

* build(cli): record the sha2 dependency edge in Cargo.lock

Already vendored for other workspace members; ov_cli now uses it for the
registry slot hash.

* test(plugins): the fixtures the plan named that were still missing

A moved or renamed repository keeping its identity is the change's whole point
and had no test; a shallow clone was worth pinning because it is exactly what
the rejected root-commit scheme could not answer; and the registry's
read-modify-write window between two hooks of one session is now written down as
a test rather than only as a comment.

* fix: the defects a plan review turned up

An independent review against the plan this branch was built from found ten
real defects. Each was reproduced before being fixed and is now covered by a
test.

The four that broke a promise the feature makes:

- The workspace config layer was resolved from the hook process's own working
  directory, not from the `cwd` on its stdin payload — which is the
  authoritative one. A hook started in one repository while the session sits in
  another applied the wrong `.openviking/config.json`: its peer, its bypass
  patterns, its `capture.enabled`. `loadConfig` now takes the directory, and
  every hook that receives one re-resolves with it. Late re-resolution is safe
  precisely because connection and credential keys are structurally forbidden
  in a workspace file, so `baseUrl` and `apiKey` cannot move under an already
  built client — the gates that a workspace can switch off moved below the
  parse so they are decided on the right config too.
- Codex threw away a workspace file's `peer.id`: it returned the credential
  chain's peer verbatim, so `{"peer": {"id": "team-a"}}` did nothing. Claude
  Code had always honoured it.
- Claude Code's session pin returned only the id and source, dropping
  `legacyPeerId` — so from the second hook of a session onward, dual-read
  stopped asking the peer that holds everything written before the derivation
  changed. Silently, and exactly where it mattered.
- `ov peer migrate` read the source peer with the actor-peer header set, which
  the server refuses for another peer's path, and treated every `stat` error as
  "does not exist". The common case — an `actor_peer_id` in ovcli.conf — got a
  cheerful "Nothing to migrate" instead of a 403. It now uses a client with no
  actor peer and tells a real error apart from an empty source.

The rest:

- A directory outside any repository is a workspace again. It had no root at
  all, so a `.openviking/config.json` there was ignored entirely. `$HOME` and
  `/` are still never roots, now judged on the starting directory rather than
  on where the upward walk stops, and `git_root` stays empty outside a
  repository so the `git` preset still falls through to `{cwd}`.
- The registry slot is keyed on identity, not path. Two linked worktrees of one
  repository are one workspace — one peer, so one set of settings and one
  `ov peer link` — and keying on the checkout path split them in two. This also
  makes crossing two repositories physically impossible rather than merely
  detected. (Their `config.json` files still follow each checkout; those are
  files on a branch.)
- git folds section and key names to lower case, so `[Remote "origin"]` with
  `URL = …` is a remote `git config` reads and we did not. A quoted subsection
  stays case-sensitive.
- `min_client_version` warns instead of being silently kept as data, and still
  never blocks.
- `cli_config_profile` was validated and then never used. It now selects
  `~/.openviking/ovcli.conf.<name>` before credentials resolve — registry-only,
  name-only, and a hard error when the profile is missing, because quietly
  authenticating somewhere the user did not choose is the failure the key
  exists to prevent.
- `ov workspace show` is exempt from the language gate. It is a diagnostic and
  has to work on a machine where no language was ever chosen; `peer link`,
  `migrate` and `forget-previous` mutate state and still gate.
- `ov peer link` records the peer it replaced, so a later `migrate` with no
  `--from` finds it instead of falling back to a recomputed cwd id.

Two more the plan asked for that were missing: doctor now checks the knobs
*inside* ovcli.conf's `plugin` section — it was on the allowlist, so until now
`peerSorce` sat there doing nothing with no complaint — and suggests the key
you probably meant. A test derives the known-knob set from what the two loaders
actually read, so the list cannot rot into one that rejects a real knob; it
caught a missing entry the moment it was written.

The RFC is archived at docs/design/, with the three claims implementation
disproved corrected in place: the knob is `commit_token_threshold`, `__self`
and `ext-` are not reserved server-side, and a worktree converges its identity
rather than its config file.

* revert(cli): withdraw ov workspace and ov peer from this branch

The command surface these two files added was larger than the feature they
served: 4792 lines of Rust for `ov workspace show` and
`ov peer link|migrate|forget-previous`, against a change whose whole point is
what the hooks send. None of it had reached a user-facing document — only the
RFC named it — so it goes back out whole and the branch becomes a plugin
change plus one CLI bug fix.

Restored from the branch's merge-base rather than from origin/main, since main
has moved on since the branch was cut and those commits are not this branch's
to carry. `sha2` was pulled in only by `workspace.rs`, so its dependency edge
leaves with it.

What stays is `config.rs` and `config_wizard/store.rs`: `ov config add|edit`
dropped the whole `plugin` section because the wizard round-tripped the file
through a typed struct, and that fix has nothing to do with the withdrawn
commands.

The registry under `~/.openviking/workspaces/` stays too, as a layer the
plugins read. Nothing writes it for now; `ov-memory-doctor` prints the path it
expects, and the file is small enough to create by hand. A writer can come back
on its own merits.

* fix(plugins): derive a peer only inside a git repository

Codex desktop opens a directory per task — `~/Documents/Codex/<date>/<slug>/` —
and none of them is a repository. The `git` preset ended its fallback chain at
`{cwd}`, so every one-off task minted its own empty peer, and each new one
started with no memory. Nine such directories here, nine peers.

There is nothing app-specific to read: the state file that lists those threads
is Electron-private, a megabyte wide, desktop-only, and would have to be parsed
inside SessionEnd's 3-second budget. The signal that generalizes is structural
— the directory is not a repository, and nothing in it says it is a project.

So the default chain is now `["{git_remote}", "{git_root}"]` and stops there. A
directory that is neither a repository nor marked gets no peer at all, and what
is remembered in it goes to the user-level space, which is where it went before
peers existed. Deriving an identity from a bare path is what `peer.source:
"cwd"` is for, and it is a word away.

Naming such a directory is the other half. `findWorkspaceRoot` now also stops
at a directory holding `.openviking/config.json` or `config.local.json`, so a
marker file works from any depth below it, the way a repository does — and when
that marker sits inside a repository the git variables still resolve to the
enclosing repository, so marking a subdirectory of a monorepo does not split
the default peer. `{git_root}` is the repository's root, `{dir}` the workspace
root's name whichever made it one.

Nothing moves. When no template resolves, the pre-git id is still computed and
returned as `legacyPeerId`, so `peer_scope: "actor"` keeps asking for it and
`"all"` keeps sweeping it.

Two doctor bugs fell out of the same walk: `checkWorkspace` read `git.kind`
unconditionally and threw wherever there was no repository, and the peer block
warned "set peer.source to git" at a directory where `git` is exactly what is
already set and correctly resolves to nothing. It now says why no peer is sent,
and prints the file to create.

* docs(plugins): say how to give a directory its own peer, to users and to agents

The behavior change is only useful if the reader can act on it, and two kinds
of reader have to: the person whose scratch folder stopped having a memory, and
the coding agent they ask about it.

`docs/{en,zh}/configuration/02-client.md` is the one place that spells the rule
out, and everything else links to it. It gains "Give a Directory Its Own Peer",
which opens with the file to create and then the ladder above and below it;
"By Situation", eight rows from fork to throwaway folder; and "Recall
Isolation", which separates where memories are written from what is read back,
names the server's per-category penalties, and states the cost of sending no
peer outside a repository — a user-level memory is read at full weight in every
project afterwards.

The eight integration pages, both capability references, six plugin READMEs,
the changelog and the schema stop promising a fallback to the working
directory. Checking those claims against the loaders turned up one that was
never true: opencode, dsh and pi do not read workspace files at all, so a
`peer.id` written for them does nothing. Said plainly rather than left to be
discovered.

For agents, `openviking-memory/SKILL.md` gains ten lines on where memories are
filed — it is the skill that fires when someone asks why a folder has no
project memory, and it had nothing to say — and both `ov-memory-doctor`
references gain a table from what the user says to the exact key to write.
`ov-memory-doctor` prints the same snippet, so an agent that runs it needs no
further reading.

One snippet has to be identical in twenty places for any of this to hold, so
`WORKSPACE_PEER_HINT` is a constant the report builds its line from, and
`peer-guidance.test.mjs` asserts it appears verbatim wherever it is promised,
that no page still spells the retired chain or names a command this branch
withdrew, and that every variable the canonical page documents is one the code
substitutes. It asserts no prose: rewording a page must not turn it red.

* fix(plugins): reject an unrecognized peer.source instead of using it as an id

`peer.source` accepts a preset name, a template, or a list of templates, and
anything that is not a preset was treated as a template. A template with no
`{...}` in it renders to itself, so a typo became the peer: `"Git"` wrote every
memory under a peer literally named `Git`, and `"gti"` under `gti`. Silently —
the wrong namespace is indistinguishable from an empty one until someone
notices their project has no memory.

A bare string that is neither a preset nor contains `{` now warns and falls
back to the `git` default. A list is still taken at face value: writing one is
explicit enough that a typo inside it is a different kind of mistake.

The warning travels through an optional `onWarn` callback falling back to
stderr, matching `resolveMcpActorPeerId` in `mcp-proxy-config.mjs` — there is no
warnings array in reach, because `resolveEffectivePeerId` is called from the
hook runtime and from four harness config loaders, none of which thread one.

* docs(plugins): say what each harness can actually do with a peer

The peer documentation promised the same thing everywhere, but only the Claude
Code and Codex plugins read workspace configuration files. `loadPluginSettings`
is called from exactly two loaders; the other harnesses build their config from
their own file plus the environment. So a reader following the docs under pi,
dsh, opencode or cursor would create `.openviking/config.json` and watch it do
nothing.

The skill is the sharpest case: `openviking-memory/SKILL.md` is synced to
cursor and dsh as well, and it told an agent to write that file. An agent would
have done it, reported success, and changed nothing. It now names the two
harnesses that read it and points everyone else at `OPENVIKING_PEER_ID`.

The integration pages had started teaching the recipe and then retracting it in
the same sentence, which is worse than not mentioning it; they now carry the
one instruction that works there, and link to the canonical section for the
rest. The capability reference gains the same qualification, next to the
paragraph that already says only two harnesses read those layers.

Two smaller corrections. The doctor references had the same question answered
twice, once in the peer table and once in the troubleshooting table 140 lines
below; the troubleshooting row survives, since it carries a diagnostic column.
The RFC still archived implementation notes for the CLI this branch withdrew,
which would read as a description of commands that exist.

`peer-guidance.test.mjs` guards this alignment, and had two flaws of its own: it
swept the changelogs, which are generated from GitHub Releases and would go red
on a release note nobody wrote by hand, and it sliced a page between two
headings with `indexOf` without checking either was found — renaming the closing
heading would have silently scanned to end of file.

Also here, because it is the same kind of mismatch: the zcode MCP proxy sent an
actor peer under broad recall, where the other proxies leave the header unset.
It now routes through `resolveMcpActorPeerId` like they do. The dsh proxy
deliberately does not — its parent process resolves the peer per session and
injects it into the child environment, so it is not guessing at a launch
directory, and that reason is now recorded next to the line.

* fix(plugins): ship and install only the shared modules a plugin imports

Three new modules were fanned out to all seven plugin directories in one hunk,
and only two plugins call them. That left dead weight in five directories, and
it broke three installs.

The install is the part that mattered. `install.sh` copies a hand-written list
of shared files into `~/.openviking/agent-integrations/memory-plugin-shared/lib`,
where cursor, TRAE and TRAE CLI import from. The list names `workspace-peer.mjs`
but not `workspace-identity.mjs`, which `workspace-peer.mjs` imports — nor
`workspace-config.mjs`, which identity had come to import for three filename
constants. Copying exactly that list and importing the hook runtime fails with
ERR_MODULE_NOT_FOUND, so every hook of those three harnesses would have died on
startup. Nothing tested the list.

`CONFIG_DIR_NAME`, `TEAM_FILE` and `LOCAL_FILE` now live in
`workspace-identity.mjs`, which is where the walk that recognises a marked
directory needs them; `workspace-config.mjs` imports and re-exports them, so no
call site moves. Identity has no library-internal dependency left, which is what
makes the installed set closed at fifteen files instead of pulling the whole
configuration layer along behind it. `install-lib-closure.test.mjs` derives both
sides — the list parsed out of the shell script, and the transitive imports of
the three entrypoints — and fails in either direction, so neither a new
dependency nor a stale entry can go unnoticed again.

With identity standing alone, the fan-out can follow what is actually imported.
`sync.mjs` moves from arrays chained by spread — where the harness that does not
need a file is often the one the array is named after — to explicit per-target
lists. `plugin-config.mjs`, `workspace-config.mjs` and `workspace-registry.mjs`
leave dsh, pi, opencode and zcode, none of which import them; all four workspace
modules leave agent-plugins, whose only importer was deleted a half hour after
they arrived and whose peer is environment-only by design. That is about 4500
lines of vendored code that said something the code did not do.

The registry loses its write path in the same spirit. `writeEntry`,
`rememberPreviousPeer` and `listEntries` had no caller outside their own tests:
the CLI that would have written them was withdrawn from this branch. `readEntry`
and `entryPath` stay, because a hand-created entry is still read and the doctor
still prints where to put one. `cli_config_profile` goes with them — the whole
mechanism, down to the documentation that described it, since nothing ever
resolved a profile through it.

This is not a new policy. `HARNESS_KEYS` already carried the rule in a comment,
added the day after the same speculative fan-out happened in August: add a key
as its loader starts calling `loadPluginSettings`, not before, so the section
never promises a knob that silently does nothing.

* fix(dsh): thread peerSource into the per-session peer

The integration page documents `peerSource` in dsh's Cordis patch, but `stateFor` never passed it to `resolveEffectivePeerId`, so the key resolved to nothing and every dsh session ran on the default derivation. Pass it, and keep the pre-git id alongside so dual-read reaches memories written before the default changed.

* docs(plugins): correct the shared-layer counts after the distribution changed

The capability reference still described the pre-branch distribution: 18 library modules against 23, per-target counts from before each target stopped receiving the workspace configuration layer, and `workspace-config` / `workspace-registry` listed as reaching every JS harness when only claude-code and codex load them. It also still said `cli_config_profile` was registry-only, and that the registry is written for you.

* docs(rfc): lead with a TL;DR of the workspace config and peer source proposal

* feat(plugins): let peer.source name the harness with {harness}

The peer templates could describe where a checkout sits but never which
agent was running in it, so one repository could not keep a separate
memory per agent even when its user wanted that. The harness name was
already in every config, only baked into the User-Agent string.

No preset uses the new variable: sharing one project memory across
agents is the more useful default, so splitting stays opt-in via a
template such as "{git_remote}-{harness}". It is composed at render
time rather than in the workspace identity, whose result is cached
under a cwd-only key that two harnesses in one directory would share.

* docs(rfc): record git_branch and peer.command as directions, not deliverables

* chore(plugins): resync the openclaw vendored recall-core after the rebase

* test(opencode): follow resolveEffectivePeerId's widened return shape

Also mark the openclaw shared copies generated, the way every other sync
target already is.
2026-09-04 12:50:43 +08:00
now-ingandmac da94ac1afd fix(opencode-plugin): fall back to config peerId when shared credentials define none (#4632)
loadConfig() unconditionally assigned config.peerId from resolved shared
credentials, and applyLegacyConnection() (the only reader of the config
file's peerId) is skipped whenever those credentials exist. When ovcli.conf
or environment variables authenticate without actor_peer_id, the project
config's peerId was silently dropped, so sessions and memories landed in
the shared user tree instead of the peer-scoped tree.

Apply the file config's peerId as a fallback when shared credentials carry
no peer, keeping the documented precedence: shared credentials >
extension config peerId > workspace-derived peer. Same defect class as
#3649 (Pi extension, addressed by #3653 for Pi only).

Fixes #4487

Co-authored-by: mac <bishopapril850965@yahoo.com>
2026-09-04 12:05:24 +08:00
ktz03 a7cbf93214 fix(opencode-plugin): isolate flushAll session failures (#4608)
Fixes #4490

Wrap each flushSession in try/catch so one failed session during dispose does not abort later sessions.
2026-09-03 11:25:29 +08:00
Pedro Perez 9c9ad8c694 fix(opencode-plugin): make messageID optional in prependSyntheticRecallPart (#4601)
The chat.message hook in opencode's plugin API declares messageID as
optional (messageID?: string in @opencode-ai/plugin/dist/index.d.ts).
When opencode invokes the hook without a messageID, the null check in
prependSyntheticRecallPart causes it to return false silently — the
recall block is discarded, no context is injected, and no error is
logged. This means the autoRecall feature silently fails for all
opencode versions that don't pass messageID.

Fix: generate a timestamp-based fallback for messageID when absent.
sessionID remains the only hard requirement (it is always provided
by the opencode API).

Verified against opencode 1.18.26: synthetic recall parts are now
injected on every user message (confirmed via opencode session DB).
2026-09-03 11:25:25 +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 3b1db2082f fix(memory-plugin): setup wizard first-run path, proxy hint, config source reporting (#4387) 2026-08-27 16:31:20 +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 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
t0saki a83b81715b feat(uri)!: remove uid-less current-user shorthand in favor of viking://~ (#4196)
* feat(uri)!: reject uid-less current-user shorthand in favor of viking://~

viking://user/<segment> (memories/resources/skills/peers/privacy/sessions
without a user id) was ambiguous with a user literally named after the
segment, and a user actually named e.g. "memories" was unreachable for
USER/ADMIN callers. Now that the viking://~ home alias (#4167) covers the
same need unambiguously, the shorthand fails closed at the request
boundary instead of expanding:

- resolve_current_user_uri raises NamespaceShapeError with a corrective
  hint naming both viking://~/<rest> and the explicit-uid form. Silently
  parsing the reserved segment as a peer user id would misdirect reads
  and writes, so rejection is the only safe removal.
- Bare viking://user falls through to the canonical parser and keeps
  container semantics (a user key listing it sees only its own space).
- The self-id escape stays: a caller whose user_id equals a reserved
  name keeps viking://user/<own-id> as their canonical root. ROOT-role
  literal parsing and the legacy viking://session alias are unchanged.
- AddTargetsConfig normalizes stored legacy config spellings
  (viking://user/resources|skills) to the viking://~ form at validation
  so existing ov.conf/user_config deployments keep working; the accepted
  per-user spelling is now viking://~/resources and viking://~/skills.
- usage_reporter keeps canonicalizing the historical shorthand found in
  old transcripts and additionally recognizes viking://~/memories/.

BREAKING CHANGE: requests using the uid-less viking://user/<segment>
spelling now fail with 400; use viking://~/<segment> or an explicit
viking://user/{user_id}/<segment> URI.

* refactor(clients): migrate first-party emitters to the viking://~ home alias

Every in-repo client that emitted the removed uid-less current-user
shorthand now sends viking://~/... instead: vikingbot fallbacks and
default sentinels, the LangChain store/tools defaults, the shared
recall-core.mjs (all synced plugin copies), the codex/claude-code/
openclaw/openwebui/dsh/zcode/pi plugin emitters, quick-app examples,
Go SDK example, tau2 benchmark targets, and the eval golden dataset.

Compat kept where legacy strings live in stored user configs: bot and
ov_dream sentinels accept both spellings while emitting only ~, and
recall-core still rewrites legacy viking://user/<reserved> config values
client-side. langchain_openviking._uri now classifies viking://~ with
the explicit-user shape so canonicalized server responses keep matching
a ~ root. Plugin READMEs note the server requirement for the alias.

* docs: replace current-user shorthand guidance with the viking://~ home alias

Rewrite every EN/ZH doc and model-facing prompt that advertised the
uid-less viking://user/<segment> spelling: URI concept catalogue,
context-types/storage/extraction/retrieval/session/privacy concepts,
configuration guide (with the legacy add_targets auto-normalization
note), resources/skills/sessions/retrieval/admin API references, FAQ,
capability reference, and the openviking-memory / ov-experience-memory /
openclaw / ov-resources skills. The stale MCP viking://user/<path>
dialect passage in the MCP guide is replaced by ~ guidance, and bare
viking://user is documented as the container of user spaces.

* test(api): migrate live API session-used tests off the removed shorthand

tests/api_test/sessions sent uid-less viking://user/skills/... URIs to
record_used, which the request boundary now rejects with 400 (caught by
the API & CLI Integration Tests CI job; these tests need a live server
and are not part of the local suites). The api_test client authenticates
as an admin-role user key, so the viking://~ home alias expands for it.
tests/api_test/common/test_edge_cases.py is left as is: it asserts a 400
for a non-resource add target, which still holds.
2026-08-21 19:00:19 +08:00
t0saki c7044075ef feat(dsh): serve tools over the shared stdio MCP proxy (#4157)
* feat(dsh): serve tools over the shared stdio MCP proxy

Replace the dsh bundle's seven hand-registered `viking_*` tools with the
OpenViking MCP surface, reached through the same stdio proxy every other
memory integration starts, and collapse the four duplicated proxy
entrypoints onto a shared config builder.

The bundle now mounts `@deepseek-ai/dsh-mcp-client` (which ships with dsh
itself) on `servers/mcp-proxy.mjs`. Pointing an MCP SDK client straight at
the server's `/mcp` endpoint does not work: with `stateless_http=True` the
server still answers `GET /mcp` with an idle 200 SSE stream, and once the
SDK client opens that standalone stream it stops resolving POST responses,
so `tools/list` never returns. The stdio proxy owns the transport itself
and is unaffected.

`trimSlash`, `normalizePath`, `uniq`, the watched-credential-path list and
the cfg -> proxyConfig mapping existed in four near-identical copies
(claude-code, codex, opencode, agent-plugins; the last one carried a
"keep in sync with claude-code" comment). They move to
`memory-plugin-shared/lib/mcp-proxy-config.mjs` and all five entrypoints —
including the new dsh one — now shape their config through
`buildMcpProxyConfig`. Behavior is preserved per field, including codex's
explicit `mcpUrl` override, claude-code's `ovcli.conf` credential-source
probe, and opencode's extra watched config file.

The bridge is mounted last in `apply()` so a proxy that fails to start
cannot hold up profile injection, recall, capture, commit, or the URI
guard registrations above it.

* feat(dsh): add to the unified installer and ship the shared skill

The bundle now registers its own isolated `ctx.skills` provider serving the
shared `openviking-memory` skill, so DSH gets the same guidance the Claude
Code, Codex, and Cursor integrations ship. `sync.mjs` distributes the skill
to the bundle, and the provider uses `includeDefaultRoots: false` so it
never shadows DSH's own project/user skill catalog.

`install.sh` grows a `dsh` harness id, auto-detected like the others, plus a
profile prompt that defaults to `web` (`--dsh-profile` / `OPENVIKING_DSH_PROFILE`
answer it up front). The installer always installs the published package:
`dsh plugin` forwards to pnpm, and a linked source tree cannot resolve the
dsh peers the bundle imports because Node resolves them from the checkout's
realpath rather than from the profile.

Documentation is restructured around installing rather than internals. The
integration page now leads with the one-line installer and keeps behavior at
the level the other harness pages use, with configuration in a details block;
design rationale moves to the bundle README, which itself leads with Install
and groups the rationale under "Design notes". Capability-reference claims
that dsh is outside the unified installer are corrected.

* chore(dsh): release 0.2.0

The MCP tool surface, the stdio proxy transport, and the bundled skill all
change what the bundle does for an existing user, so this is a minor bump
rather than a patch. 0.1.0 remains the native-`viking_*` tool surface.

* docs(dsh): note pnpm's 24h minimum release age

pnpm 11 refuses releases younger than minimumReleaseAge (24 hours by
default), and surfaces it as a registry 404, so installing a freshly
published version reads as "the package does not exist".

* fix(dsh): honour dev source mode in the installer

install_dsh ignored SOURCE_MODE and always fetched the published package,
so selecting "current checkout" installed npm's build instead of the
working tree and validation still reported success.

npm is the bundle's only distribution channel, so the github/tos choice
does not apply to it: every mode except dev now installs the published
package, and dev packs the checkout with npm pack first. It has to arrive
as a real package rather than a link, because a linked source tree
resolves its dsh peers from its own realpath and misses the profile's
hoisted node_modules. The install line reports which source was used.

* fix(dsh): make repeated installs actually overwrite

Two ways a re-run silently kept stale code:

pnpm treats an already-satisfied version as a no-op regardless of which
tarball the file: dependency points at, so a dev re-install after editing
the checkout left the previous build in place. Local installs now drop the
package before adding it back; that is confined to local sources, since
doing it for the registry path would leave nothing installed when add
fails.

A bare package name has the same effect in reverse: a profile holding a
dev build satisfies it, so switching back to the published package was a
no-op. The registry path now asks for @latest.

The packed tarball is named after a fingerprint of the checkout's shipped
files, so an unchanged checkout skips the pack and keeps a stable path in
the profile lockfile.
2026-08-20 19:05:18 +08:00
t0saki 458e7fae37 docs: add a cross-integration capability reference page (#4076)
* docs: fix integration docs and comments that contradict the code

- codex: credential resolution in the default `auto` mode is env-first — `credentials.mjs`
  only falls back to `ovcli.conf` when no credential env var is set, while the docs and the
  `config.mjs` header comment claimed `ovcli.conf` wins by default. Also document
  `OPENVIKING_CREDENTIAL_SOURCE=cli`, which was undocumented.
- codex: the four hook scripts send the key as `X-API-Key` in addition to
  `Authorization: Bearer`; the README documented Bearer only.
- claude-code: the OV session id is `cc-<cc_session_id>` verbatim (`deriveHarnessSessionId`
  does no hashing), not `cc-<sha256(cc_session_id)>`.
- claude-code: `hooks.json` registers 9 hooks, not 7 — the responsibilities table was
  missing the `PreToolUse` `viking://` guard and the `PostToolUse` skill-experience hook.
- claude-code: archival is triggered client-side (the `Stop` hook commits once
  server-reported pending tokens cross `commitTokenThreshold`, default 20000, plus
  unconditional commits from `PreCompact` / `SessionEnd` / `SubagentStop`). The README
  attributed it to a server-side `auto_commit_threshold`, but
  `memory.session_auto_commit.default_enabled` is false and no plugin sends a policy.
- trae / opencode: the MCP proxy transparently exposes the full server tool set (16 tools);
  the docs listed a 4-item sample or 11-13 tools and omitted `tree` / `write` / `edit`.
- trae-cli: the installer registers the MCP server as `openviking-memory`, but the verify
  step told users to look for `openviking`.
- pi: the manual install block omitted the `pi install <dest>` registration step that the
  one-click installer runs, so a hand-copied extension is never registered.
- install.sh: `--uninstall` handles cursor, trae, trae-cn, trae-cli and zcode; the `--help`
  text still said Cursor/TRAE only.
- mcp_endpoint.py: the module docstring enumerated 13 tools and omitted `recall`,
  `list_watches` and `cancel_watch`; replaced the stale enumeration with a pointer to the
  `@mcp.tool` registrations.

* docs: add a cross-integration capability reference page

The agent-integrations section had per-integration install guides but no place
to compare integrations against each other. This adds one bilingual page that
does that, and wires it into the existing pages in both directions.

- New page `docs/{en,zh}/agent-integrations/16-capability-reference.md`: a
  dimension-first comparison of every OpenViking integration — active tool
  surface, automatic hook surface, install/credential/config layering, recall
  and injection, session and commit lifecycle (including a shutdown-path x
  harness end-state matrix), compaction takeover, write/delete boundaries,
  degradation, and a per-harness profile card for each integration.
- Sidebar: `StructuredSidebarCopy` gains an optional `topItems` field so a
  section can list flat entries next to its overview; agent-integrations uses
  it to place the new page beside the overview. Other sections are unaffected.
- Links both ways: the overview and all 14 per-integration pages link to the
  reference, and the reference links back to each integration page from its
  profile card, from the non-coding integration table, and from the custom
  agent integration paths. Section cross-references (§x.x) are real in-page
  anchor links, generated from the built heading ids.
- trae-cli is documented as TraeCode CLI 2.0 only, installed through a codex
  plugin alias; 1.0 and its standalone plugin are called out as unsupported.
- The MCP tool surface is described as 15 tools throughout, matching the
  removal of the `recall` tool in favour of `search` with `mode="context"`.
  Pages outside this change that still mention an MCP `recall` tool
  (04-codex, 12-cursor, 15-agent-plugins, guides/06-mcp-integration) need a
  follow-up sweep once that removal lands.

* docs: 更新服务端 MCP 工具面描述,简化信息并明确更新方式
2026-08-18 20:38:06 +08:00
t0saki eb5aaf78e9 feat(mcp): consolidate recall into context search (#4075) 2026-08-18 12:45:45 +08:00
t0saki 33043cb1b8 feat(plugins): expose session commit trace IDs (#3977)
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.
2026-08-13 23:29:41 +08:00
agent 00f3738edb feat(usage): emit resource-scoped experience usage records (#3921)
* feat(usage): expand experience tracking and log schema

* fix(usage): preserve experience count event names

* refactor(agent-evolution): use generic OpenViking tools

* fix(usage): capture generic OpenViking tool events

* feat(skills): guide cross-agent experience retrieval

* fix(usage): address generic tool migration review
2026-08-11 22:20:18 +08:00
t0saki 7e26fab61c fix(memory-plugins): report tool output verbatim, let the server externalize (#3933)
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.
2026-08-11 19:17:54 +08:00
Kuang Mi c25a7219db fix(opencode-plugin): serialize saveState to prevent ENOENT rename race (#3885) 2026-08-10 11:19:40 +08:00
ValorandValor fdb4b571ca feat(opencode): add hook-only installation mode (#3734)
* feat(opencode): add hook-only installation mode

* test(opencode): cover hook-only plugin configuration

---------

Co-authored-by: Valor <210239105+zrh805@users.noreply.github.com>
2026-08-07 17:11:45 +08:00
t0sakiandTRAE CLI 0ab48f96fc fix(session): recover partial capture sessions (#3820)
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>
2026-08-06 14:48:42 +08:00
t0saki 674f5e6039 fix(retrieval): honor context tier ceilings and stop cooling unserved recalls (#3746)
* 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.
2026-08-05 23:52:15 +08:00
2cc96e393e feat(retrieval): assemble auto-recall context server-side via /search mode="context" (#3534)
* 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>
2026-08-05 12:31:21 +08:00
zgy 49b182045b refactor(parser): Refactor code summaries to fixed skeleton-first routing (#3568)
* Refactor code summary skeleton routing

* Simplify code skeleton routing configuration

* Render C tag skeletons as signatures

* Revert "Render C tag skeletons as signatures"

This reverts commit 8e342055f8.

* Simplify fixed code skeleton summary route

* Inline process skeleton rendering

* Simplify code skeleton routing entrypoints

* Fix code summary review issues

* Address final code summary review feedback

* Route failed tags skeletons to LLM fallback

* Restore CUDA and TS extension routing

* Improve code skeleton query coverage

* Route semantic code detection through skeleton support

* Move process skeleton engine into ast package

* Admit skeleton-supported files during directory scan

* Align code summary docs after main merge

* Reduce code skeleton fallback log verbosity

* chore: require grep-ast 0.9.0
2026-07-31 11:38:57 +08:00
yufeng 229300e886 fix(codex): preserve session tool activity (#3579) 2026-07-29 12:05:48 +08:00
t0saki 29ba81b53e feat(plugins): identify memory plugin traffic with a User-Agent (#3532)
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.
2026-07-27 15:31:21 +08:00
yijie zhao 09cd4782df fix: allow opencode recall after session context (#3464)
* fix: allow opencode recall after session context

* test: cover opencode recall with session context
2026-07-22 19:32:13 +08:00
Hao Zhe ebe7fa2a7d fix(plugins): align OpenCode and OpenClaw contracts (#3232) 2026-07-17 17:06:27 +08:00
agent ccc271ff27 feat: add extensible usage reporting (#3222)
* feat: add extensible usage reporting

* fix: harden usage reporter lifecycle

* fix: scope experience usage events correctly

* refactor: generalize usage event schema

* docs: design Codex experience memory tools

* docs: plan Codex experience memory tools

* feat: add Codex experience memory tools

* docs: remove temporary Codex implementation plans

* fix: capture Codex MCP tool parts

* fix: harden experience usage reporting

* fix: reload credentials for local MCP tools

* fix: preserve MCP tool-level errors

* fix: bound synchronous sink shutdown

* fix: enforce sink shutdown timeout

* fix: enforce experience tool contracts

* fix(usage-reporter): keep tool schemas and replay ids stable

* fix(usage-reporter): reject unidentifiable tool events
2026-07-16 15:33:27 +08:00
t0saki 6f10e26361 perf(plugins): batch add-message writes and shared async detach across memory plugins (#3261)
* Batch add-message writes across memory plugins

* fix(plugins): stop batch enqueue at first failure and bump plugin versions

Keep pending-queue entries a contiguous prefix when a mid-stream enqueue
fails: consumers mark the first sent+queued payloads as captured, so a
queued entry after a gap could silently drop the gapped message.

Bump claude-code 0.4.3, codex 0.7.3, opencode 0.2.3, cursor/trae 0.1.2 so
existing installs pick up the batch write path.
2026-07-15 18:20:10 +08:00
t0saki 82999c8b4b fix(plugins): pin MCP-Protocol-Version header to the negotiated version (#3214)
* fix(plugins): pin MCP-Protocol-Version header to the negotiated version

The shared stdio proxy forwarded the client's un-negotiated
protocolVersion ask as the MCP-Protocol-Version header on initialize and
cached it for follow-up requests, never reading the version the server
actually negotiated in the initialize response. Strict streamable-HTTP
upstreams (e.g. modelcontextprotocol/go-sdk servers) validate that header
on every request and answer HTTP 400 'Unsupported protocol version'
before negotiation can run, so clients on a newer spec than the upstream
(Trae sends 2025-11-25) failed MCP registration outright.

Send the proxy default (2025-06-18) while initializing, adopt
result.protocolVersion from the initialize response for all subsequent
requests, and keep the client's ask intact in the forwarded initialize
body so end-to-end version negotiation still happens.

* chore(plugins): bump patch versions for the MCP proxy protocol fix

claude-code 0.4.2, codex 0.7.2, cursor 0.1.1, trae 0.1.1, opencode 0.2.2.
2026-07-13 15:12:43 +08:00
t0saki a878fc1a96 chore(plugins): bump memory plugin versions (#3126) 2026-07-10 17:38:12 +08:00
t0saki 2a81edc707 Add workspace peer mode for memory plugins (#3099) 2026-07-09 17:53:36 +08:00
t0saki b511d91ee5 feat(plugins): retrofit OpenCode and pi memory integrations (hybrid MCP, shared lib, 4-harness installer) (#3079)
* feat(plugins): align opencode and pi memory integrations

* fix(installer): tolerate missing optional harness CLIs

* fix(installer): install opencode file wrapper

* fix(opencode): import path for logger initialization

* fix(installer): register pi extension after copy

* feat(plugins): use MCP for opencode integration

* docs: move OpenCode and pi integrations to dedicated pages

Promote the OpenCode plugin and pi extension out of the community-plugins
page into their own numbered agent-integrations pages (10-opencode, 11-pi,
en + zh), update the overview routing table, and refresh the OpenCode image
cards to the hybrid MCP architecture (unified installer, openviking_* MCP
tools, ovcli.conf credentials).

* docs: bare TOS installer commands and reference more examples

Drop --harness from TOS-mirror install commands (image cards use the bare
installer URL, matching the claude-code/codex cards); add Open WebUI tool
server and an examples/ pointer to the community-plugins page (en + zh).

* docs: bare TOS installer commands across agent-integration pages

TOS-mirror install commands carry no flags anywhere; the installer wizard
asks for source, harnesses, language, and credentials.
2026-07-08 15:54:37 +08:00
Zayn JarvisandQin Haojie 41144890fe fix(opencode-plugin): declare OpenCode plugin targets (#2851)
Co-authored-by: Qin Haojie <qinhaojie.exe@bytedance.com>
2026-06-30 19:38:04 +08:00
Zayn Jarvis c8fc0a658c chore(opencode-plugin): scope package name and bump to 0.1.6 (#2895) 2026-06-30 19:35:33 +08:00
Zayn Jarvis e43a4c5e6e ci(opencode-plugin): publish scoped npm package (#2807)
* fix(opencode-plugin): use js source install wrapper

* ci(opencode-plugin): publish scoped npm package
2026-06-24 21:21:57 +08:00
Zayn Jarvis 0829477d5d feat(opencode-plugin): memwrite & guard grep/glob on viking:// path (#2801)
* feat(opencode-plugin): improve project memory UX

* chore(opencode-plugin): defer project isolation
2026-06-24 13:43:48 +08:00
Zayn Jarvis 9beb18ff9b fix(opencode): drop invalid recall mode (#2793) 2026-06-24 12:04:53 +08:00
Zayn Jarvis 5e622ba6ea docs(plugin): keep one OpenCode plugin (#2540)
* docs(plugin): keep one OpenCode plugin

* docs(opencode): expand plugin install guide
2026-06-22 10:26:46 +08:00
AutoCoder ecced9930a feat(code-tools): add code navigation endpoints (#2671)
Add HTTP APIs for code outline, search, and expansion backed by the existing AST tooling.

Expose the same capabilities through the opencode plugin and cover the new routes, parser behavior, and plugin wiring with tests.
2026-06-17 16:12:19 +08:00
Qin HaojieandMijamind719 8353976bc8 feat(plugins): use actor peer scope (#2595)
* feat(plugins): use actor peer scope

* docs(openclaw): clarify actor peer recall scope

---------

Co-authored-by: Mijamind719 <mijamind@163.com>
2026-06-14 10:15:27 +08:00
Evo 58ff0290b2 fix(opencode-plugin): fall back to session mapping when memcommit gets empty session_id (#2412) (#2488)
memcommit used `??` so an empty-string session_id bypassed the
current-session fallback and hit the 'No OpenViking session' error.
Match sibling memsearch's truthiness fallback so '' falls back like a
missing arg (session_id is always a non-empty UUID when set).
2026-06-08 20:47:50 +08:00
Qin Haojie ff258768c2 feat(memory): 引入 User/Peer 记忆隔离模型 (#2236)
* feat(memory): introduce user and peer memory isolation

Unify agent-scoped memory behavior into user-owned memory spaces, add peer_id compatibility for session and retrieval paths, and wire memory_policy through session commit flows.

* feat(memory): align session identity around peer IDs

* feat(search): pass peer id through retrieval

* refactor(memory): remove agent identity from integrations

* fix(memory): isolate peer identity from self extraction

* fix(tau2): provision benchmark user configs

* fix(auth): allow admin keys to access data APIs

* fix(openclaw): enable peer memory policy for peer roles

* fix(openclaw): resolve sender for peer recall

* refactor(session): simplify memory extraction routing

* refactor(ov-cli): reduce formatting-only diff

* refactor(message): remove unused message helpers

* refactor(retrieval): simplify peer target resolution

* refactor(namespace): remove deprecated agent namespace policy

* fix(agent): propagate peer id through integrations

* fix(auth): align integration clients with api-key mode
2026-06-05 10:55:48 +08:00
Qin Haojie 77b604a641 fix(storage): 优化路径锁与语义刷新并发 (#2029)
* fix(storage): refine path lock semantic refresh concurrency

Use exact path locks for source commits, tree locks only for lifecycle and schema scopes, and coalesce derived semantic writes to avoid stale summary overwrites under concurrent resource and memory updates.

* chore: split benchmark changes into separate PR

* chore: keep semantic refresh design notes out of docs

* style: format lock changes

* fix: preserve resource lifecycle locks

* Revert "fix: preserve resource lifecycle locks"

This reverts commit d2fb274f85.

* fix(resource): simplify lifecycle locking

* fix(queuefs): consolidate semantic sidecar writes
2026-05-14 20:44:28 +08:00
Jiahao Cao 31a31ff4d1 refactor(plugin):Merge OpenCode plugins (#1859)
* Merge opencode plugins

* update:readme&install docs

* Fix Opencode plugin packaging issues

Rename wrapper to .mjs, clean install docs, align README/INSTALL wording with the current directory layout, remove invalid README_ZH.md from package files, and pin @opencode-ai/plugin version.

* Address OpenCode plugin review feedback - Remove OpenViking server auto-start logic from the plugin runtime - Replace fixed-interval auto commits with lifecycle-boundary commits - Commit sessions on compaction, deletion, error, and plugin shutdown - Exclude source-install wrapper files from the npm package - Update docs to clarify wrapper usage and remove deprecated config fields

* fix(opencode-plugin): persist memory recall via chat.message synthetic parts

Replace experimental.chat.messages.transform recall injection with chat.message.

Inject recalled memories as hidden synthetic text parts persisted in session history.

Preserve idempotency with relevant-memories marker detection and update README wording.
2026-05-12 15:41:43 +08:00