-
Notifications
You must be signed in to change notification settings - Fork 31
fix(ingest): persist staged content pointers #67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
227f9b9
2b68447
5dd9b8d
671e78a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -98,12 +98,15 @@ impl KvStore { | |
| /// characters collapse to `_` so `"team alpha/#1"` and `"team_alpha/_1"` | ||
| /// address the same bucket. | ||
| fn sanitize_namespace(namespace: &str) -> String { | ||
| namespace | ||
| let trimmed = namespace.trim(); | ||
| if trimmed.is_empty() { | ||
| return crate::memory::types::GLOBAL_NAMESPACE.to_string(); | ||
| } | ||
| trimmed | ||
| .chars() | ||
| .map(|c| match c { | ||
| c if c.is_whitespace() => '_', | ||
| '#' => '_', | ||
| other => other, | ||
| c if c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '/') => c, | ||
| _ => '_', | ||
|
Comment on lines
+108
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a workspace already has KV rows in namespaces containing characters that the old sanitizer preserved, such as Useful? React with 👍 / 👎. |
||
| }) | ||
| .collect() | ||
| } | ||
|
Comment on lines
+101
to
112
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check whether kv_namespace rows persist across app restarts/deployments
# and whether any migration/backfill exists for previously-sanitized namespace values.
rg -n "kv_namespace" --type=rust -C3
rg -n "migration|backfill" src/memory/store -C3Repository: tinyhumansai/tinycortex Length of output: 161 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Map the relevant files and inspect namespace handling.
git ls-files 'src/memory/store/*' 'src/memory/types*' | sed -n '1,200p'
printf '\n--- kv.rs outline ---\n'
ast-grep outline src/memory/store/kv.rs --view expanded || true
printf '\n--- kv.rs relevant lines ---\n'
nl -ba src/memory/store/kv.rs | sed -n '1,240p'
printf '\n--- search for namespace persistence / storage backend ---\n'
rg -n "struct .*Namespace|kv_namespace|sanitize_namespace|get_namespace|set_namespace|delete_namespace|list_namespace|records_namespace|GLOBAL_NAMESPACE" src -C 3Repository: tinyhumansai/tinycortex Length of output: 2895 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- src/memory/store/kv.rs (selected lines) ---'
sed -n '1,380p' src/memory/store/kv.rs
printf '\n%s\n' '--- src/memory/store/kv_tests.rs ---'
sed -n '1,260p' src/memory/store/kv_tests.rs
printf '\n%s\n' '--- src/memory/store/mod.rs ---'
sed -n '1,220p' src/memory/store/mod.rs
printf '\n%s\n' '--- src/memory/store/store.rs ---'
sed -n '1,220p' src/memory/store/store.rsRepository: tinyhumansai/tinycortex Length of output: 26062 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Look for any compatibility layer or migration that rewrites kv_namespace rows.
rg -n "sanitize_namespace|kv_namespace|backfill|migration|compat|legacy namespace|namespace.*old" src -C 3
# Check whether the KV store is documented as durable / file-backed.
rg -n "open\\(|open_in_memory|SQLite-backed|persist|durable|db_path" src/memory/store/kv.rs src/memory/store/mod.rs src/memory/store/store.rs -C 3Repository: tinyhumansai/tinycortex Length of output: 50379 Namespace sanitization needs a migration path.
🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,13 +221,24 @@ impl ComposioClient { | |
| let _ = response.bytes().await; | ||
| anyhow::bail!("Composio proxy request failed with HTTP {status}"); | ||
| } | ||
| response | ||
| let raw: serde_json::Value = response | ||
| .json() | ||
| .await | ||
| .map_err(|error| anyhow::anyhow!("Composio proxy response decode failed: {error}")) | ||
| .map_err(|error| anyhow::anyhow!("Composio proxy response decode failed: {error}"))?; | ||
| decode_proxy_response(raw) | ||
| } | ||
| } | ||
|
|
||
| fn decode_proxy_response(raw: serde_json::Value) -> anyhow::Result<ExecuteResponse> { | ||
| let payload = if raw.get("successful").is_some() { | ||
| raw | ||
| } else { | ||
| raw.get("data").cloned().unwrap_or(raw) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the proxy uses the backend envelope, this branch replaces the entire response with Useful? React with 👍 / 👎. |
||
| }; | ||
| serde_json::from_value(payload) | ||
| .map_err(|error| anyhow::anyhow!("Composio proxy response decode failed: {error}")) | ||
| } | ||
|
|
||
| fn retryable_provider_error(error: Option<&str>) -> bool { | ||
| error.is_some_and(|error| { | ||
| let lower = error.to_ascii_lowercase(); | ||
|
|
@@ -259,3 +270,36 @@ async fn decode_response( | |
| .await | ||
| .map_err(|error| anyhow::anyhow!("Composio {mode} response decode failed: {error}")) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn proxied_backend_envelope_decodes_provider_response() { | ||
| let response = decode_proxy_response(serde_json::json!({ | ||
| "success": true, | ||
| "data": { | ||
| "successful": true, | ||
| "data": {"messages": [{"messageId": "message-1"}]}, | ||
| "error": null | ||
| } | ||
| })) | ||
| .unwrap(); | ||
|
|
||
| assert!(response.successful); | ||
| assert_eq!(response.data["messages"][0]["messageId"], "message-1"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn flat_proxy_response_remains_supported() { | ||
| let response = decode_proxy_response(serde_json::json!({ | ||
| "successful": true, | ||
| "data": {"items": [1]} | ||
| })) | ||
| .unwrap(); | ||
|
|
||
| assert!(response.successful); | ||
| assert_eq!(response.data["items"], serde_json::json!([1])); | ||
| } | ||
| } | ||
|
Comment on lines
+273
to
+305
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Move tests to New tests are added inline in As per path instructions, "Keep tests in per-file 🤖 Prompt for AI AgentsSource: Path instructions |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,9 @@ pub trait IncrementalSource: Send + Sync { | |
| fn retain_dedup_keys(&self) -> bool { | ||
| true | ||
| } | ||
| fn stop_on_empty_pending(&self) -> bool { | ||
| false | ||
| } | ||
| fn server_side_depth(&self) -> bool { | ||
| false | ||
| } | ||
|
|
@@ -271,6 +274,7 @@ async fn run_pages( | |
|
|
||
| let fetched = source.extract_page(&response.data, page_token.as_deref()); | ||
| let mut reached_cursor_boundary = false; | ||
| let mut saw_unsynced_item = false; | ||
| for raw in fetched.items { | ||
| if config | ||
| .sync | ||
|
|
@@ -291,6 +295,7 @@ async fn run_pages( | |
| if state.is_synced(&dedup_key) { | ||
| continue; | ||
| } | ||
| saw_unsynced_item = true; | ||
| let sort_cursor = source.sort_cursor(&raw); | ||
| if sort_cursor | ||
| .as_deref() | ||
|
|
@@ -376,6 +381,15 @@ async fn run_pages( | |
| } | ||
|
|
||
| page_token = fetched.next; | ||
| if source.stop_on_empty_pending() && !saw_unsynced_item { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Gmail has more results than one capped run can process, Useful? React with 👍 / 👎. |
||
| tracing::debug!( | ||
| toolkit = source.toolkit(), | ||
| connection_id, | ||
| scope = %scope.label, | ||
| "[sync:orchestrator] stopping after all-deduplicated page" | ||
| ); | ||
| break; | ||
| } | ||
| if reached_cursor_boundary { | ||
| break; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this switches ingest to
upsert_staged_chunks_tx, the SQLitecontentcolumn is no longer the full chunk; that helper stores only the first 500 chars as a preview. L0 sealing still hydrates leaves viahydrate_leaf_inputsinsrc/memory/tree/hydrate.rs, which callsget_chunkand feedschunk.contentdirectly to the summarizer, so any ingested document/chat chunk longer than 500 chars is summarized from a truncated preview; plainingest_emailalso has no raw refs forread_chunk_bodyto recover the skipped email body. Please either hydrate seal inputs from the content store/raw archive or avoid staged previews until those consumers are switched.Useful? React with 👍 / 👎.