Repository navigation
Generic Composio sync orchestrator so every toolkit enforces max_items and sync_depth_days #3333
Description
Summary
Move the per-toolkit memory-sync loop (paginate → filter → dedup → ingest → advance cursor) into one shared sync orchestrator, and shrink each provider to small primitives (fetch_page, ingest_item, item_timestamp/item_id). The orchestrator owns cap enforcement (max_items), the sync_depth_days window, pagination, and dedup — so every current and future toolkit gets these for free, with zero per-provider cap code.
Problem
Each Composio provider implements its entire sync() loop itself — ComposioProvider::sync (src/openhuman/memory_sync/composio/providers/traits.rs:55) — and these are 600–1000 lines each, all differently shaped:
gmail/provider.rsbatch-ingests a whole page;github/notion/linearingest per-item;clickup/provider.rsnests workspaces → pages → tasks;slack/provider.rs(1043 lines) nests channels → history → messages plus a separate user-list backfill.
Because the cap/window logic must live inside each loop, it is duplicated 6×. This directly caused two bugs (fixed in #3304): max_items was page-granular in every provider (a "max 10" pulled a whole page, ~25–50 items), and Slack had no precise cap at all — it was simply missed. ItemCap (providers/helpers.rs) centralized the math, but each provider still wires it by hand, so:
- A new toolkit (7th provider) must re-implement the cap/window wiring or it silently ignores the user's limits.
- Toolkits without a registered provider — only 6 are registered (
registry.rs:81-86); e.g. Excel / Google Calendar / Outlook connected via Composio — don't sync at all (run_connection_sync→ "no native memory sync provider registered for toolkit",composio/mod.rs:149), so the per-source limit UI is inert for them.
This is fragile by construction: correctness depends on every author remembering to wire caps into a bespoke 600–1000-line loop.
Solution (optional)
Invert control. Introduce a generic run_sync(provider, ctx, reason) orchestrator plus a slim primitive trait (e.g. IncrementalSource):
- Provider supplies: a preamble hook (resolve identity / list scopes like workspaces/channels),
fetch_page(scope, cursor) -> (items, next),item_dedup_key,item_sort_ts, an optional server-sidedepth_query_fragment, andingest(items). - Orchestrator owns: budget check → preamble → scope/page loop bounded by
ItemCap::max_pages→sync_depth_daysfilter → dedup →ItemCap::clamp_batch→ ingest →record→ cursor advance →is_reached()stop → save state → buildSyncOutcome. - A scopes abstraction handles flat (gmail/github/notion/linear) and nested (clickup workspaces, slack channels) uniformly; Slack's user-list backfill becomes a preamble hook.
- Migrate incrementally: land the orchestrator alongside the existing
sync(), convert one clean per-item provider first (notion or github) and keep its e2e green, convert the rest one commit at a time, then delete the per-provider cap code.
Alternative (rejected as primary): keep ItemCap per-provider and add a lint/test gate that fails if a provider does not route caps through it. Cheaper, but still leaves 6 bespoke loops and does nothing for unregistered toolkits.
Acceptance criteria
- Generic orchestrator — one
run_syncowns pagination +max_items+sync_depth_days+ dedup + cursor for all toolkits. - Slim providers — each provider implements only fetch/ingest primitives; no per-provider cap/window/pagination code remains.
- All 6 converted — gmail/github/notion/linear/clickup/slack run through the orchestrator with behavioral parity (cursor, dedup, budget, thread/account handling).
- Caps proven generically — e2e:
max_items=Ncaps to exactly N andsync_depth_daysfilters, asserted via the shared orchestrator path so a new toolkit inherits both. - New-toolkit ergonomics — adding a toolkit needs only the primitive methods to get caps + window.
- Diff coverage ≥ 80% — the implementing PR meets the changed-lines coverage gate (Vitest + cargo-llvm-cov).
Related
- PR feat(memory-sources): default sync sources on with conservative caps, per-source settings & All In #3304 — default caps + per-source settings UI +
ItemCapcentralization (the stopgap this supersedes). - Sources:
providers/traits.rs,providers/helpers.rs(ItemCap),providers/registry.rs,composio/mod.rs(run_connection_sync), and the sixproviders/{gmail,github,notion,linear,clickup,slack}/provider.rs.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
refactor(memory_sync): generic Composio sync orchestrator + convert N…
refactor(memory_sync): convert GitHub + Linear to the generic sync or…
refactor(memory_sync): convert ClickUp to the generic sync orchestrat…
sanil-23 commentedon Jun 22, 2026
Migration status
Landed (PRs, stacked in order — merge oldest first):
run_syncorchestrator +IncrementalSourcetrait + convert notionserver_side_depthhook)page_ceiling+stop_on_empty_pendinghooks)That's 5 of 6 providers on the orchestrator, each a behavior-preserving PR with its existing cap/window e2e green on the shared path.
Pending (sub-issues):
conversations.historyloop; needs per-scope cursors + per-scope error tolerance). TheSLACK_SEARCH_MESSAGESbackfill is a manual CLI tool, out of scope.refactor(memory_sync): convert Gmail to the generic sync orchestrator…
refactor(memory_sync): convert Gmail to the generic sync orchestrator…
refactor(memory_sync): convert Gmail to the generic sync orchestrator…
refactor(memory_sync): convert Gmail to the generic sync orchestrator…
refactor(memory_sync): internalize the cap math into the orchestrator
refactor(memory_sync): generic Composio sync orchestrator + convert N…
refactor(memory_sync): convert GitHub + Linear to the generic sync or…
refactor(memory_sync): convert ClickUp to the generic sync orchestrat…
refactor(memory_sync): convert Gmail to the generic sync orchestrator…