Teaching My Dashboard to Think Across Modules
Life Hub had 239 MCP tools and 34 UI pages. Each module was smart in isolation, but they didn't talk to each other. Here's how a cross-module intelligence layer changed everything.
Last month I wrote about teaching Life Hub to think across categories: adding cross-category correlations, co-anomaly detection, and person-centric context. That work connected data domains that had been running independently: sleep affecting mood, spending correlating with stress, social energy predicting the next day's productivity.
But I'd been ignoring a bigger problem. The intelligence modules themselves were siloed.
The Problem: Smart Modules That Don't Talk
After a systematic product evaluation, Life Hub had:
- 239 MCP tools across 5 domains
- 34 UI pages with 134 HTMX interactive endpoints
- 21 data collectors pulling from external services
- A pillar scoring engine with 60+ sub-scores across 8 life pillars
- Granger causality tests running on 13 metric pairs
- Co-anomaly detection, mood forecasting, energy prediction, advisor triggers
Each module was genuinely intelligent in its own right. The Granger causality engine uses proper OLS regression with Gaussian elimination: no numpy, pure Python. The next-action system considers 12 different factors including recovery state, weather, and calendar load. The baselines system tracks rolling 30-day statistics with z-score utilities.
But when an advisor trigger fired "mood low for 3 consecutive days," the advisor had no idea why. The Granger tests knew that sleep causally affects mood with a 2-day lag. The co-anomaly detector knew that mood, sleep, and habits had all dropped simultaneously. An active experiment in the health pillar might explain the change entirely.
None of this context reached the trigger. The advisor gave generic advice because the intelligence was locked in separate modules.
CrossModuleContext: One Object, All Signals
The fix was a unified context assembly layer: not a cache, not a new database, just a Python dataclass that pulls from existing modules on demand:
@dataclass
class CrossModuleContext:
baselines: dict[str, Any] # Personal rolling stats
causal_findings: list[dict] # Granger test results
top_causal_finding: dict | None # Strongest relationship
co_anomalies: list[dict] # Multi-metric drops
mood_forecast: dict | None # 7-day prediction
energy_forecast: dict | None # Tomorrow's energy
pillar_scores: dict[str, float] # Current 0-10 scores
weakest_pillars: list[str] # Below 6.0
active_experiments: list[dict] # May affect interpretationThe key method is enrich_trigger(): it takes an advisor trigger dict and adds causal context:
def enrich_trigger(self, trigger: dict) -> dict:
# Map trigger types to metrics
metric = {"mood_drop": "mood_score", "sleep_decline": "sleep_hours", ...}
explanation = self.causal_explanation_for(metric)
if explanation:
trigger["causal_context"] = explanation
# Also check co-anomalies and experiments
return triggerNow when the mood trigger fires, the advisor commentary can reference actual causal data: "Your mood has been low for 3 days. Strong causal link: higher sleep_hours tends to increase mood_score with ~1-day lag." The LLM generating the commentary gets this context and produces specific, actionable advice instead of generic coaching.
Wiring It Into Everything
The context layer connects to 7 systems:
| System | What It Gets |
|---|---|
| Advisor triggers | Causal explanations in commentary + webhooks |
| Next actions | Reason enrichment ("habit_rate causally affects mood") |
| Mood forecast | Discovers its own causal predictors and reports them |
| Daily brief | Top causal finding + energy outlook for LLM context |
| Life Wheel page | Per-pillar causal drivers + experiment badges |
| Trends page | Granger pairs table + co-anomaly alert banner |
| Today page | 3-day energy outlook card |
The mood forecast integration was particularly satisfying. Previously, forecast_mood() used three signals: day-of-week seasonality, sleep correlation, and meeting correlation. Now it queries the cross-module context for significant Granger findings where mood is the effect variable, and reports them as named causal predictors:
for finding in xctx.causal_findings:
if finding["y"] == "mood_score" and finding["strength"] in ("strong", "moderate"):
causal_adjustments.append({
"predictor": finding["x"],
"lag": finding["best_lag"],
"direction": finding["direction"],
})The forecast output now includes a causal_predictors list and active_experiment_warnings: if you're running a health experiment, the forecast's confidence downgrades from "high" to "medium" because the experimental intervention may confound the prediction.
Performance: Cache Once, Use Everywhere
The context assembly queries baselines (30-day rolling stats), Granger causality (60-day scan), co-anomalies (14-day scan), and active experiments. That's expensive, and 6 different call sites were running it independently.
The fix: one cached wrapper with a 120-second TTL:
@ttl_cache(ttl=120, key_prefix="cross_module_context")
async def cached_cross_module_context():
async with get_session() as session:
return await build_cross_module_context(
session, include_forecasts=False, include_pillars=False,
)All 6 call sites (advisor triggers, next actions, predictions, daily brief, Life Wheel, Trends) now go through this single cached entry point. In practice, the context computes once when the first page loads, and every subsequent page within 2 minutes gets the cached result for free.
The TTL of 120 seconds works because the underlying data (causal findings, baselines) changes on a daily scale, not a per-request scale. Granger tests over 60-day windows don't shift meaningfully in 2 minutes.
Beyond Wiring: What Else Shipped
The cross-module work was the architectural centerpiece, but it enabled a cascade of smaller features that would have been awkward without it:
Thought → Action Pipeline. Every thought captured in the Open Brain now has two conversion buttons: "→ Action" creates an ActionItem with one click, "→ Goal" opens a dropdown of active goals and creates a milestone. Previously, thoughts were captured but never became anything.
Forward Energy Planner. A 3-day outlook card on the Today page that combines calendar workload (CalDAV meeting density), mood forecast, and energy prediction into a per-day strategy: push (high energy, low meetings: do hard things), protect (heavy meeting day: guard your focus), balanced, or recovery (low energy: scale back). The same outlook appears on the Calendar page.
Project Activity Feeds. The project detail page now shows a keyword-matched timeline of GitHub commits, time blocks, and journal entries related to the project. No manual tagging: project names are tokenized and matched against activity metadata.
Learning Pattern Analysis. Reading (Miniflux) and audiobook (Audiobookshelf) activity is analyzed for patterns: top feeds, top authors, reading streaks, time-of-day preferences, feed diversity. Available via MCP tool and eventually the Intellect pillar page.
Experiment-Aware Forecasts. The mood forecast now checks for active experiments targeting health, happiness, or connection pillars. If one exists, the forecast's confidence is downgraded and the experiment is flagged in the output. This prevents the system from treating an intentional intervention as anomalous behavior.
The Shift: From Data-Rich to Insight-Connected
The data didn't change. The collectors are the same 21 sources. The ORM models are the same 51 tables. The intelligence modules are the same Granger tests, baselines, anomaly detectors.
What changed is that every surface that shows intelligence now has access to context from every other module. The advisor doesn't just know your mood is low: it knows why (causal), how unusual (baselines), what else dropped (co-anomalies), and whether you're running an experiment (experiments). The forecast doesn't just extrapolate: it discovers its own predictors and flags when its assumptions might be wrong.
The implementation is 250 lines of Python. One dataclass, one assembly function, one cache wrapper. No ML frameworks, no vector databases, no external services. Just a layer that asks "what do the other modules know?" and makes the answer available everywhere.
2,791 tests pass. The total code footprint for P22 was about 3,500 lines across 20 files and 4 new modules. Same data, dramatically better answers.