Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
782f1d9
refactor(memory): complete tinycortex engine migration
senamakel Jul 12, 2026
b388046
build(memory): unify vendored tinyagents source
senamakel Jul 12, 2026
db76303
fix(embeddings): preserve provider contracts
senamakel Jul 12, 2026
a8acb19
fix(memory): preserve sync credentials and state
senamakel Jul 12, 2026
cf0baab
fix(memory): preserve source integration parity
senamakel Jul 12, 2026
67b1b84
fix(memory): keep recency reads provider-free
senamakel Jul 12, 2026
ad37001
chore(vendor): update tinycortex conflict resolution
senamakel Jul 12, 2026
2fc300d
Merge upstream/main into feat/tinycortex-crate-ports
senamakel Jul 12, 2026
e03b9f3
chore(vendor): update tinycortex review fixes
senamakel Jul 12, 2026
0dd7e33
fix(memory): preserve sync audit and lazy drill-down
senamakel Jul 12, 2026
7b7786e
chore(vendor): update tinycortex clippy fixes
senamakel Jul 12, 2026
de75d93
docs(memory): update tinycortex ownership links
senamakel Jul 12, 2026
9d6fadb
docs(memory): use submodule-safe ownership paths
senamakel Jul 12, 2026
4a42fa7
chore(vendor): track merged tinycortex main
senamakel Jul 12, 2026
92645ab
fix(memory): align tinycortex persistence adapters
senamakel Jul 12, 2026
ed988cd
fix(memory): align backfill and namespace semantics
senamakel Jul 12, 2026
dde408f
fix(memory): preserve kv namespace safety checks
senamakel Jul 12, 2026
4abe952
fix(embeddings): preserve adapter rpc contracts
senamakel Jul 12, 2026
e797300
test(memory): await deferred tree sealing
senamakel Jul 12, 2026
88c4bc8
test(memory): await deferred chunk embeddings
senamakel Jul 12, 2026
751500c
test(embeddings): align adapter raw coverage
senamakel Jul 12, 2026
1511318
test(memory): align persisted score coverage
senamakel Jul 12, 2026
f9439b9
fix(sync): forward provider request budgets
senamakel Jul 13, 2026
a64e623
test(sync): assert tinycortex notion outcome
senamakel Jul 13, 2026
e06a1d8
test(sync): match tinycortex notion page size
senamakel Jul 13, 2026
d944413
test(sync): assert tinycortex slack persistence
senamakel Jul 13, 2026
11500fb
test(sync): align tree fixtures with tinycortex
senamakel Jul 13, 2026
d427a27
test(memory): align tinycortex score and state contracts
senamakel Jul 13, 2026
caf05b6
test(memory): cover tinycortex recovery contracts
senamakel Jul 13, 2026
823142b
Merge remote-tracking branch 'upstream/main' into feat/tinycortex-cra…
senamakel Jul 13, 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
13 changes: 11 additions & 2 deletions Cargo.lock

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

8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ tinyagents = { version = "1.7", features = ["sqlite", "repl"] }
# security gating, and the global singleton stay host-side. rusqlite/git2 are
# aligned to the host pins (=0.40 / 0.21) so one bundled SQLite + one libgit2
# link. Keep the version pin in lockstep with the submodule tag.
tinycortex = "0.1"
tinycortex = { version = "0.1", features = ["git-diff", "sync"] }
tinychannels = { version = "0.1", features = ["relay-websocket"] }
# TokenJuice code compressor — AST-aware signature extraction. Optional (C build)
# behind the default `tokenjuice-treesitter` feature; disabling it falls back to
Expand Down Expand Up @@ -377,6 +377,12 @@ tinyjuice = { path = "vendor/tinyjuice" }
tinychannels = { path = "vendor/tinychannels" }
tinyplace = { path = "vendor/tinyplace/sdk/rust" }

# TinyCortex temporarily pins the embedding API port while tinyagents #58 is
# pending. Resolve that git source to the same vendored crate so host adapters
# and TinyCortex share one `EmbeddingModel` trait identity.
[patch."https://github.com/senamakel/tinyagents"]
tinyagents = { path = "vendor/tinyagents" }

# Emit just enough DWARF in release builds for Sentry to symbolicate Rust
# panics + render surrounding source lines. `line-tables-only` keeps the
# binary small (only file+line tables, no full type info) while still
Expand Down
13 changes: 11 additions & 2 deletions app/src-tauri/Cargo.lock

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

7 changes: 6 additions & 1 deletion app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ tinyagents = { path = "../../vendor/tinyagents" }
# TinyAgents so integration work can test crate changes against OpenHuman before
# publishing.
tinyflows = { path = "../../vendor/tinyflows" }
tinycortex = { path = "../../vendor/tinycortex" }
tinycortex = { path = "../../vendor/tinycortex", features = ["git-diff"] }
tinyjuice = { path = "../../vendor/tinyjuice" }
tinychannels = { path = "../../vendor/tinychannels" }
tinyplace = { path = "../../vendor/tinyplace/sdk/rust" }
Expand Down Expand Up @@ -248,6 +248,11 @@ tauri-plugin-global-shortcut = { git = "https://github.com/tauri-apps/plugins-wo
tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", rev = "c6561ab6b4f9e7f650d4fc8c53fd8acc9b65b9b2" }
tauri-plugin-notification = { path = "vendor/tauri-plugin-notification" }

# Match the root Cargo world: TinyCortex's temporary git pin must resolve to
# the vendored TinyAgents crate to preserve one embedding trait identity.
[patch."https://github.com/senamakel/tinyagents"]
tinyagents = { path = "../../vendor/tinyagents" }

[dev-dependencies]
# `test-util` enables `#[tokio::test(start_paused = true)]` for the
# idle-watchdog unit tests in `cdp/session.rs` (#1213).
Expand Down
185 changes: 46 additions & 139 deletions src/bin/gmail_backfill_3d.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
//! Backfill the last N days of Gmail into the memory-tree content store.
//!
//! Authenticates via Composio (JWT from `<workspace>/auth-profiles.json`),
//! fetches Gmail pages via `GMAIL_FETCH_EMAILS`, converts each thread into an
//! [`EmailThread`], ingests it through `ingest_page_into_memory_tree` (which
//! writes `.md` files via `content_store` and populates SQLite), then drains
//! the async worker pool until idle.
//! fetches and ingests Gmail pages through tinycortex, then drains the async
//! worker pool until idle.
//!
//! After draining, the binary performs an integrity check: for every chunk
//! that has a `content_path` in SQLite, it verifies the on-disk SHA-256
Expand All @@ -31,15 +29,6 @@

use anyhow::{Context, Result};
use clap::Parser;
use serde_json::{json, Value};

use openhuman_core::openhuman::composio::client::{
create_composio_client, direct_execute, ComposioClientKind,
};
use openhuman_core::openhuman::composio::providers::gmail::ingest::ingest_page_into_memory_tree;
use openhuman_core::openhuman::composio::providers::registry::{
get_provider, init_default_providers,
};
use openhuman_core::openhuman::config::Config;
use openhuman_core::openhuman::memory_queue::drain_until_idle;
use openhuman_core::openhuman::memory_store::chunks::store::{
Expand All @@ -55,6 +44,10 @@ use openhuman_core::openhuman::memory_store::content::read::{
about = "Backfill last N days of Gmail into the memory-tree content store (.md files + SQLite)."
)]
struct Cli {
/// Composio Gmail connection id. Defaults to the configured Gmail source.
#[arg(long)]
connection_id: Option<String>,

/// Lookback window in days. Default 3.
#[arg(long, default_value_t = 3)]
days: u32,
Expand Down Expand Up @@ -114,6 +107,9 @@ async fn main() -> Result<()> {
if cli.days == 0 {
anyhow::bail!("--days must be >= 1");
}
if cli.owner.is_some() {
log::warn!("[gmail_backfill_3d] --owner is retained for compatibility but ignored");
}

let config = Config::load_or_init()
.await
Expand All @@ -123,29 +119,8 @@ async fn main() -> Result<()> {
wipe_memory_tree_state(&config)?;
}

// Resolve through the mode-aware factory so the backfill runs in
// EITHER backend mode (legacy JWT-driven path) OR direct mode (BYO
// Composio API key on the user's personal tenant) — #1710 Wave 2.
// Pre-fix this binary was hard-wired to backend mode via
// `build_composio_client`, so a direct-mode user couldn't run a
// gmail backfill even with a healthy personal connection.
let client_kind = create_composio_client(&config).map_err(|e| {
anyhow::anyhow!(
"No Composio client — user not signed in (backend session) and no direct-mode \
API key configured. Sign in via the desktop app or set a Composio API key, \
then re-run this binary. ({e})"
)
})?;

init_default_providers();
let provider = get_provider("gmail").ok_or_else(|| {
anyhow::anyhow!("GmailProvider not registered after init_default_providers")
})?;

let owner = cli
.owner
.clone()
.unwrap_or_else(|| "gmail-backfill".to_string());
openhuman_core::openhuman::memory::global::init(config.workspace_dir.clone())
.map_err(anyhow::Error::msg)?;

let mut query = format!("in:inbox newer_than:{}d", cli.days);
if !cli.include_spam_trash {
Expand Down Expand Up @@ -175,94 +150,42 @@ async fn main() -> Result<()> {
content_root.display()
);

// ─── Fetch + ingest ────────────────────────────────────────────────────
// ─── Fetch + ingest through tinycortex ──────────────────────────────────

let mut page_token: Option<String> = None;
let mut total_chunks = 0usize;
let mut total_pages = 0usize;
let mut total_cost: f64 = 0.0;

for page_num in 0..cli.max_pages {
let mut args = json!({
"max_results": cli.page_size,
"query": query,
});
if cli.include_spam_trash {
args["include_spam_trash"] = json!(true);
}
if let Some(token) = &page_token {
args["page_token"] = json!(token);
}

log::info!(
"[gmail_backfill_3d] fetching page {}{}…",
page_num,
page_token.as_ref().map(|_| " (paginated)").unwrap_or(""),
);

let mut resp = match &client_kind {
ComposioClientKind::Backend(client) => client
.execute_tool("GMAIL_FETCH_EMAILS", Some(args.clone()))
.await
.map_err(|e| anyhow::anyhow!("GMAIL_FETCH_EMAILS page {page_num}: {e:#}"))?,
ComposioClientKind::Direct(direct) => direct_execute(
direct,
"GMAIL_FETCH_EMAILS",
Some(args.clone()),
&config.composio.entity_id,
None,
let connection_id = cli
.connection_id
.as_deref()
.map(str::trim)
.filter(|id| !id.is_empty())
.map(str::to_owned)
.or_else(|| {
config.memory_sources.iter().find_map(|source| {
(source.kind == openhuman_core::openhuman::memory_sources::SourceKind::Composio
&& source.toolkit.as_deref() == Some("gmail"))
.then(|| source.connection_id.clone())
.flatten()
})
})
.ok_or_else(|| {
anyhow::anyhow!(
"no Gmail connection configured; pass --connection-id or add a Gmail memory source"
)
.await
.map_err(|e| anyhow::anyhow!("GMAIL_FETCH_EMAILS (direct) page {page_num}: {e:#}"))?,
};
total_cost += resp.cost_usd;

if !resp.successful {
anyhow::bail!(
"GMAIL_FETCH_EMAILS page {page_num} failed: {:?}",
resp.error
);
}

provider.post_process_action_result("GMAIL_FETCH_EMAILS", Some(&args), &mut resp.data);

let (messages, next_token) = extract_envelope(&resp.data);
log::info!(
"[gmail_backfill_3d] page {} -> {} messages, next_token={}",
page_num,
messages.len(),
next_token.as_deref().unwrap_or("(none)"),
);

if messages.is_empty() {
break;
}

// CLI runs don't fetch the user profile, so pass `None` and
// let the ingest fall back to per-participants source ids.
let chunks_this_page =
ingest_page_into_memory_tree(&config, &owner, None, &messages).await?;
total_chunks += chunks_this_page;
total_pages += 1;

log::info!(
"[gmail_backfill_3d] page {} ingested chunks={} running_total={}",
page_num,
chunks_this_page,
total_chunks,
);

match next_token {
Some(tok) => page_token = Some(tok),
None => break,
}
}

})?;
let outcome = openhuman_core::openhuman::tinycortex::run_gmail_backfill(
&connection_id,
&query,
cli.max_pages as usize,
cli.page_size as usize,
&config,
)
Comment on lines +164 to +170

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Honor --include-spam-trash in Gmail backfills

When gmail-backfill-3d --include-spam-trash is used, the new path only omits the -in:spam -in:trash query filter and then calls run_gmail_backfill, which has no way to pass the Gmail/Composio include_spam_trash flag. The previous implementation set args["include_spam_trash"] = true; without that boolean, Gmail fetches still use the default exclusion, so this CLI option no longer includes Spam/Trash despite reporting that it will.

Useful? React with 👍 / 👎.

.await
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
log::info!(
"[gmail_backfill_3d] fetch+ingest done pages={} total_chunks={} cost=~${:.4}",
total_pages,
total_chunks,
total_cost,
"[gmail_backfill_3d] fetch+ingest done records={} actions={} cost_usd={:.4} note={:?}",
outcome.records_ingested,
outcome.actions_called,
outcome.provider_cost_usd,
outcome.note,
);

// ─── Drain async worker pool ────────────────────────────────────────────
Expand Down Expand Up @@ -317,8 +240,8 @@ async fn main() -> Result<()> {
}

println!(
"\nBackfill complete. pages={} chunks_written={} cost=~${:.4}",
total_pages, total_chunks, total_cost,
"\nBackfill complete. records_ingested={} actions={} cost=~${:.4}",
outcome.records_ingested, outcome.actions_called, outcome.provider_cost_usd,
);
Ok(())
}
Expand Down Expand Up @@ -479,19 +402,3 @@ fn verify_all_summary_files(config: &Config) -> Result<(usize, usize, usize, usi

Ok((verified, mismatched, no_pointer, missing_file))
}

/// Extract the `messages` array and `nextPageToken` from a Composio response.
fn extract_envelope(data: &Value) -> (Vec<Value>, Option<String>) {
let candidates: [Option<&Value>; 2] = [Some(data), data.get("data")];
for cand in candidates.into_iter().flatten() {
if let Some(arr) = cand.get("messages").and_then(|v| v.as_array()) {
let token = cand
.get("nextPageToken")
.and_then(|v| v.as_str())
.filter(|s| !s.trim().is_empty())
.map(str::to_string);
return (arr.clone(), token);
}
}
(Vec::new(), None)
}
Loading
Loading