Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
4fa0df9
Merge pull request #46 from sopaco/v2
sopaco Feb 27, 2026
452ea39
Merge pull request #47 from sopaco/v2
sopaco Feb 27, 2026
627d7e3
Merge pull request #48 from sopaco/v2.5
sopaco Mar 5, 2026
980e3a4
Merge pull request #49 from sopaco/v2.5
sopaco Mar 6, 2026
c322707
Merge pull request #50 from sopaco/v2.5
sopaco Mar 8, 2026
377597a
Merge pull request #51 from sopaco/v2.5
sopaco Mar 11, 2026
530e538
Merge pull request #52 from sopaco/dev
sopaco Mar 12, 2026
8bcc606
Merge pull request #53 from sopaco/dev
sopaco Mar 12, 2026
762cba4
Merge pull request #54 from sopaco/dev
sopaco Mar 13, 2026
0088476
Merge pull request #55 from sopaco/dev
sopaco Mar 13, 2026
79a0ca8
Merge pull request #56 from sopaco/dev
sopaco Mar 14, 2026
4d762b3
Merge pull request #57 from sopaco/dev
sopaco Mar 16, 2026
56d0b3d
Merge pull request #58 from sopaco/dev
sopaco Mar 16, 2026
4e1bc22
Merge pull request #59 from sopaco/dev
sopaco Mar 18, 2026
bb1fde5
Merge pull request #60 from sopaco/dev
sopaco Mar 20, 2026
84cae77
Merge pull request #61 from sopaco/dev
sopaco Mar 21, 2026
0a19ade
Merge pull request #62 from sopaco/dev
sopaco Mar 31, 2026
f6cef9d
Merge pull request #63 from sopaco/dev
sopaco Mar 31, 2026
f0b99d3
Merge pull request #64 from sopaco/dev
sopaco Apr 1, 2026
eca1fec
Merge pull request #65 from sopaco/dev
sopaco Apr 1, 2026
a10ab72
Merge pull request #66 from sopaco/dev
sopaco Apr 2, 2026
3024b44
Merge pull request #67 from sopaco/dev
sopaco Apr 2, 2026
06b4d7d
Merge pull request #68 from sopaco/dev
sopaco Apr 2, 2026
1c95a04
Merge pull request #69 from sopaco/dev
sopaco Apr 2, 2026
54d8100
Merge pull request #70 from sopaco/dev
sopaco Apr 5, 2026
d4cc90c
Merge pull request #73 from sopaco/dev
sopaco Apr 14, 2026
0bbcdb1
Merge pull request #75 from sopaco/dev
sopaco Apr 25, 2026
5194c01
search: precompute intent once, reuse across all semantic_search calls
donsummerwind Apr 28, 2026
d10d40e
docs: add TweetClaw source memory example
kriptoburak May 24, 2026
a0c0659
Merge pull request #77 from kriptoburak/codex/add-tweetclaw-source-me…
sopaco Jul 7, 2026
e086989
Merge pull request #76 from donsummerwind/main
sopaco Jul 7, 2026
1abacd6
fix the missing field of precomputed_intent
Jul 22, 2026
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,15 @@ openclaw plugins install @memclaw/memclaw

## Documentation

For detailed configuration, troubleshooting, and best practices, see the [MemClaw README](examples/@memclaw/plugin/README.md).
For current maintenance status and repository details, see the [MemClaw project notice](examples/@memclaw/README.md).

---

# Source Memory Example: TweetClaw X/Twitter Signals

Cortex Memory can keep reviewed public X/Twitter research from TweetClaw as durable source memory without storing credentials, raw exports, or private account data. This is useful for OpenClaw agents that collect market, support, competitor, or community signals and need to recall why a decision was made later.

See [TweetClaw X/Twitter source memory](examples/tweetclaw-x-signal-memory.md) for setup, storage shape, and search examples.

---

Expand Down
1 change: 1 addition & 0 deletions cortex-mem-cli/src/commands/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub async fn execute(
threshold: min_score,
root_uri: Some(scope_uri.clone()),
recursive: true,
precomputed_intent: None,
};

// Perform layered vector search (L0/L1/L2 hierarchical search)
Expand Down
48 changes: 42 additions & 6 deletions cortex-mem-core/src/search/vector_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use tokio::sync::mpsc;
use tracing::{debug, info, warn};

/// Search options
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone)]
pub struct SearchOptions {
/// Maximum number of results
pub limit: usize,
Expand All @@ -27,6 +27,10 @@ pub struct SearchOptions {
pub root_uri: Option<String>,
/// Enable recursive search
pub recursive: bool,
/// Precomputed intent from LLM analysis.
/// If provided, semantic_search skips intent analysis and reuses this intent.
/// This reduces LLM calls from 5 per search → 1 per search.
pub precomputed_intent: Option<Arc<EnhancedQueryIntent>>,
}

impl Default for SearchOptions {
Expand All @@ -36,6 +40,7 @@ impl Default for SearchOptions {
threshold: 0.6,
root_uri: None,
recursive: true,
precomputed_intent: None,
}
}
}
Expand Down Expand Up @@ -262,13 +267,30 @@ impl VectorSearchEngine {
Some((scope, owner_id, memory_id))
}

/// Parse root_uri to extract (scope, owner_id) for filtering.
/// e.g. "cortex://session/wecom-alis" -> Some(("session", "wecom-alis"))
fn parse_root_uri(root_uri: &str) -> Option<(String, String)> {
let stripped = root_uri.strip_prefix("cortex://")?;
let parts: Vec<&str> = stripped.splitn(3, '/').collect();
if parts.len() < 2 {
return None;
}
Some((parts[0].to_string(), parts[1].to_string()))
}

/// Semantic search using vector similarity
pub async fn semantic_search(
&self,
query: &str,
options: &SearchOptions,
) -> Result<Vec<SearchResult>> {
let intent = self.analyze_intent(query).await?;
// Reuse precomputed intent if available (reduces LLM calls from 5 → 1 per search)
let intent = if let Some(ref precomputed) = options.precomputed_intent {
info!("semantic_search: reusing precomputed intent (type={:?})", precomputed.intent_type);
(**precomputed).clone()
} else {
self.analyze_intent(query).await?
};
let query_text = if intent.rewritten_query.trim().is_empty() {
query
} else {
Expand All @@ -279,7 +301,13 @@ impl VectorSearchEngine {

let mut filters = crate::types::Filters::default();
if let Some(scope) = &options.root_uri {
filters.uri_prefix = Some(scope.clone());
// Set owner_scope + uri_prefix so qdrant-level filtering uses exact scope
if let Some((owner_scope, _owner_id)) = Self::parse_root_uri(scope) {
filters.owner_scope = Some(owner_scope);
filters.uri_prefix = Some(scope.clone());
} else {
filters.uri_prefix = Some(scope.clone());
}
}

let scored = self
Expand Down Expand Up @@ -355,8 +383,13 @@ impl VectorSearchEngine {
query: &str,
options: &SearchOptions,
) -> Result<Vec<SearchResult>> {
// 1. LLM 统一意图分析(单次请求)
let intent = self.analyze_intent(query).await?;
// Reuse precomputed intent if available (reduces LLM calls from 5 → 1 per search)
let intent = if let Some(ref precomputed) = options.precomputed_intent {
info!("layered_semantic_search: reusing precomputed intent (type={:?})", precomputed.intent_type);
(**precomputed).clone()
} else {
self.analyze_intent(query).await?
};

info!(
"Intent analysis: type={:?}, entities={:?}, keywords={:?}, rewritten='{}'",
Expand All @@ -376,6 +409,9 @@ impl VectorSearchEngine {
);
let mut l0_filters = crate::types::Filters::with_layer("L0");
if let Some(scope) = &options.root_uri {
if let Some((owner_scope, _owner_id)) = Self::parse_root_uri(scope) {
l0_filters.owner_scope = Some(owner_scope);
}
l0_filters.uri_prefix = Some(scope.clone());
}

Expand Down Expand Up @@ -560,7 +596,7 @@ impl VectorSearchEngine {
}

/// 统一意图分析(优先使用 LLM 单次调用,LLM 不可用时使用最小 fallback)
async fn analyze_intent(&self, query: &str) -> Result<EnhancedQueryIntent> {
pub async fn analyze_intent(&self, query: &str) -> Result<EnhancedQueryIntent> {
if self.enable_intent_analysis {
if let Some(llm) = &self.llm_client {
match self.analyze_intent_with_llm(llm.as_ref(), query).await {
Expand Down
2 changes: 2 additions & 0 deletions cortex-mem-core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ pub struct Filters {
pub max_importance: Option<f32>,
/// URI prefix filter for scope-based searching
pub uri_prefix: Option<String>,
/// Owner scope hint: "session", "agent", or "user" (used with uri_prefix to construct qdrant filter)
pub owner_scope: Option<String>,
pub custom: HashMap<String, serde_json::Value>,
}

Expand Down
15 changes: 15 additions & 0 deletions cortex-mem-core/src/vector_store/qdrant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,21 @@ impl QdrantVectorStore {
fn filters_to_qdrant_filter(&self, filters: &Filters) -> Option<Filter> {
let mut conditions = Vec::new();

// Filter by scope + uri_prefix as a Match on the uri field
// This enables session/agent/user scope filtering at the qdrant level
if filters.owner_scope.is_some() && filters.uri_prefix.is_some() {
let uri_prefix = filters.uri_prefix.as_ref().unwrap();
conditions.push(Condition {
condition_one_of: Some(condition::ConditionOneOf::Field(FieldCondition {
key: "uri".to_string(),
r#match: Some(Match {
match_value: Some(r#match::MatchValue::Text(uri_prefix.clone())),
}),
..Default::default()
})),
});
}

if let Some(user_id) = &filters.user_id {
conditions.push(Condition {
condition_one_of: Some(condition::ConditionOneOf::Field(FieldCondition {
Expand Down
12 changes: 12 additions & 0 deletions cortex-mem-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM ubuntu:24.04

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

COPY target/release/cortex-mem-service /cortex-mem-service

ENV CORTEX_DATA_DIR=/mnt/sata-trace/cortex-mem/data
ENV CORTEX_TENANT_ID=tenant_claw

ENTRYPOINT ["/cortex-mem-service"]
1 change: 1 addition & 0 deletions cortex-mem-service/src/handlers/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ pub async fn explore(
threshold: 0.3, // Lower threshold for exploration
root_uri: Some(req.start_uri.clone()),
recursive: true,
precomputed_intent: None,
};

let search_results = vector_engine
Expand Down
9 changes: 9 additions & 0 deletions cortex-mem-service/src/handlers/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async fn search_layered(
threshold: min_score,
root_uri: None,
recursive: true,
precomputed_intent: None,
};
let mut semantic_options = options.clone();
semantic_options.threshold = (min_score * 0.5).max(0.0);
Expand All @@ -85,6 +86,14 @@ async fn search_layered(

let profile = build_query_profile(query);

// [优化] 一次性 intent 分析,后续 semantic_search 复用(5次LLM→1次)
let precomputed_intent = vector_engine.analyze_intent(query).await?;
let precomputed_intent = Arc::new(precomputed_intent);
options.precomputed_intent = Some(precomputed_intent.clone());
semantic_options.precomputed_intent = Some(precomputed_intent.clone());
tracing::info!("[search优化] intent precomputed: type={:?}, keywords={:?}",
precomputed_intent.intent_type, precomputed_intent.keywords);

let layered_results = vector_engine
.layered_semantic_search(query, &options)
.await
Expand Down
5 changes: 3 additions & 2 deletions cortex-mem-service/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use axum::{Router, routing::get};
use clap::Parser;
use std::fs::File;
use std::net::SocketAddr;
use std::net::{SocketAddr, IpAddr};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tower_http::cors::CorsLayer;
Expand Down Expand Up @@ -139,7 +139,8 @@ async fn main() -> anyhow::Result<()> {
.with_state(state);

// Start server
let addr = SocketAddr::from(([127, 0, 0, 1], cli.port));
let ip: IpAddr = cli.host.parse().unwrap_or(IpAddr::from([0, 0, 0, 0]));
let addr = SocketAddr::from((ip, cli.port));
info!("Server listening on http://{}", addr);

let listener = tokio::net::TcpListener::bind(addr).await?;
Expand Down
1 change: 1 addition & 0 deletions cortex-mem-tools/src/tools/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl MemoryOperations {
threshold: 0.5,
root_uri: args.scope.clone(),
recursive: args.recursive.unwrap_or(true),
precomputed_intent: None,
};

// Use layered semantic search for L0/L1/L2 tiered retrieval
Expand Down
72 changes: 72 additions & 0 deletions examples/tweetclaw-x-signal-memory.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# TweetClaw X/Twitter Source Memory

Use this recipe when an OpenClaw agent gathers public X/Twitter context with TweetClaw and stores only reviewed source notes in Cortex Memory.

## Install TweetClaw

```bash
openclaw plugins install @xquik/tweetclaw
openclaw plugins inspect tweetclaw --runtime
openclaw config set tools.alsoAllow '["explore", "tweetclaw"]'
openclaw config set plugins.entries.tweetclaw.config.apiKey "$XQUIK_API_KEY"
```

Keep the key in OpenClaw plugin config or environment-backed secret storage. Do not paste API keys, cookies, DMs, private account data, raw exports, or media files into Cortex memories.

## Collect And Review Sources

Start with `explore` to inspect the TweetClaw endpoint shape, then call `tweetclaw` only for the fields the agent needs. Typical source workflows include:

- Search tweets for a product, competitor, keyword, or campaign.
- Search tweet replies to understand objections, support themes, or follow-up questions.
- Look up public user context when the workflow needs a handle, display name, profile URL, or public metrics.
- Export follower context only when the account owner and workflow allow it.
- Read monitor or webhook events only for monitors the user configured.

Review the results before writing memory. Store conclusions and source references, not raw timelines.

## Store Reviewed Notes

Use one session thread per research run so Cortex Memory can later extract a compact L0/L1/L2 memory trail.

```bash
cortex-mem --config config.toml --tenant acme add \
--thread tweetclaw-research-2026-05-24 \
--role user \
"TweetClaw public X/Twitter search for query 'openclaw memory plugin' found recurring requests for examples that connect social signals to durable agent memory. Sources reviewed: tweet IDs 1234567890 and 2345678901. Decision: add a source-memory recipe and avoid storing raw exports."

cortex-mem --config config.toml --tenant acme session close tweetclaw-research-2026-05-24
```

A reviewed note should usually include:

- Query, endpoint, and capture date.
- Tweet IDs, URLs, author handles, or monitor event IDs.
- The decision, evidence summary, and next action.
- Confidence and any known gaps.

Use `cortex://resources/x-twitter/tweetclaw/...` paths for reusable source summaries and session memory for one-off investigations.

## Search Later

```bash
cortex-mem --config config.toml --tenant acme search \
"why did we add a TweetClaw source memory example?" \
--thread tweetclaw-research-2026-05-24 \
--scope session \
--limit 5
```

If the answer should outlive the run, promote the final summary into a shared resource note after review.

## Approval Boundaries

TweetClaw can support write workflows such as post tweets, post tweet replies, media upload, and direct messages. Do not store or replay pending write actions from memory without a fresh explicit user approval. Treat Cortex Memory as source context and decision history, not an action queue.

## Verification Checklist

- TweetClaw installs with `openclaw plugins install @xquik/tweetclaw`.
- `openclaw plugins inspect tweetclaw --runtime` reports the plugin runtime as loaded.
- Cortex Memory can add, close, and search the research session.
- Stored notes contain reviewed summaries and source references only.
- No API keys, cookies, DMs, raw exports, or private account material are stored.
Loading