The text-only gate sniffs the first 8192 bytes of a document and rejects
the read when they are not valid UTF-8. The window was cut at a plain byte
offset, so on non-ASCII text it regularly landed inside a multi-byte rune
and left a dangling prefix. The gate then reported the document as binary
even though the file was intact — measured on Vietnamese documents, about
one in four files above 8 KB was blocked, depending on where the accented
characters happen to fall.
Trim up to 3 bytes off the window when, and only when, the content was
actually cut there, then judge. A file that fits inside the window is left
alone, so genuinely broken trailing bytes are still caught, and binary
content stays blocked because its invalid bytes are spread over the whole
window. readCapped had the same byte-offset cut at max_bytes, which put a
broken character at the end of every truncated non-ASCII document.
vault_read is a singleton wired once at boot with the global workspace
root, then joins doc.Path (stored relative to the TENANT root) onto it.
For master tenant both roots coincide, so the bug stayed invisible; every
other tenant reads ENOENT because its files live under tenants/<slug>/.
vault_search is DB-only and keeps returning the doc, so the symptom is
"search finds it, read fails" rather than an obvious missing file.
Resolve the tenant-scoped root per request via TenantLayer, which is a
no-op for master and preserves existing single-tenant behaviour. Passing
the tenant root into resolvePath also narrows the boundary check, so it
now doubles as cross-tenant isolation.
Runs originating from channels or cron may not carry the tenant slug in
context; without it TenantLayer falls back to a UUID-named directory that
does not match the slug-named one the write path creates. Add a nil-safe
TenantStore lookup for that case only.
- Add vault_documents.chat_id + composite index (migration 000056)
- Filter vault_search by chat_id when team.workspace_scope=isolated
- Stamp chat_id on AfterWrite/AfterWriteMedia for isolated teams
- Deny cross-chat vault_read in isolated teams (M2 fix)
- RunContext.TeamIsolated flag resolved once per run
- Fallback WorkspaceChatID → ChatID in loop_context for entry points
that don't set WorkspaceChatID explicitly (WS direct, HTTP, cron)
Fixes cross-chat doc leak where agent in chat A could see vault docs
from chat B within the same isolated team.
* fix(vault): prevent vault_read id-namespace collision
vault_search was leaking KG/episodic entity ids into result sets even when
narrow `types` were requested, and callers then passed those ids to
vault_read which returned a generic "document not found". The cause was
threefold:
1. `types` filter was only applied to the vault fan-out; KG and episodic
ran unconditionally. Now gated by shouldFanout(types, key).
2. vault_search output lacked a per-source tool hint. Each result now ends
with " → use <tool>" naming the correct follow-up (vault_read,
knowledge_graph_search, or memory_search).
3. vault_read miss returned "document not found" without checking whether
the id belonged to a foreign namespace. It now probes KG then episodic
and returns a namespace-specific redirect error. Stores are injected
via SetKGStore/SetEpisodicStore, nil-safe, tenant-scoped.
Adds red→green characterization tests plus an end-to-end integration
scenario seeding a vault doc + KG entity with identical basenames.
* test(agent): bump none-mode prompt size budget to 3100
vault_read wiring (#948) added ~95 chars to read_file tool summary,
pushing none-mode prompt from <3000 to 3075 chars. Bump budget to
3100 (~775 tokens) to match the intentional addition.
* test(integration): ensure data_migrations table exists in reset helper
The reset helper runs before RunPendingHooks, but RunPendingHooks is
what normally creates data_migrations. On a fresh CI database the
DELETE fails with 'relation does not exist'. Create the table
defensively so reset works regardless of execution order.
* refactor(vault): per-source id fields + wire episodic into search
Align vault_search output fields with downstream tool input params:
doc_id (vault_read), entity_id (knowledge_graph_search), episodic_id
(memory_expand). Prevents LLMs from pattern-matching a generic `id:`
and misrouting a foreign-namespace uuid into vault_read. Fallback
redirect in vault_read now quotes id + names the correct param so
the LLM can self-correct in one turn.
Also wire stores.Episodic into VaultSearchService (stale comment
claimed pending-impl; PGEpisodicStore has existed and been in use
since v3). Unifies search fan-out with vault_read namespace probe.
---------
Co-authored-by: viettranx <viettranx@gmail.com>
List up to 20 wikilink/reference outlinks as "## Links" after the doc
body, deduped by target, scope-filtered via existing allowed() matrix.
Non-fatal — store errors log and skip the footer.
Agents could not read vault docs registered at the tenant workspace root
because read_file enforces a per-agent workspace boundary. vault_read
fetches content by doc_id (from vault_search) with scope-based access
checks (shared/personal/team), a 3-layer text-only gate (DocType +
extension blocklist + UTF-8 sniff), 500KB default cap / 1MB ceiling,
and symlink/hardlink guards shared with read_file. Updated system-prompt
summaries and boundary-error hints so the LLM chains vault_search →
vault_read instead of retrying read_file.