Skip to content

feat: rule-based tool input compaction for auto-capture (~10x storage reduction) - #2874

Open
lg320531124 wants to merge 305 commits into
volcengine:mainfrom
lg320531124:feat/tool-input-compaction
Open

feat: rule-based tool input compaction for auto-capture (~10x storage reduction)#2874
lg320531124 wants to merge 305 commits into
volcengine:mainfrom
lg320531124:feat/tool-input-compaction

Conversation

@lg320531124

@lg320531124 lg320531124 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Add rule-based tool input compaction to the Claude Code auto-capture pipeline. compactToolInput() reduces Write/Edit/Bash/TaskCreate/TaskUpdate tool inputs to structural summaries instead of full file contents.

Problem

Sampling 16 CC sessions shows 79% of session storage volume is full code content from Write (avg 11,575 chars) and Edit (avg 2,525 chars) tool inputs. This causes:

  1. Storage bloat — sessions are ~100KB when ~20KB would suffice
  2. Vector signal dilutionbge-large-zh-v1.5 embeddings are dominated by code tokens instead of semantic content
  3. VLM extraction failure — 1081 sessions with 0 memories extracted, likely because content is too large for the VLM ReAct loop to process effectively

Solution

No LLM involved — purely rule-based compaction:

Tool Before After Ratio
Write Full file content (~7KB) file_path + "100 lines, 7200 chars" + 200-char preview (~280 chars) ~25x
Edit old_string + new_string (~5KB) file_path + length summary + 150-char previews (~400 chars) ~12x
Bash command + description command only ~1.5x
TaskCreate/Update All fields subject + status + taskId ~3x
Read/Glob/Grep/LSP Full preservation Unchanged 1x

Measured: 10.6x compression on realistic mixed session data.

Changes

  • auto-capture.mjs: Add compactToolInput() with per-tool-type policies (TOOL_INPUT_POLICIES); modify harvestContent() to use compaction when enabled
  • config.mjs: Add toolInputCompaction (default: true) and toolInputMaxChars (default: 2000) config options with env var overrides
  • __tests__/auto-capture-compaction.test.mjs: 21 regression tests covering all tool types, truncation, edge cases, and compression ratio

Configuration

Env Var Default Description
OPENVIKING_TOOL_INPUT_COMPACTION true Enable/disable compaction
OPENVIKING_TOOL_INPUT_MAX_CHARS 2000 Global truncation cap (0 = disabled)

Set OPENVIKING_TOOL_INPUT_COMPACTION=false to restore the old verbatim behavior.

Test Plan

  • 21 regression tests pass
  • 10.6x compression ratio on realistic mixed tool call data
  • JSON round-trip validation for all summary-mode tools
  • Edge cases: empty content, empty old_string, maxChars=0, string input pass-through
  • Manual verification: run a CC session with compaction enabled, check messages.jsonl size reduction
  • VLM extraction comparison: same session before/after compaction, compare memory extraction success

Closes #2875

@itxaiohanglover

Copy link
Copy Markdown

Nice implementation! Rule-based tool input compaction without LLM involvement is a smart approach — reduces cost and latency for auto-capture. The inline test functions avoid coupling to the full module. Tool input policies (full vs summary) are well-designed.

@lg320531124

Copy link
Copy Markdown
Contributor Author

The 06. API & CLI Integration Tests job fails here too with the identical tally — 3 failed, 25 passed, 7 skipped, 11 errors, all CONFLICT: Resource is busy on the session-scoped cli_test_4f5946d2 fixture.

I posted the full diagnosis on sibling PR #2900 (rebased onto latest main, head b00e336, same failure → rules out base-skew; two PRs touching disjoint code producing byte-identical failure counts = shared infra flake, not a code defect in either branch). Root cause points at the TreeLock-release path in openviking/utils/resource_processor.py:441retryable=True but the 15×@10s retry never recovers within the session fixture's lifetime.

This PR (feat/tool-input-compaction) touches tool-input compaction, not add-resource/locks/conftest, so the failing setup step is never reached by my code.

I don't have admin rights to rerun from a fork — could someone with access rerun the 06. API & CLI Integration Tests job on this branch? If it passes on retry that confirms the flake. (No code changes from my side — happy to act if it points back at the branch.)

@lg320531124

Copy link
Copy Markdown
Contributor Author

Opened #2916 to track the Resource is busy TreeLock flake at the root-cause level (CI-infra, not this PR). This PR and #2900 hit the identical tally on the same run despite touching disjoint modules — evidence chain and the rerun ask are in the issue.

lg320531124 added a commit to lg320531124/OpenViking that referenced this pull request Jul 1, 2026
buildParts() (structured tool parts sent to OV as tool_input) bypassed
compactToolInput, sending raw block.input objects while the prose
extractAllTurns path already compacted Write/Edit/Bash/Task* to
structural summaries. The ~10x storage reduction from volcengine#2874 only
covered the inlined-text path; structured parts carried full Write
content verbatim.

Mirror the prose path: apply compactToolInput when
cfg.toolInputCompaction !== false, fall back to formatToolInput
otherwise. Non-summary tools (Read/Glob/Grep) keep full input.
tool_input now a string (consistent with tool_output).

Adds 6 buildParts regression tests asserting parity with the prose
path for Write/Edit/Bash/Read, compaction=off fallback, and
non-object input handling.
lg320531124 added a commit to lg320531124/OpenViking that referenced this pull request Jul 2, 2026
buildParts() (structured tool parts sent to OV as tool_input) bypassed
compactToolInput, sending raw block.input objects while the prose
extractAllTurns path already compacted Write/Edit/Bash/Task* to
structural summaries. The ~10x storage reduction from volcengine#2874 only
covered the inlined-text path; structured parts carried full Write
content verbatim.

Mirror the prose path: apply compactToolInput when
cfg.toolInputCompaction !== false, fall back to formatToolInput
otherwise. Non-summary tools (Read/Glob/Grep) keep full input.
tool_input now a string (consistent with tool_output).

Adds 6 buildParts regression tests asserting parity with the prose
path for Write/Edit/Bash/Read, compaction=off fallback, and
non-object input handling.
@lg320531124
lg320531124 force-pushed the feat/tool-input-compaction branch from 2583cd0 to afe7c14 Compare July 2, 2026 23:28
yuanqingz and others added 14 commits July 9, 2026 11:06
Co-authored-by: Yuanqing Zhao <2604121+yuanqingz@users.noreply.github.com>
Add multimodal image vectorization and image query support across the server, SDKs, and CLI.
* fix(memory): avoid nesting rendered links

* fix(memory): cover spaces in link targets

Reviewer (@fujiajie666) noted that _RELATIVE_LINK_RE rejected whitespace
inside link targets, so an existing link like
  [Frank Ocean](entities/frank ocean.md)
was not detected as a link span, causing the next render to wrap the
inner 'Frank' and produce a nested broken link.

- Loosen _RELATIVE_LINK_RE to \[^)]+\ so the target may contain spaces
  (and \%20\ percent-encoded forms) when matching.
- Percent-encode spaces in generated link targets in render_links so the
  emitted markdown is portable across renderers.

Tests cover: skip inside existing literal-space link, encode spaces in
new targets, skip when existing target is %20-encoded, strip_links on
both literal-space and %20-encoded targets, full round-trip, and
idempotency on re-render.

---------

Co-authored-by: AITree69 <292109083+AITree69@users.noreply.github.com>
中文:对 HTTP/HTTPS URL 使用 urlparse(url).path 提取扩展名,确保 video.mp4?signature=... 命中 UnderstandingAPI fast path。新增带签名视频 URL 的 ParserRouter 回归测试。

English: Parse HTTP/HTTPS URL paths before checking extensions so signed video URLs hit the UnderstandingAPI fast path. Add a ParserRouter regression test for signed video URLs.

Co-authored-by: chenpengfei <chenpengfei@bytedance.com>
Disable recursive submodule fetching and avoid exposing remote Git errors to resource API callers.
Co-authored-by: huangruiteng <huangruiteng@bytedance.com>
…olcengine#3104)

Commit 4d34025 added the required `sender_name` parameter to
`BaseChannel._handle_message()` and updated `feishu.py`, but
`slack.py` and `email.py` were not updated in the same change.

This caused a `TypeError` on every inbound message in both channels.
Because Slack SDK swallows exceptions in asyncio listener callbacks,
the error was silent — the bot received events, added emoji reactions,
but never called the LLM or sent any reply.

Fix: pass `sender_name=sender_id` in SlackChannel and
`sender_name=sender` in EmailChannel.

Co-authored-by: scott.kim <scott@ScottMacBookPro.local>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…gine#3114)

* fix(vectordb): only write content field for VikingDB backends

* fix: update doc
Regenerate uv.lock metadata to match pyproject.toml after two upstream
deps changes that left the lock unreconciled:
- b596e26 (volcengine#2965): bump litellm pin <1.89.3 -> <1.90.3
- 47da6ce (volcengine#3037): merge bot-* extras into a single [bot] extra

Metadata-only diff (13 insertions / 66 deletions) — no resolved package
versions or hashes change. Confirmed by a clean `uv sync` leaving
uv.lock unmodified.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
ehz0ah and others added 30 commits August 1, 2026 22:55
…ne#3685)

* refactor(langchain): extract standalone integration package

* fix(langchain): preserve optional legacy imports

* fix(langchain): guard legacy submodule imports
* feat(ragfs): add structured lock tracing logs

* feat(ragfs): add log async writer

* fix(ragfs): add CRITICAL trans && add rust hook to receive rotate log file
…#3548)

* fix(server): stop exporting raw query strings and buffering zip responses in observability

Sweep findings: B-03, B-13. Prevent query secrets from reaching traces and keep ZIP responses streaming.

(cherry picked from commit d8ac3dc)

* fix(session): tolerate missing/corrupt archive in Phase-2 replay

(NotFoundError / _ArchiveMessagesCorruptError) on a missing or corrupt
archive messages.jsonl instead of returning []. That PR added skip-on-
missing tolerance to the read path (_get_uncovered_archive_messages) and to
resume_queued_commit, but not to the Phase-2 commit replay path
(_prepare_phase2_archive_messages), which calls _read_archive_messages
unguarded while rolling earlier failed archives into the current commit.

Consequence: a terminally-failed earlier archive whose messages.jsonl is
missing/corrupt (legacy "no messages" terminal data, or produced by volcengine#3417's
own archive_read terminal path) makes every subsequent commit's Phase-2
extraction raise -> caught by _run_memory_extraction's except -> the current
archive is terminal-failed too. Because the poisoned archive is only removed
from replay once "covered" (which requires a later archive to complete), and
no later archive can ever complete, the session's memory extraction is
permanently poisoned. Raw messages are safe, but extraction is stuck.

Fix: wrap the replay-loop _read_archive_messages call in the same tolerance
_get_uncovered_archive_messages already uses -- skip + warn on not-found
(_is_storage_not_found) and on _ArchiveMessagesCorruptError, re-raise real
storage failures. The skipped archive stays in covered_failed so the current
archive's .done marks it covered, clearing the poison permanently.

Adds a regression test asserting the replay skips a failed archive with a
missing messages.jsonl (and marks it covered) instead of raising, and that a
real storage failure still propagates.

Follow-up to volcengine#3417.

(cherry picked from commit 5b8ec9e)

* fix(client): align client surfaces without leaking memory metadata

Reconstructs the client-parity work from upstream PR volcengine#3439 on current main and strips reserved memory metadata before line slicing in both embedded and HTTP reads.

Based-on: 48b411d

Co-authored-by: zhiheng.liu <zhiheng.liu@bytedance.com>

* fix(index): propagate semantic vectorization failures safely

Reconstructs upstream PR volcengine#3437 on current main, carries enqueue failures through SemanticDagExecutor, and drains the attempt's embedding tracker before retry-visible failure propagation.

Based-on: 02387de

Co-authored-by: zhiheng.liu <zhiheng.liu@bytedance.com>

* fix(core): close privacy and embedding failure gaps

* fix(memory): strip repeated metadata trailers

* fix(core): close public memory visibility gaps

* ci: skip embedding-dependent resource test without secrets

---------

Co-authored-by: zhiheng.liu <zhiheng.liu@bytedance.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>
…olcengine#3706)

Route the query() random-sampling branch through search_by_vector with a
client-generated random vector (config.embedding.dimension) so every
backend behaves consistently, instead of each backend's server-side
search_by_random. This also makes Qdrant/OpenGauss truly random rather
than a deterministic scroll/scan.

Drop the raise_on_error path entirely per request: query(), the
Collection wrapper, and HttpCollection.search_by_random no longer take
raise_on_error, and delete() no longer requests it. As a result, HTTP
filter-based deletion id lookups now return empty on non-200 instead of
raising.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
volcengine#3704)

* feat(config): add enable_watch_scheduler toggle for read-only replicas

Add a top-level `enable_watch_scheduler` config flag (default true) and gate
`WatchScheduler.start()` on it in service initialization.

Read-only replicas that share a writer's data must not run the periodic
watch/refresh background loop: it would execute writes against shared storage
(vectordb / backups) and race the writer when persisting watch task state,
which has no cross-process lock. The scheduler instance is still created and
wired for on-demand read paths; only its background loop is skipped.

Defaults to true, so existing single-instance deployments are unaffected.

* fix

* fix
GET /api/v1/fs/attrs read search tags with a bare `uri` equality filter
and no level constraint. Over a path-typed `uri` field this does not pin
an exact node, and it merged tags across all levels, so a directory whose
tags live on its L0/L1 summary records could read back tags from an
unrelated record instead of its own.

Mirror how ContentWriteCoordinator.set_tags writes tags: a directory
carries them on its L0/L1 records and a file on its L2 record. Query with
And([Eq("uri", uri), In("level", levels)]) so Eq compiles to an exact
path match (-d=0) and the level set matches the node kind.

Co-authored-by: TRAE CLI <noreply@bytedance.com>
…ngs (volcengine#3695)

* feat(agent-evolution): record trajectory mapping in snapshot commits

* feat(agent-evolution): apply global switch immediately

* fix(snapshot): hide memory fields in visible history

* fix(agent-evolution): preserve batch snapshot provenance

* feat(agent-evolution): scope runtime settings by account

* fix(agent-evolution): serialize experience snapshot finalization
The bytes_row STRING type uses a uint16 length prefix, capping a single
field at 65535 bytes. Add a new TEXT field type (enum value 9) that mirrors
STRING semantics (utf-8 str round-trip) but uses a uint32 length prefix,
lifting the per-field limit to ~4GB.

TEXT is added only at the physical bytes_row layer, across all serializers
that must stay byte-identical: the C++ engine (bytes_row.h/.cpp), the abi3
boundary (abi3_engine_backend.cpp, decoding to str not bytes), the pure
Python fallback (store/bytes_row.py), and the engine API (_python_api.py).
Existing types and the CandidateData.fields field are untouched, so old
on-disk data stays readable without reindex.

Fields opt into the new type via metadata={"field_type": FieldType.text}.

Add TestTextFieldType covering >65535-byte round-trips, py<->cpp cross
read/write, binary consistency, and metadata-based declaration.

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 volcengine#3127
Related: volcengine#3442, volcengine#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 volcengine#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>
…engine#3710)

* feat(config): make external parse concurrency configurable

* refactor(config): move external parse concurrency to queue workers

---------

Co-authored-by: chenpengfei <chenpengfei@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>
volcengine#3730)

Surface the vector store's search_tags on each matched context (returned
under the "tags" key to match the tags filter param) and remove the
result fields the retrieval pipeline never populates (category,
match_reason, relations, overview).

Co-authored-by: TRAE CLI <noreply@bytedance.com>
…e#3727)

* feat(agent-evolution): track experience trajectory lineage

* fix(agent-evolution): return matched trajectory records

* fix(agent-evolution): stabilize lineage pagination
… mode="context" (volcengine#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>
…hots (volcengine#3742)

* feat(agent-evolution): aggregate trajectory outcomes by experience

* fix(agent-evolution): snapshot only visible experience changes

* docs(api): document agent evolution queries
…engine#3738)

* fix(compile): resolve pickle error and merge skill_name changes

* fix
Resolve import conflict in subagent-stop.mjs (keep both workspace-peer and
compact-tool-input imports). Fix package.json test script to include
scripts/__tests__/*.test.mjs so the compaction tests actually run.
… recalls (volcengine#3746)

* fix(retrieval): honor tier ceilings and stop cooling unserved recalls

Follow-up to volcengine#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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

feat: rule-based tool input compaction for auto-capture pipeline