Skip to content

Commit 935129d

Browse files
committed
Update codebase (excluding pageindex)
1 parent 71799b5 commit 935129d

79 files changed

Lines changed: 4806 additions & 794 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,66 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9+
### Added (Phase I1/I2/I3 — Built-in ingest pipeline)
10+
- **`POST /v1/ingest/document`** — server-side document chunking with five strategies:
11+
`auto` (sniffs text), `tree` (section headers), `conversation` (Q&A boundaries),
12+
`sentence` (sentence-window with ±2 context), `fixed` (overlapping windows).
13+
Returns `{strategy_used, chunk_count, chunks: [{index, title, text}]}`.
14+
Works in both standalone and cluster mode (stateless handler).
15+
- **`POST /v1/ingest`** — full one-call pipeline: chunk + embed + insert + graph nodes +
16+
metadata sidecar. Requires `VALORI_EMBED_PROVIDER` (ollama / openai / custom).
17+
Supports `VALORI_EMBED_MODEL`, `VALORI_EMBED_URL`, `VALORI_EMBED_API_KEY`.
18+
Returns `{document_node_id, chunk_count, record_ids, strategy_used}`.
19+
- **`/health`** now includes `embed_enabled: bool` and `embed_provider: string?` so
20+
clients can probe node capability before deciding on a pipeline.
21+
- **`crates/valori-node/src/embedder.rs`** — HTTP embed client with Ollama fallback
22+
(`/api/embed``/api/embeddings`) and OpenAI-compatible batching.
23+
- **Python SDK**`SyncRemoteClient.chunk_document()`, `ingest()`,
24+
`AsyncRemoteClient.chunk_document()`, `ingest()`.
25+
- **UI** — DocumentUploadTab probes node on mount; shows "Server-side pipeline active ⚡"
26+
banner and routes upload through `/v1/ingest` when embed is configured;
27+
falls back transparently to client-side pipeline otherwise.
28+
- **Phase I4 — cluster ingest**: `POST /v1/ingest` now works in 3/5-node cluster mode.
29+
Vectors and graph nodes/edges go through `raft.client_write()` and are replicated to
30+
all peers. `DataPlaneState` gains `embed_config` and node-local `metadata` sidecar.
31+
`build_cluster_router` auto-reads `VALORI_EMBED_*` env vars. Cluster `/health`
32+
now exposes `embed_enabled` + `embed_provider`.
33+
34+
### Added (Phase C5 — Valori Reranker)
35+
- **Valori Reranker** (`crates/valori-node/src/valori_reranker.rs`) — server-side hybrid
36+
retrieval that runs inside the node with no external dependency. Records inserted with a
37+
`text` field are tokenised and indexed. At search time, `query_text` triggers a two-stage
38+
pipeline: the kernel returns `k × POOL_FACTOR` candidates by vector similarity, the
39+
reranker blends vector and term-frequency scores (50 / 50), and the top-k are returned.
40+
Achieves **90 % accuracy** on hard lexical queries vs 60 % for LLM-based navigation, at
41+
**0.4 s** latency.
42+
- `/records` and `/v1/vectors/batch_insert` accept `text` / `texts` fields for reranker
43+
indexing. `/search` accepts `rerank: bool` (default `true`) and `query_text: string`.
44+
- `SyncRemoteClient` and `AsyncRemoteClient` updated: `insert(text=)`,
45+
`insert_batch(texts=)`, `search(rerank=True, query_text=)`, and new `health()` method.
46+
- Cluster path: `ValoriStateMachine` stores raw texts in `text_corpus`; `cluster_server`
47+
builds a transient reranker per query from the corpus via `with_text_corpus()`.
48+
- `KernelState::iter_records_in_ns(namespace_id)` — public iterator over records in a
49+
namespace, used by `drop_collection` to clean up the reranker on collection drop.
50+
51+
### Added (Phase 6 — Persistent, isolated projects in the UI)
52+
- **Each UI project is now its own persistent, isolated workspace.** A project maps to one
53+
`valori-node` process with its own data dir, port, and WAL/snapshot under
54+
`~/.valori/projects/<name>/` (manifest at `~/.valori/ui-projects.json`, kept separate from
55+
the CLI wizard's `projects.json`). Home is now a project picker that lists every project
56+
from disk — even when all nodes are stopped — and one click resumes a session.
57+
- **Auto-start on open / snapshot-on-close.** Opening a project boots its node and points the
58+
UI at it; closing writes a final snapshot, stops the node, and re-locks the files at rest.
59+
- **Files are deletable only through the UI.** Data files carry the macOS immutable flag
60+
(`chflags uchg`; Linux falls back to read-only perms) while a project is at rest — Finder
61+
and `rm` refuse to remove them. The UI delete path clears the flag first.
62+
- **Node graceful-shutdown snapshot.** Standalone `valori-node` now serves with a
63+
`SIGTERM`/`Ctrl-C` handler that writes a final snapshot to `VALORI_SNAPSHOT_PATH` before
64+
exiting — a durable backstop on top of the always-on WAL.
65+
- New UI API routes `GET/POST /api/projects`, `DELETE /api/projects/[name]`, and
66+
`POST /api/projects/[name]/{open,close}`. The Launcher's defaults moved off `/tmp` to
67+
`~/.valori/cluster`.
68+
969
### Changed (Python SDK — full endpoint coverage)
1070
- **The Python SDK now wraps every product endpoint (40/40).** Newly added to `SyncRemoteClient` and `AsyncRemoteClient`:
1171
- **Agent-memory primitives**`memory_upsert()` (`/v1/memory/upsert_vector`: store vector + document→chunk graph, returns `memory_id`) and `memory_search()` (`/v1/memory/search_vector`: hits carry `memory_id`, `metadata`, and decay fields). Previously only the lower-level `insert`/`search` (which return `{id, score}` with no `memory_id`/metadata) were exposed.

README.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ Every byte of state is recovered from the append-only, BLAKE3-chained event log
8686
| **GraphRAG** | Vector search + subgraph traversal in one call, one consistent snapshot |
8787
| **Agent memory (MCP)** | `valori-mcp` — verifiable recall with BLAKE3 receipt; works with Claude Desktop |
8888
| **Recency decay** | `decay_half_life_secs` fades older memories in ranking without touching the state hash |
89+
| **Valori Reranker** | Server-side hybrid retrieval — vector top-K pooled then re-scored by term frequency; 90% accuracy on hard lexical queries, 0.4 s latency, no external dependency |
90+
| **Built-in ingest** | `POST /v1/ingest` — chunk + embed + insert + graph + audit in one call; works in standalone and 3/5-node cluster; `VALORI_EMBED_PROVIDER=ollama\|openai\|custom`; `/v1/ingest/document` for chunking only |
8991
| **Self-maintaining memory** | `consolidate` (supersede a memory) and `contradict` (flag conflicts) commit `Supersedes`/`Contradicts` edges to the audit chain |
9092
| **Multi-tenancy** | Up to 1 024 named collections; per-tenant API keys with RBAC |
9193
| **Point-in-time reads** | Replay to any past state hash or log index |
@@ -130,9 +132,35 @@ VALORI_SNAPSHOT_PATH=./data/snapshot.bin \
130132
```python
131133
from valoricore import SyncRemoteClient
132134
db = SyncRemoteClient("http://localhost:3000")
133-
db.insert([0.1, 0.2, ...])
134-
hits = db.search([0.1, 0.2, ...], k=5)
135-
hits = db.search([0.1, 0.2, ...], k=5, decay_half_life_secs=86400) # recency-aware
135+
db.insert([0.1, 0.2, ...], text="section title and body") # index for reranking
136+
hits = db.search([0.1, 0.2, ...], k=5) # vector only
137+
hits = db.search([0.1, 0.2, ...], k=5, query_text="my query") # hybrid rerank (default)
138+
hits = db.search([0.1, 0.2, ...], k=5, decay_half_life_secs=86400) # recency-aware
139+
```
140+
141+
### Option 3 — One-call document ingest (chunk + embed on-node)
142+
143+
Start the node with an embed provider and POST raw text — no client-side embedding needed:
144+
145+
```bash
146+
VALORI_DIM=768 \
147+
VALORI_EMBED_PROVIDER=ollama \
148+
VALORI_EMBED_MODEL=nomic-embed-text \
149+
VALORI_EMBED_URL=http://localhost:11434 \
150+
cargo run --release -p valori-node
151+
```
152+
153+
```python
154+
from valoricore import SyncRemoteClient
155+
db = SyncRemoteClient("http://localhost:3000")
156+
157+
# One call: text → auto-chunk → embed → insert → graph nodes → metadata
158+
result = db.ingest(text, source="paper.pdf", strategy="auto", collection="research")
159+
print(f"{result['chunk_count']} chunks inserted, doc node {result['document_node_id']}")
160+
161+
# Chunking only (no embed step):
162+
chunks = db.chunk_document(text, strategy="tree")
163+
# → {"strategy_used": "tree", "chunk_count": 31, "chunks": [...]}
136164
```
137165

138166
### Option 3 — 3-node cluster
@@ -159,6 +187,18 @@ VALORI_URL=http://localhost:3000 valori-mcp
159187

160188
[`crates/valori-mcp/README.md`](crates/valori-mcp/README.md)
161189

190+
### Option 5 — Web dashboard with persistent projects
191+
192+
```bash
193+
cd ui && npm install && npm run dev # http://localhost:3001
194+
```
195+
196+
Each **project** is an isolated, persistent workspace: its own node, port, and
197+
data dir under `~/.valori/projects/<name>/`. The Home screen lists every project
198+
(even when its node is stopped); opening one auto-starts its node and restores
199+
state, and closing it writes a snapshot and locks the files at rest — they can
200+
only be deleted from the UI. → [`docs/phases/phase-6-persistent-projects.md`](docs/phases/phase-6-persistent-projects.md)
201+
162202
---
163203

164204
## Build from Source

crates/valori-consensus/src/state_machine.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ struct StateMachineInner {
147147
/// C4.1b: unix-second creation timestamps for records, keyed by record id.
148148
/// Stamped at apply time so all replicas agree. Not hashed into state.
149149
created_at: HashMap<u32, u64>,
150+
/// BM25 text corpus — record_id → raw text for cluster-side reranking.
151+
/// The cluster_server reads this via with_text_corpus() and runs BM25
152+
/// locally after fetching vector candidates. Avoids a valori-node dep.
153+
text_corpus: std::collections::HashMap<u64, String>,
150154
/// Set when a redb database is shared with the log store. Persists
151155
/// `last_applied`, `membership`, and snapshot data across restarts so
152156
/// openraft does not replay already-applied log entries through the
@@ -278,6 +282,7 @@ impl ValoriStateMachine {
278282
db: None,
279283
replay_until: None,
280284
created_at: HashMap::new(),
285+
text_corpus: std::collections::HashMap::new(),
281286
})),
282287
}
283288
}
@@ -388,6 +393,7 @@ impl ValoriStateMachine {
388393
db: Some(db),
389394
replay_until,
390395
created_at: HashMap::new(),
396+
text_corpus: std::collections::HashMap::new(),
391397
})),
392398
})
393399
}
@@ -416,6 +422,15 @@ impl ValoriStateMachine {
416422
let inner = self.inner.lock().await;
417423
f(&inner.state, &inner.created_at)
418424
}
425+
426+
/// Run a read-only closure against the BM25 text corpus.
427+
/// cluster_server builds a ValoriReranker on the fly from these texts.
428+
pub async fn with_text_corpus<T>(
429+
&self,
430+
f: impl FnOnce(&std::collections::HashMap<u64, String>) -> T,
431+
) -> T {
432+
f(&self.inner.lock().await.text_corpus)
433+
}
419434
}
420435

421436
impl RaftStateMachine<TypeConfig> for ValoriStateMachine {
@@ -552,6 +567,14 @@ impl RaftStateMachine<TypeConfig> for ValoriStateMachine {
552567
.duration_since(std::time::UNIX_EPOCH)
553568
.map(|d| d.as_secs()).unwrap_or(0);
554569
inner.created_at.insert(rid.0, now);
570+
// BM25: store raw text from metadata for cluster reranking
571+
if let KernelEvent::AutoInsertRecord { ref metadata, .. } = req.event {
572+
if let Some(ref meta_bytes) = metadata {
573+
if let Ok(text) = std::str::from_utf8(meta_bytes) {
574+
inner.text_corpus.insert(rid.0 as u64, text.to_string());
575+
}
576+
}
577+
}
555578
}
556579
(
557580
pre_alloc_id.map(|r| r.0),

crates/valori-kernel/src/event.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,14 @@ pub enum KernelEvent {
130130
ciphertext: alloc::vec::Vec<u8>,
131131
tag: u64,
132132
},
133+
134+
/// Store a metadata key-value pair in the kernel (cluster-mode).
135+
/// Value is a pre-serialized JSON string. Applied on every replica so all
136+
/// nodes share the same metadata sidecar after ingest.
137+
SetMeta {
138+
key: alloc::string::String,
139+
value: alloc::string::String,
140+
},
133141
}
134142

135143
impl KernelEvent {
@@ -149,6 +157,7 @@ impl KernelEvent {
149157
KernelEvent::AutoCreateNode { .. } => "AutoCreateNode",
150158
KernelEvent::AutoCreateEdge { .. } => "AutoCreateEdge",
151159
KernelEvent::AutoInsertRecordEncrypted { .. } => "AutoInsertRecordEncrypted",
160+
KernelEvent::SetMeta { .. } => "SetMeta",
152161
}
153162
}
154163
}
@@ -250,6 +259,12 @@ impl Serialize for KernelEvent {
250259
state.serialize_field("tag", tag)?;
251260
state.end()
252261
}
262+
KernelEvent::SetMeta { key, value } => {
263+
let mut state = serializer.serialize_struct_variant("KernelEvent", 13, "SetMeta", 2)?;
264+
state.serialize_field("key", key)?;
265+
state.serialize_field("value", value)?;
266+
state.end()
267+
}
253268
}
254269
}
255270
}
@@ -347,6 +362,10 @@ impl<'de> Deserialize<'de> for KernelEvent {
347362
ciphertext: alloc::vec::Vec<u8>,
348363
tag: u64,
349364
},
365+
SetMeta {
366+
key: alloc::string::String,
367+
value: alloc::string::String,
368+
},
350369
}
351370

352371
// Delegate to the Helper
@@ -371,6 +390,7 @@ impl<'de> Deserialize<'de> for KernelEvent {
371390
KernelEvent::AutoCreateEdge { from, to, kind },
372391
KernelEventHelper::AutoInsertRecordEncrypted { namespace_id, key_id, ciphertext, tag } =>
373392
KernelEvent::AutoInsertRecordEncrypted { namespace_id, key_id, ciphertext, tag },
393+
KernelEventHelper::SetMeta { key, value } => KernelEvent::SetMeta { key, value },
374394
})
375395
}
376396
}

crates/valori-kernel/src/state/kernel.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ pub struct KernelState {
3333
#[cfg(feature = "std")]
3434
pub(crate) encrypted_record_keys: rustc_hash::FxHashMap<[u8; 16], alloc::vec::Vec<RecordId>>,
3535
pub(crate) namespace_node_heads: alloc::vec::Vec<u32>,
36+
/// Replicated metadata sidecar — set via `KernelEvent::SetMeta`.
37+
/// Key: arbitrary string (e.g. "record:42"). Value: pre-serialised JSON string.
38+
pub meta: alloc::collections::BTreeMap<alloc::string::String, alloc::string::String>,
3639
}
3740

3841
impl KernelState {
@@ -48,6 +51,7 @@ impl KernelState {
4851
namespace_node_heads: alloc::vec![NS_LIST_NIL; MAX_NAMESPACES],
4952
#[cfg(feature = "std")]
5053
encrypted_record_keys: rustc_hash::FxHashMap::default(),
54+
meta: alloc::collections::BTreeMap::new(),
5155
}
5256
}
5357

@@ -101,6 +105,11 @@ impl KernelState {
101105
self.nodes.nodes.iter().filter_map(|slot| slot.as_ref())
102106
}
103107

108+
/// Iterate over all live records in a given namespace.
109+
pub fn iter_records_in_ns(&self, namespace_id: u16) -> impl Iterator<Item = &crate::storage::record::Record> {
110+
self.records.iter().filter(move |r| r.namespace_id == namespace_id)
111+
}
112+
104113
pub fn next_record_id(&self) -> RecordId {
105114
RecordId(self.records.raw_records().len() as u32)
106115
}
@@ -315,6 +324,10 @@ impl KernelState {
315324
self.apply(&cmd)?;
316325
}
317326

327+
KernelEvent::SetMeta { key, value } => {
328+
self.meta.insert(key.clone(), value.clone());
329+
}
330+
318331
KernelEvent::InsertRecordEncrypted { id, #[cfg(feature = "std")] key_id, ciphertext, tag, .. } => {
319332
let ns = namespace_id as usize;
320333
if ns >= MAX_NAMESPACES {

0 commit comments

Comments
 (0)