Skip to content

feat(memory-sync): pause Composio periodic tick when Memory Tree is toggled off (#2719 follow-up)#2825

Merged
sanil-23 merged 5 commits into
tinyhumansai:mainfrom
justinhsu1477:fix/composio-periodic-pause-on-memory-toggle
May 28, 2026
Merged

feat(memory-sync): pause Composio periodic tick when Memory Tree is toggled off (#2719 follow-up)#2825
sanil-23 merged 5 commits into
tinyhumansai:mainfrom
justinhsu1477:fix/composio-periodic-pause-on-memory-toggle

Conversation

@justinhsu1477
Copy link
Copy Markdown
Contributor

@justinhsu1477 justinhsu1477 commented May 28, 2026

Closes the named follow-up listed in #2719's PR body:

Pause the 20-min Composio fetch loop on toggle (`memory_sync/composio/periodic.rs` needs a `Notify` signal).

Before this change, flipping the Memory Tree toggle off (via the Settings panel #2719 added) only paused LLM-bound workers via the scheduler gate; the periodic Composio fetch loop kept ticking every 20 minutes, calling `list_connections` and walking every active provider regardless. That silently burned API budget and contradicted the user's "stop background work" intent.

Approach — gate-check, not Notify

Rather than adding a separate `Notify` channel and wiring it through `scheduler_gate::update_config`, this PR routes the pause signal through the existing `scheduler_gate::current_policy()` surface that the rest of the memory subsystem already consults:

`scheduler_gate` state `current_policy()` This tick?
`SchedulerGateMode::Off` (toggle off) `Policy::Paused { UserDisabled }` ❌ skipped
No live session `Policy::Paused { SignedOut }` ❌ skipped
Battery / CPU pressure `Policy::Paused { LowPower / HighCpu }` ✅ proceeds (network-light)
Normal `Policy::Normal` ✅ proceeds

A new `periodic_pause_reason()` helper checks the relevant pause reasons and is called at the top of `run_one_tick` — before config load and auth-client build — so a paused session never even resolves the API token.

Battery / CPU pause is intentionally not treated as a tick-blocker here; periodic Composio fetch is network-light enough that those signals shouldn't gate it. They already throttle LLM-bound work through the regular gate.

Trade-off vs Notify

YOMXXX's PR body suggested a `Notify` signal. The gate-check approach trades instant wake-up (which `Notify` would give) for ~20 min worst-case resume latency: when the user toggles back on, the next tick fires normally. The simplicity is worth it because:

  • The whole point is "stop doing work" — a tick that fires every 20 min but immediately returns `Ok(())` is functionally equivalent to "loop paused" from an API-budget / battery perspective.
  • `current_policy()` is already the canonical "should the memory subsystem do background work?" signal used by other workers; the periodic loop honouring it stays consistent.
  • Faster wake-up can be layered on later (separate PR) without changing the gate-check contract here.

Tests

8/8 pass in `memory_sync::composio::periodic::tests` (was 6, +2 new):

  • `periodic_pause_reason_returns_none_when_gate_not_initialised` — pins the OnceLock-uninitialised default (`Policy::Normal` → helper returns `None`), guarding against an "always pause" regression that would silently break every periodic-driven test.
  • `run_one_tick_does_not_short_circuit_when_not_paused` — runs the full tick under the standard `OPENHUMAN_WORKSPACE`-isolated env and confirms the new early-return arm doesn't fire spuriously.

Coverage for the paused arm is deferred to integration tests that own `scheduler_gate::init_global(...)` — `OnceLock` makes per-test paused-state isolation brittle in unit tests, and the paused branch itself is a trivial match on `Policy::Paused`.

  • `cargo test --lib memory_sync::composio::periodic` — 8/8 pass.
  • `cargo check --lib` — clean.
  • `cargo fmt --check` — clean.
  • `cargo test --tests --no-run` — clean (integration-test target compiles).

Refs #2719, #1856 (Part 1).

Summary by CodeRabbit

  • New Features

    • Periodic memory synchronization now respects pause states and skips sync operations when the user is disabled or signed out; emits concise logs when pausing or resuming.
  • Tests

    • Added tests verifying pause-awareness, correct no-op behavior when the gate is uninitialized, and continued tick behavior without an active client.

Review Change Stack

…oggled off (tinyhumansai#2719 follow-up)

Closes the named follow-up listed in tinyhumansai#2719's PR body:

> Pause the 20-min Composio fetch loop on toggle
> (`memory_sync/composio/periodic.rs` needs a `Notify` signal).

Before this change, flipping the Memory Tree toggle off (via the Settings
panel tinyhumansai#2719 added) only paused LLM-bound workers via the scheduler gate;
the periodic Composio fetch loop kept ticking every 20 minutes, calling
`list_connections` and walking every active provider regardless. That
silently burned API budget and contradicted the user's "stop background
work" intent.

## Approach

Rather than adding a separate `Notify` channel and wiring it through
`scheduler_gate::update_config`, this PR routes the pause signal through
the existing `scheduler_gate::current_policy()` surface that the rest of
the memory subsystem already consults:

- `SchedulerGateMode::Off` → `Policy::Paused { UserDisabled }`
- No live session → `Policy::Paused { SignedOut }`

A new `periodic_pause_reason()` helper checks for both and is called at
the top of `run_one_tick` *before* config load / auth-client build, so a
paused session never even resolves the API token.

Other `PauseReason` variants (battery / CPU pressure) are intentionally
**not** treated as a global pause here — periodic Composio fetch is
network-light enough that those signals shouldn't gate it. They already
throttle LLM-bound work through the regular gate.

## Trade-off vs Notify

YOMXXX's PR body suggested a `Notify` signal. The gate-check approach
trades instant wake-up (which `Notify` would give) for ~20 min worst-case
resume latency: when the user toggles back on, the next tick fires
normally. The simplicity is worth it because:

- The whole point is "stop doing work" — a tick that fires every 20 min
  but immediately returns `Ok(())` is functionally equivalent to "loop
  paused" from an API-budget / battery perspective.
- The `current_policy()` surface is already the canonical "should the
  memory subsystem do background work?" signal used by other workers, so
  the periodic loop honouring it stays consistent with the rest of the
  system.
- Faster wake-up can be layered on later (separate PR) without changing
  the gate-check contract here.

## Tests

8/8 pass in `memory_sync::composio::periodic::tests` (was 6, +2 new):

- `periodic_pause_reason_returns_none_when_gate_not_initialised` — pins
  the OnceLock-uninitialised default (`Policy::Normal` → helper returns
  `None`), guarding against an "always pause" regression that would
  silently break every periodic-driven test.
- `run_one_tick_does_not_short_circuit_when_not_paused` — runs the full
  tick under the standard `OPENHUMAN_WORKSPACE`-isolated env and confirms
  the new early-return arm doesn't fire spuriously.

Coverage for the *paused* arm is deferred to integration tests that own
`scheduler_gate::init_global(...)` — `OnceLock` makes per-test paused-state
isolation brittle in unit tests, and the paused branch itself is a trivial
match on `Policy::Paused`.

`cargo check --lib`, `cargo fmt --check`, and
`cargo test --tests --no-run` all clean.
@justinhsu1477 justinhsu1477 requested a review from a team May 28, 2026 08:03
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 10619136-4c85-4086-b645-5a79496b0946

📥 Commits

Reviewing files that changed from the base of the PR and between 4a2dfb3 and 6ee23f7.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • src/openhuman/memory_sync/composio/periodic.rs

📝 Walkthrough

Walkthrough

The Composio periodic tick now queries scheduler-gate policy via periodic_pause_reason() and early-returns from run_one_tick() when paused for UserDisabled or SignedOut, emitting transition-aware logs. A unit test verifies the helper returns None when the gate is uninitialized.

Changes

Scheduler-gate pause awareness in periodic sync

Layer / File(s) Summary
Imports for scheduler-gate and pause types
src/openhuman/memory_sync/composio/periodic.rs
Adds imports to access current_policy and PauseReason used by the new helper and gating logic.
periodic_pause_reason() helper
src/openhuman/memory_sync/composio/periodic.rs
Implements periodic_pause_reason() mapping scheduler Policy::Paused pause reasons to Some(PauseReason) only for UserDisabled and SignedOut; otherwise returns None.
run_one_tick() early-return gating
src/openhuman/memory_sync/composio/periodic.rs
Calls periodic_pause_reason() at tick start; when paused, logs transition/steady-state messages using an AtomicBool and returns Ok(()) before config/client/provider work.
Unit test for uninitialized gate behavior
src/openhuman/memory_sync/composio/periodic.rs
Adds test asserting periodic_pause_reason() returns None when scheduler-gate global state is uninitialized to preserve existing test scenarios that run without a Composio session.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Suggested labels

working

Suggested reviewers

  • sanil-23

Poem

I twitch my whiskers near the clock,
A quiet hop to halt the knock.
When users leave or tokens fall,
The tick will pause — I mind the call.
Tests guard the gate, so syncs won’t mock. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: pausing the Composio periodic sync tick when Memory Tree is toggled off, and correctly references the related issue as a follow-up.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 28, 2026
oxoxDev
oxoxDev previously approved these changes May 28, 2026
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev left a comment

Choose a reason for hiding this comment

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

Author: @justinhsu1477 (CONTRIBUTOR — external contributor)

Clean small change. Gate-check placement is correct (runs before config load + auth-client build), current_policy() fallback semantics are right (Policy::Normal when STATE uninitialised), and the rationale for excluding OnBattery/CpuPressure from the periodic gate (network-light work) is sound.

Three minor nits + two questions — none blocking:

Nitpick 1 — src/openhuman/memory_sync/composio/periodic.rs:166-176 — helper can collapse via existing Policy::pause_reason()

The two explicit Policy::Paused { reason: X } arms reproduce what Policy::pause_reason() (policy.rs:70) already returns. Same semantics, half the lines, and the allow-list of "reasons that gate the periodic tick" becomes a clean matches!:

fn periodic_pause_reason() -> Option<PauseReason> {
    let reason = current_policy().pause_reason()?;
    matches!(reason, PauseReason::UserDisabled | PauseReason::SignedOut).then_some(reason)
}

Behaviour identical for the four enum variants today. Future PauseReason additions (e.g. OnBattery, CpuPressure per #1073) still won't gate periodic — same as today's wildcard _ => None. Tradeoff: loses the explicit Policy::Paused { reason: X } pattern which doubled as documentation. OK to keep current form if you prefer the explicitness.

Nitpick 2 — src/openhuman/memory_sync/composio/periodic.rs:444-462 — new "does-not-short-circuit" test is functionally identical to pre-existing run_one_tick_returns_ok_when_no_client

Both set up ENV_LOCK + tempdir + OPENHUMAN_WORKSPACE, call run_one_tick() under 5s timeout, assert Ok(_). With no client available, both exit at the create_composio_client early-return — neither actually proves the new gate-check didn't fire first. A regression that made the helper "always pause" would still make this test pass.

Either drop the duplicate, or strengthen — e.g. via tracing_test::traced_test + logs_contain to assert the "scheduler-gate paused — skipping tick" line was NOT emitted:

#[traced_test]
#[tokio::test]
async fn run_one_tick_does_not_short_circuit_when_not_paused() {
    // existing setup…
    let _ = run_one_tick().await;
    assert!(!logs_contain("scheduler-gate paused"));
}

Nitpick 3 — src/openhuman/memory_sync/composio/periodic.rs:155-161 — skip-tick log is debug! only

Fleet operators investigating "why is Composio not syncing?" won't see anything at info/warn levels. A single info!-once-per-policy-transition log when entering the paused arm would aid prod observability without spamming (debug fires every 20 min × every user). Optional — debug is defensible given per-LLM-call gating elsewhere is also debug.

Questions

  • periodic.rs:160 — Should the helper also gate on PauseReason::Unknown? Documented as "safe fallback" — current code lets the tick proceed when reason is Unknown. Worth a one-line rationale comment.
  • periodic.rs:153-161 — PR body mentions "Faster wake-up can be layered on later (separate PR) without changing the gate-check contract here." Has a follow-up issue been filed for the Notify wakeup path so the ~20-min worst-case resume doesn't get forgotten? Per [[feedback_deferred_classifier_arm_trap]] — defer-without-tracking burns events.

CI required checks all green. coderabbit APPROVED. APPROVE.

Five items raised in @oxoxDev's tinyhumansai#2825 review (all marked non-blocking,
APPROVE-d) — addressing the substantive ones:

* **Nitpick 1 (helper redundancy)** — collapsed the two explicit
  `Policy::Paused { reason: X }` match arms into a single
  `current_policy().pause_reason()` call + `matches!` allow-list. Same
  semantics, half the lines, future `PauseReason` variants stay
  explicitly opt-in via the `matches!` allow-list rather than the
  wildcard `_ => None` arm.

* **Nitpick 2 (test functionally identical)** — dropped
  `run_one_tick_does_not_short_circuit_when_not_paused`. As @oxoxDev
  noted, both it and the pre-existing
  `run_one_tick_returns_ok_when_no_client` exited at the same
  `create_composio_client` no-client branch — neither actually proved
  the new gate-check arm fired in the right direction. The
  helper-level `periodic_pause_reason_returns_none_when_gate_not_initialised`
  test already pins the wiring. Asserting log-line absence via
  `tracing-test` would prove the early-return logically but adds a
  new dev-dependency for one assertion.

* **Nitpick 3 (skip log only at `debug!`)** — added transition-edge
  `info!` logging. New `LAST_TICK_WAS_PAUSED: AtomicBool` tracks the
  previous tick's pause state; the log site emits `info!` exactly
  once when the loop crosses the boundary in either direction, and
  stays at `debug!` for the already-paused / already-running steady
  state. Fleet operators investigating "why is Composio not syncing?"
  now see a single breadcrumb at default log level without `info`
  spam every 20 min.

* **Question 1 (`PauseReason::Unknown` rationale)** — added explicit
  doc comment on `periodic_pause_reason()` explaining why `Unknown`
  is intentionally allowed through (`Unknown` is documented in
  `scheduler_gate::policy` as a transitional / not-yet-resolved
  fallback; letting the tick proceed keeps periodic sync running
  through brief transitions instead of pausing on stale unresolved
  state).

* **Question 2 (Notify wakeup follow-up)** — filed a dedicated
  tracking issue (tinyhumansai#2831) so the ~20 min worst-case resume latency
  doesn't get forgotten. Per the
  "defer-without-tracking burns events" feedback pattern.

Tests: 7/7 pass in `memory_sync::composio::periodic::tests` (was 8;
-1 from dropped duplicate, net same coverage on substantive paths).
`cargo check --lib`, `cargo fmt --check`, and
`cargo test --tests --no-run` all clean.
@justinhsu1477 justinhsu1477 dismissed stale reviews from oxoxDev and coderabbitai[bot] via 4a2dfb3 May 28, 2026 08:43
@justinhsu1477
Copy link
Copy Markdown
Contributor Author

@oxoxDev thanks for the thorough pass — all five addressed in `4a2dfb3e`:

# Your nit What I did
N1 helper redundancy ✅ Collapsed to `current_policy().pause_reason()?` + `matches!` allow-list. Future `PauseReason` variants stay explicitly opt-in instead of falling through a wildcard `_ => None`.
N2 test functionally identical ✅ Dropped the duplicate. You're right that both exited at the same `create_composio_client` no-client branch — neither actually proved the gate-check fired in the right direction. The helper-level `periodic_pause_reason_returns_none_when_gate_not_initialised` test still pins the wiring. Took your second option (drop) over `tracing-test` since adding a new dev-dep for one assertion felt heavy.
N3 skip log only at `debug!` ✅ Added `LAST_TICK_WAS_PAUSED: AtomicBool` transition tracker. `info!` fires exactly once when the loop crosses the boundary in either direction; steady state stays at `debug!` so we don't spam `info` every 20 min. Fleet operators investigating "why is Composio not syncing?" now see a single breadcrumb at default log level.
Q1 `PauseReason::Unknown` ✅ Added explicit rationale in the helper docstring — `Unknown` is documented as a transitional / not-yet-resolved fallback in `scheduler_gate::policy`, so letting the tick proceed keeps periodic sync running through brief transitions instead of pausing on stale unresolved state.
Q2 Notify wakeup follow-up tracking ✅ Filed #2831 with acceptance criteria covering rapid-toggle debounce, sign-in transition, and tokio `pause()` clock testing. "Defer-without-tracking burns events" — explicitly cited in the issue body.

Tests: 7/7 pass (was 8 with the duplicate dropped; same coverage on substantive paths). `cargo check --lib` + `cargo fmt --check` + `cargo test --tests --no-run` clean.

@coderabbitai coderabbitai Bot added rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. memory Memory store, memory tree, recall, summarization, and embeddings in src/openhuman/memory/. labels May 28, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 28, 2026
graycyrus
graycyrus previously approved these changes May 28, 2026
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Clean follow-up to #2719. Gate-check placement is correct — fires at the top of run_one_tick before config load and auth-client build, so a paused session never resolves the API token. The transition logging design (info once on boundary crossing, debug on steady state) is the right call for fleet observability without spam.

The AtomicBool with Relaxed ordering is sound given the singleton scheduler loop — no cross-thread visibility concern. The Unknown pause-reason passthrough rationale in the doc comment is clear and defensible (avoid pausing on transient unresolved state).

One minor thing to track: the PR body acknowledges ~20 min worst-case resume latency if someone wants faster Notify-based wakeup, but I don't see a follow-up issue filed for that. Not blocking — the gate-check approach fully satisfies the stated goal — but worth filing so the tradeoff doesn't get forgotten.

Approved.

@sanil-23 sanil-23 self-assigned this May 28, 2026
`Policy` was imported alongside `PauseReason` but only `PauseReason` is
used directly — `periodic_pause_reason()` delegates the `Policy::Paused`
destructure to `current_policy().pause_reason()`. Remove the dead import
to silence the `unused_import` compiler warning.

Co-Authored-By: Claude <noreply@anthropic.com>
@sanil-23 sanil-23 dismissed stale reviews from graycyrus and coderabbitai[bot] via 6ee23f7 May 28, 2026 13:07
@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 28, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

Copy link
Copy Markdown
Contributor

@sanil-23 sanil-23 left a comment

Choose a reason for hiding this comment

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

Round-3 shepherd review: implementation looks good; CI signal (current or expected after re-run) clean. Approving on behalf of sanil-23.

@sanil-23 sanil-23 merged commit 9e0b889 into tinyhumansai:main May 28, 2026
34 of 35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

memory Memory store, memory tree, recall, summarization, and embeddings in src/openhuman/memory/. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants