* feat(workspace): multi-scope workspace reads
Adds the ability for a workspace to read from multiple user scopes
while keeping writes isolated to the primary scope. Configuration
via WORKSPACE_READ_SCOPES env var (comma-separated user IDs).
Includes identity file isolation (read_primary), multi-scope search,
list, and read operations, WorkspaceConfig refactor, and comprehensive
integration tests.
* fix: address review feedback for multi-scope workspace reads
- fix(memory): deduplicate timezone parsing for daily_log target
parse_timezone was called twice when target was "daily_log" without a
layer — once in path resolution, again in the fallback. Now computed
once and reused.
- fix(config): add character validation for WORKSPACE_READ_SCOPES and
layer scopes — both enforce [a-zA-Z0-9_-] to prevent path traversal
or injection via scope strings used as user_id in SQL queries.
- fix(config): use chars().take(32) instead of byte-index slicing for
scope length error messages (UTF-8 safety).
- fix(error): remove unused WorkspaceError::NotFound variant
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* style: downgrade search log to debug, add comments on list iteration
- Downgrade hybrid_search_multi tracing::info! to debug! — fires on
every multi-scope search with the default backend, too noisy for info
- Add comments explaining why list/list_all iterate per-scope instead
of using _multi trait methods (identity path filtering needs scope
attribution that merged results lose)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: [email protected] <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
* feat: configurable hybrid search fusion strategy (#169)
Add WeightedScore fusion as an alternative to the default RRF algorithm.
Users can now tune search behavior via env vars (SEARCH_FUSION_STRATEGY,
SEARCH_FTS_WEIGHT, SEARCH_VECTOR_WEIGHT, SEARCH_RRF_K) or by passing
SearchConfig with the new fields. Default behavior (RRF, k=60) is
unchanged.
- Add FusionStrategy enum (Rrf/WeightedScore) to workspace::search
- Add weighted_score_fusion() and fuse_results() dispatcher
- Add config/search.rs with WorkspaceSearchConfig from env vars
- Wire search defaults through Workspace struct
- Update both postgres and libsql backends to use fuse_results()
- Add 7 new tests (4 fusion + 3 config)
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: swap default search weights to match issue #169 spec (0.7 vector / 0.3 FTS)
The issue spec says "0.7/0.3 (vector/keyword) for weighted mode" but
our defaults had fts_weight=0.7, vector_weight=0.3 (inverted). Also
fixes the misleading docstring on weighted_score_fusion that claimed
1/rank normalizes to [0,1].
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: validate weight inputs and update stale doc comments
- Reject NaN, infinite, and negative values for SEARCH_FTS_WEIGHT and
SEARCH_VECTOR_WEIGHT with a clear ConfigError
- Fix module-level docs that incorrectly claimed WeightedScore
"normalizes per-method scores to [0,1]"
- Update SearchResult.score doc from "Combined RRF score" to
strategy-agnostic "Combined fusion score"
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: validate weight setters against NaN/inf/negative values
with_fts_weight() and with_vector_weight() now silently ignore
non-finite (NaN, ±inf) and negative values, matching the env var
validation already in place for SEARCH_FTS_WEIGHT / SEARCH_VECTOR_WEIGHT.
Values > 1.0 remain valid since weights are normalized internally.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: use crate-wide ENV_MUTEX in search config tests
Replace the module-local `ENV_MUTEX` in `search.rs` with a shared
`crate::config::helpers::ENV_MUTEX` to prevent cross-module env races
when `cargo test` runs tests in parallel.
Addresses copilot review comment. Tracked in #245.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: per-strategy weight defaults to match issue #169 spec
RRF mode now defaults to 0.5/0.5 (fts/vector) and WeightedScore
defaults to 0.3/0.7, matching the acceptance criteria in #169.
Previously both modes used 0.3/0.7 uniformly.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: reject both weights=0 in weighted fusion mode
When both SEARCH_FTS_WEIGHT and SEARCH_VECTOR_WEIGHT are 0.0 under
WeightedScore strategy, all scores would be 0.0, producing arbitrary
ordering. RRF mode is unaffected since it ignores weights entirely.
Addresses Copilot review comment. The other comment (rrf_k=0 division
by zero) is a false positive — ranks are 1-based, so k=0 just gives
inverse-rank scoring with no infinity.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: clarify weight doc comments and error key
- SearchConfig field docs: clarify that Default always uses 0.5,
per-strategy defaults only apply via WorkspaceSearchConfig::resolve()
- WorkspaceSearchConfig field docs: same clarification
- Error key for both-weights-zero now references both env vars
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: remove broken intra-doc links to pub(crate) resolve()
WorkspaceSearchConfig::resolve is pub(crate), so linking to it from
public field docs triggers rustdoc private_intra_doc_links warnings.
Switch to plain-text references.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* fix: add document_path to weighted_score_fusion results
The weighted_score_fusion function was missing the document_path field
added in a recent main branch commit, causing a compile error after rebase.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* chore: trigger CI re-check after rebase
* fix: resolve pre-existing staging fmt and clippy issues
- Fix import ordering in cli/mod.rs (cargo fmt)
- Fix line wrapping in tools/mcp/auth.rs (cargo fmt)
- Move path_routing_tests before MemoryTreeTool to fix
clippy::items_after_test_module
[skip-regression-check]
* fix: remove duplicate path_routing_tests module after rebase
[skip-regression-check]
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
* fix(workspace): thread document path through search results
Memory search results were showing chunk UUIDs instead of source file
paths. Thread document_path through RankedResult, SearchResult, and the
RRF fusion pipeline so handlers can display the actual file path.
Fixes#481
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* refactor: use into_iter to move values instead of cloning
Address review feedback: consume results with into_iter() to move
String fields directly instead of cloning them.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
- Consolidate all migrations into V1__initial.sql
- Replace DocType enum with flexible path-based file storage
- Add list_workspace_files SQL function for directory listing
- Update memory tools for path-based API (memory_read, memory_write,
memory_search, memory_list)
- Remove unused OpenAI/Anthropic providers (NEAR AI only)
- Simplify config to remove multi-provider support
- Update CLAUDE.md documentation
Co-Authored-By: Claude Opus 4.5 <[email protected]>