Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ repository = "https://github.com/TerminallyLazy/Tree-Ring-Memory"

[workspace.dependencies]
chrono = { version = "0.4", features = ["serde", "clock"] }
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
libc = "0.2"
once_cell = "1"
regex = "1"
rusqlite = { version = "0.32", features = ["bundled"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
tempfile = "3"
thiserror = "1"
uuid = { version = "1", features = ["v4"] }
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Tree Ring Memory is in protocol-preview status. Current launch links:
project truths.
- Rust-native import/export, audit, consolidation, maintenance, DOX/Revolve
adapters, harness discovery, and terminal UI.
- Same-host multi-agent correlation, scoped recall filters, and idempotent
worker writes through the public CLI.
- Evidence artifacts for install size, recall speed, harness readiness, and
recall quality.
- Privacy defaults that block secret-like memory, hide sensitive details, and
Expand Down Expand Up @@ -236,6 +238,70 @@ Command ownership is Rust-native:
- `export`, `import`, `audit`, `consolidate`, and `maintain` are local maintenance surfaces over the same SQLite store.
- `welcome` and `tui` are the terminal onboarding and operator-console surfaces.

## Same-Host Multi-Agent Workflow

A coordinator can fan work out to multiple local CLI processes that share one
Tree Ring root. Give every worker a unique agent profile and operation ID while
sharing the workflow and session:

```bash
tree-ring --root .tree-ring remember "Storage worker validated WAL behavior." \
--event-type lesson \
--scope agent \
--project example-service \
--agent-profile worker-storage \
--workflow-id release-readiness \
--session-id attempt-1 \
--operation-id validate-storage-v1 \
--source-ref runs/release-readiness/worker-storage.json
```

At fan-in, omit the agent-profile filter so the coordinator sees every
agent-partitioned result:

```bash
tree-ring --root .tree-ring --json recall "release readiness" \
--project example-service \
--workflow-id release-readiness \
--session-id attempt-1 \
--scope agent \
--limit 64
```

When writing, `scope=agent` requires `agent_profile`, `scope=workflow` requires
`workflow_id`, and `scope=session` requires `session_id`. A coordinator's
aggregate recall may intentionally omit the agent-profile filter. Project and
global scopes remain shared. These are routing and consolidation partitions,
not ACLs; any process with filesystem access to the store can perform
unfiltered recall.
Pre-0.12 private-scope records that lack the now-required identity are migrated
to a deterministic, per-record `legacy-*` partition and marked for review
instead of being widened into shared scope or becoming unexportable.

`TREE_RING_AGENT_PROFILE`, `TREE_RING_WORKFLOW_ID`, and
`TREE_RING_SESSION_ID` can supply the matching flag defaults. `operation_id`
provides write idempotency inside the `(project, workflow_id, agent_profile)`
namespace: an exact retry returns the existing memory ID, while a different
payload using the same key fails nonzero. Session ID is retained context but is
not part of that idempotency namespace. Replacing an active row preserves its
prior operation namespace as a one-way claim. Redaction also keeps a memory-ID
tombstone, so replacement import cannot restore the payload by omitting the
operation ID; only explicit hard deletion releases those claims.

The shared-root contract is limited to concurrent processes on one host using a
local filesystem. Tree Ring does not claim distributed locking, cross-host
SQLite coordination, or safe database sharing over NFS/network filesystems.
For work spanning hosts, keep per-host roots and use an explicit,
evidence-preserving fan-in.

The bounded acceptance test at
`crates/tree-ring-memory-cli/tests/multi_agent_acceptance.rs` holds a real
SQLite write lock, starts eight real CLI workers, verifies they wait and then
complete, exercises each recall filter and operation conflict behavior, and
checks exact row/FTS parity through `tree-ring --json maintain`. This is
same-host evidence, not sustained-load, crash-recovery, fairness, or distributed
storage certification.

## Evidence Loop

The Revolve-inspired loop is exposed through `tree-ring evidence`. It records
Expand Down Expand Up @@ -422,6 +488,7 @@ cargo run -p tree-ring-memory-cli -- maintain --help
cargo run -p tree-ring-memory-cli -- dox sync --help
cargo run -p tree-ring-memory-cli -- revolve sync --help
cargo run -p tree-ring-memory-cli -- integrations scan --help
cargo test -p tree-ring-memory-cli --test multi_agent_acceptance
cargo run --release -p tree-ring-memory-sqlite --example performance_smoke -- 1000
sh scripts/certify-tree-ring.sh
sh scripts/package-release.sh
Expand Down
6 changes: 6 additions & 0 deletions crates/tree-ring-memory-cli/src/actions/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub struct ConsolidateActionRequest {
pub period_type: String,
pub period_key: Option<String>,
pub project: Option<String>,
pub agent_profile: Option<String>,
pub workflow_id: Option<String>,
pub session_id: Option<String>,
pub dry_run: bool,
pub force: bool,
}
Expand All @@ -34,6 +37,9 @@ pub fn consolidation_request(
.map_err(|err| err.to_string())?,
period_key: request.period_key,
project: request.project,
agent_profile: request.agent_profile,
workflow_id: request.workflow_id,
session_id: request.session_id,
dry_run: request.dry_run,
force: request.force,
})
Expand Down
75 changes: 64 additions & 11 deletions crates/tree-ring-memory-cli/src/actions/recall.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use tree_ring_memory_sqlite::{MemoryRetriever, RecallResult, SQLiteMemoryStore};
use tree_ring_memory_sqlite::{MemoryRetriever, RecallOptions, RecallResult, SQLiteMemoryStore};

use super::ActionResult;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecallRequest {
pub query: String,
pub project: Option<String>,
pub agent_profile: Option<String>,
pub workflow_id: Option<String>,
pub session_id: Option<String>,
pub scope: Option<String>,
pub limit: usize,
pub include_sensitive: bool,
pub include_superseded: bool,
Expand All @@ -19,17 +23,21 @@ pub struct RecallReport {

pub fn recall(store: &SQLiteMemoryStore, request: RecallRequest) -> ActionResult<RecallReport> {
let results = MemoryRetriever::new(store)
.recall(
.recall_with_options(
&request.query,
request.project.as_deref(),
None,
None,
None,
None,
request.include_sensitive,
request.include_superseded,
request.limit,
request.explain,
&RecallOptions {
project: request.project.as_deref(),
agent_profile: request.agent_profile.as_deref(),
workflow_id: request.workflow_id.as_deref(),
session_id: request.session_id.as_deref(),
scope: request.scope.as_deref(),
rings: None,
event_types: None,
include_sensitive: request.include_sensitive,
include_superseded: request.include_superseded,
limit: request.limit,
explain_ranking: request.explain,
},
)
.map_err(|err| err.to_string())?;
Ok(RecallReport { results })
Expand All @@ -54,6 +62,10 @@ mod tests {
RecallRequest {
query: "shared recall".to_string(),
project: None,
agent_profile: None,
workflow_id: None,
session_id: None,
scope: None,
limit: 8,
include_sensitive: false,
include_superseded: false,
Expand All @@ -66,4 +78,45 @@ mod tests {
assert_eq!(report.results[0].memory.id, event.id);
assert!(report.results[0].ranking.contains_key("textual_match"));
}

#[test]
fn recall_action_filters_multi_agent_context_before_ranking() {
let dir = tempdir().unwrap();
let mut store = SQLiteMemoryStore::open(dir.path().join("memory.sqlite")).unwrap();
for (agent, workflow, session) in [
("researcher", "fanout-7", "attempt-1"),
("reviewer", "fanout-7", "attempt-1"),
("researcher", "fanout-8", "attempt-2"),
] {
let mut event = MemoryEvent::new("Shared phrase from worker.", "lesson").unwrap();
event.scope = "agent".to_string();
event.agent_profile = Some(agent.to_string());
event.workflow_id = Some(workflow.to_string());
event.session_id = Some(session.to_string());
store.put(&event).unwrap();
}

let report = recall(
&store,
RecallRequest {
query: "shared phrase worker".to_string(),
project: None,
agent_profile: Some("researcher".to_string()),
workflow_id: Some("fanout-7".to_string()),
session_id: Some("attempt-1".to_string()),
scope: Some("agent".to_string()),
limit: 8,
include_sensitive: false,
include_superseded: false,
explain: false,
},
)
.unwrap();

assert_eq!(report.results.len(), 1);
assert_eq!(
report.results[0].memory.agent_profile.as_deref(),
Some("researcher")
);
}
}
Loading
Loading