A typed, deterministic day-level compiler and attention allocator for the OiMy family companion platform.
v0 · Developer Guide · May 2026Families do not want 80 skills shouting independently. They want the household to feel less chaotic tomorrow morning.
The Daily Rhythm Runtime is a day-level compiler and attention allocator inside OiMy's Hermes / Skill Engine. It collects signals from every available source, compiles them into a typed DailyState, detects opportunities worth surfacing, scores and filters nudges through a budget system, and composes concise morning briefs and evening closeouts — all without requiring LLM calls for the core pipeline.
Rhythm Runtime is a scheduled workflow layer inside Hermes, not a standalone engine or skill. It orchestrates existing skills and archetypes rather than owning domain logic.
| Principle | Implementation |
|---|---|
| Typed state | 12 fact types compiled into DailyState — not vibes-based |
| Deterministic pipeline | No LLM needed for signal collection, scoring, or brief structure |
| Privacy-first | Every fact carries a VisibleScope; nudges filtered by scope |
| Low token cost | Rules-based scoring; one optional LLM call for final prose polish |
| Provenance everywhere | Every fact tracks source, confidence, retrieval time, staleness |
| Budget-aware | Per-day and per-category caps, quiet hours, suppression history |
| Layer | Scope | Relationship to Rhythm |
|---|---|---|
| Journey | Days/weeks/months | Sets priorities and tone; Rhythm operates within Journey goals |
| Agenda | Single session | Launched only when user engages; Rhythm provides context to Agenda |
| Rhythm | Single day | This module — daily compilation & attention allocation |
| Skills | Domain-specific | Rhythm invokes skills for domain work (food, school, care, etc.) |
OiMy organizes execution patterns into 6 archetypes. Rhythm Runtime uses them as follows:
| Archetype | Role in Rhythm | Example |
|---|---|---|
| Monitor | Scans signals, detects deadlines/conflicts | Calendar conflict between 2 events |
| Planner | Synthesizes brief, closeout, schedule | Morning brief composition |
| Action | Creates reminder, adds event after confirmation | Create medication refill reminder |
| Expert | Parses newsletter, form, school email | Extract dates from school newsletter |
| Coach | Emotional tone, parent support | Warm greeting, caregiver support |
| Profile | Learns preferences, boundaries, nudge settings | User prefers minimal nudges → budget adjusted |
Rhythm uses OiMy's personalization layers for family-aware output:
Purpose: Defines the central typed state object (DailyState) and all supporting data types — fact types, enums, nudge budget, nudge candidates, brief output structures, and provenance tracking.
~500 LOC · Zero external dependencies beyond stdlib
| Enum | Values | Purpose |
|---|---|---|
VisibleScope | private, shared_family, care_team, guardian_only | Privacy scoping for every fact |
FactConfidence | high, medium, low, unknown | Trust level from source |
NudgeCategory | food, school, elder_care, habit, relationship, logistics, health, open_loop, calendar, general | Budget and suppression categories |
BriefItemType | event, deadline, task, nudge, fyi, risk, prep | Types of items in a composed brief |
| Class | Purpose |
|---|---|
ProviderFact | Tracks source, confidence, staleness, visibility for every fact |
EventFact | Calendar event with time, location, members |
DeadlineFact | Deadline with due date, urgency, action required |
TaskFact | Task with status, priority, assignment |
OpenLoopFact | Unresolved thread with carry-forward count |
RoutineFact | Habit/routine with streak tracking |
MealFact | Meal plan status with dietary notes |
SchoolFact | School item with child, due date, items needed |
CareFact | Elder/dependent care (medication, appointment, etc.) |
RiskFact | Risk with severity and mitigation |
OpportunityFact | Detected opportunity with relevance score |
NudgeBudget | Per-day budget with category caps, quiet hours, consumption |
NudgeCandidate | Scored nudge with 5 positive factors and 3 penalty factors |
MemberScope | Member identity, role, timezone, privacy scope, nudge posture |
SourceFreshness | Tracks when each signal source was last synced |
DailyState | Central state object compiled from all signals |
BriefItem | Single item in a composed brief |
BriefOutput | Full rendered brief with greeting, items, nudge, closing |
field(default_factory=...) to avoid mutable default pitfallsto_dict() method for controlled serializationDailyState auto-fills date and generated_at in __post_init__has_food_gap, has_urgent_items, total_items) enable cheap rule checks without LLMDailyState.to_dict(), total_items, and SignalCollector._distribute_signal().
Purpose: Gathers signals from all available sources (profile, memory, tasks, meals, routines, conversation history), normalizes them into SignalFact objects with provenance, and compiles them into a DailyState.
~500 LOC · Depends on daily_state.py + sqlite3
SignalCollector| Method | Signature | Purpose |
|---|---|---|
collect_all | (user_id, member_scope?, today?) → DailyState | Main entry point: collects all signals, deduplicates, compiles state |
_collect_from_profile | (user_id) → List[SignalFact] | Extracts dietary restrictions, family members needing care |
_collect_from_memory | (user_id, today) → List[SignalFact] | Retrieves active threads (open loops) and pending plans |
_collect_from_tasks | (user_id) → List[SignalFact] | Fetches incomplete tasks from task storage |
_collect_from_conversation_history | (user_id) → List[SignalFact] | Stub for future conversation signal extraction |
_collect_meal_signals | (user_id, today) → List[SignalFact] | Checks meal planning status for today |
_collect_routine_signals | (user_id) → List[SignalFact] | Checks active routine/habit status |
_distribute_signal | (state, signal) → None | Routes a SignalFact to the correct typed slot on DailyState |
_build_nudge_budget | (member_scope?) → NudgeBudget | Constructs budget from member preferences |
SignalFactA normalized signal from any source. Contains fact_type, data dict, provenance, and a deterministic fact_id (SHA-256 of content, first 12 chars) for deduplication.
_sources_checked is reset at the start of every collect_all call to prevent cross-user contamination[a-zA-Z0-9_\-.] only) to prevent SQL injectionexcept sqlite3.OperationalError: passfact_id computed from content hash_collect_from_conversation_history method is currently a stub — future integration point for Honcho/GBrain.
Purpose: Scans a compiled DailyState for nudge-worthy opportunities, scores each candidate using a multi-factor formula, applies suppression and deduplication, and filters through the nudge budget.
~350 LOC · Pure logic, no DB or LLM calls
OpportunityDetector| Method | Purpose |
|---|---|
detect_all(state) → List[NudgeCandidate] | Main entry: runs all detectors, scores, ranks, budget-filters |
_detect_dinner_gap | Unplanned dinner + busy evening → food nudge |
_detect_school_deadlines | School items due within 7 days |
_detect_open_loops | Items carried forward ≥2 times |
_detect_elder_care_risks | High/critical urgency care items + medication refill timing |
_detect_habit_misses | Pending routines with established streaks (≥3 days) |
_detect_calendar_conflicts | Events with <15 min gap or overlap |
_detect_urgent_deadlines | Deadlines due today or tomorrow |
_apply_suppression | Removes suppressed categories, applies dismissal penalties |
_deduplicate | Deduplicates by fact_id, penalizes previously-sent nudges |
_apply_budget | Selects top candidates within budget constraints |
Purpose: Generates human-readable morning briefs and evening closeouts from DailyState and optional nudge candidates.
~350 LOC · Pure logic, no DB or LLM calls
BriefComposer| Method | Signature | Purpose |
|---|---|---|
compose_morning_brief | (state, nudge_candidates?, member_name?) → BriefOutput | Composes morning brief with prioritized items |
compose_evening_closeout | (state, completed_today?, member_name?) → BriefOutput | Composes evening review with done/open/prep sections |
Purpose: Top-level orchestrator that chains signal collection → state compilation → opportunity detection → brief composition → outcome recording. Provides the entry points called by API endpoints.
~300 LOC
RhythmRuntime| Method | Returns | Pipeline Steps |
|---|---|---|
run_morning_brief(user_id, scope, member_scope?, today?) | Dict | collect → detect → privacy filter → compose → record → return |
run_evening_closeout(user_id, scope, member_scope?, completed_today?, today?) | Dict | collect → compose → record → return |
get_daily_state(user_id, member_scope?, today?) | Dict | collect → return (no brief, useful for debugging) |
OutcomeRecorderRecords every brief and nudge delivery to the rhythm_outcomes and rhythm_nudge_log tables for the Reflection QA feedback loop.
| Method | Purpose |
|---|---|
record_brief | Stores brief JSON, item count, nudge count, full DailyState |
record_nudges | Logs each delivered nudge with category and priority score |
get_sent_nudge_ids | Retrieves today's sent nudges for dedup |
get_suppression_history | Counts dismissed nudges per category over last 7 days |
After opportunity detection, nudges are filtered by scope:
private and shared_family nudgesshared_family and care_team nudges_validate_user_id() rejects empty/invalid user IDs; _validate_scope() enforces allowlistPurpose: Re-exports all public types from the rhythm package for clean imports. 50 LOC.
from archetypes.rhythm import (
DailyState, MemberScope, NudgeBudget, NudgeCandidate,
BriefOutput, BriefItem, ProviderFact,
SignalCollector, OpportunityDetector, BriefComposer,
RhythmRuntime, OutcomeRecorder,
)
| Field | Type | Purpose | Example | Privacy |
|---|---|---|---|---|
date | str | ISO date for this state | "2026-05-29" | — |
timezone | str | IANA timezone | "America/New_York" | — |
household_id | str | Household identifier | "hh_sarah" | — |
member_id | str | Individual member | "sarah" | — |
visible_scope | str | Current visibility filter | "private" | Controls output |
events | List[EventFact] | Calendar events | — | Per-fact |
deadlines | List[DeadlineFact] | Upcoming deadlines | — | Per-fact |
tasks | List[TaskFact] | Pending / in-progress tasks | — | Per-fact |
open_loops | List[OpenLoopFact] | Unresolved threads | — | Per-fact |
routines | List[RoutineFact] | Habits with streaks | — | Per-fact |
meals | List[MealFact] | Meal plan status | — | shared_family |
school_items | List[SchoolFact] | School deadlines/events | — | guardian_only |
elder_care_items | List[CareFact] | Medication, appointments | — | care_team |
risks | List[RiskFact] | Identified risks | — | Per-fact |
opportunities | List[OpportunityFact] | Detected opportunities | — | Per-fact |
nudge_budget | NudgeBudget | Today's nudge budget | — | — |
suppressed_categories | List[str] | Categories user disabled | ["food"] | — |
provenance | List[ProviderFact] | All source provenance records | — | — |
source_freshness | List[SourceFreshness] | Sync status per source | — | — |
generated_at | str | ISO timestamp of compilation | — | — |
| Property | Type | Logic |
|---|---|---|
total_items | int | Sum of all fact list lengths |
has_food_gap | bool | any(meal.meal_type == "dinner" and meal.plan_status == "unplanned") |
has_urgent_items | bool | Any deadline, open loop, or care item with urgency "high" or "critical" |
| Field | Type | Purpose |
|---|---|---|
title | str | Short display title |
description | str | Longer description |
category | str | NudgeCategory value for budget tracking |
urgency | float 0-1 | How time-sensitive |
importance | float 0-1 | How consequential if missed |
confidence | float 0-1 | How certain the system is about the data |
relevance | float 0-1 | How relevant to the user today |
actionability | float 0-1 | How actionable the suggestion is |
annoyance_cost | float | Penalty for potential annoyance (higher for naggy categories) |
duplicate_penalty | float | Penalty if already sent today (0.3 if duplicate) |
privacy_risk_penalty | float | Penalty for privacy risk |
suggested_action | str | Suggested next step for the user |
why_now | str | Explains timing of nudge |
source_facts | List[str] | Fact IDs that generated this nudge |
visibility | str | Who can see this nudge |
is_interruption | bool | Whether this counts against interrupt budget |
| Parameter | Default | Tunable? | Notes |
|---|---|---|---|
max_nudges_per_day | 3 | Yes — via MemberScope.nudge_posture | minimal=1, normal=3, detailed=5 |
max_interruptions | 1 | Yes | minimal=0, detailed=2 |
quiet_hours_start | "22:00" | Yes | No nudges during quiet hours |
quiet_hours_end | "07:00" | Yes | |
| Category caps | food=1, school=2, elder_care=2, habit=1, relationship=1, logistics=2, health=2, general=1 | Yes | Per-category daily limits |
| Field | Type | Purpose |
|---|---|---|
source | str | Origin: "calendar", "memory", "profile", "tasks", etc. |
source_id | str | Unique ID within source for dedup |
retrieved_at | str | ISO timestamp of retrieval |
confidence | str | high / medium / low / unknown |
visibility | str | Privacy scope of this fact |
stale_after_hours | int | TTL — default 24 hours |
raw_excerpt | str | Short excerpt for "why am I seeing this?" |
The system runs 7 deterministic detectors. Each produces zero or more NudgeCandidate objects with pre-set scoring factors.
| Trigger | Any meal with meal_type="dinner" and plan_status="unplanned" |
|---|---|
| Urgency boost | 0.7 if evening events exist (16:00-20:00), else 0.4 |
| Category | food |
| Annoyance cost | 0.1 (low — food help is generally welcome) |
| Visibility | shared_family |
| Trigger | School items with due_date within 7 days |
|---|---|
| Urgency formula | min(1.0, max(0.3, 1.0 - (days_until × 0.15))) |
| Category | school |
| Visibility | guardian_only if child-specific, else shared_family |
| Trigger | Open loops with carry_forward_count ≥ 2 |
|---|---|
| Urgency formula | min(1.0, 0.3 + carry_count × 0.1) |
| Annoyance cost | 0.15 (higher — nagging about old stuff) |
| Action | "Resolve, defer, or drop?" |
| Trigger | Care items with urgency="high" or "critical", OR medication items with last_completed >25 days ago |
|---|---|
| Urgency | 0.9 for critical, 0.75 for high |
| Annoyance cost | 0.02 (very low — care alerts are high-value) |
| Visibility | care_team |
| Interrupt | Yes, if urgency is critical |
| Trigger | Routines with status="pending" and streak_days ≥ 3 |
|---|---|
| Annoyance cost | 0.2 (highest — habits feel naggy) |
| Why now | "{N}-day streak at risk" |
| Trigger | Events with <15 minutes gap between end of one and start of next |
|---|---|
| Urgency | 0.8 if overlap, 0.5 if tight gap |
| Confidence | 0.9 (calendar data is authoritative) |
| Trigger | Deadlines with due_date today or tomorrow |
|---|---|
| Urgency | 0.95 if today, 0.7 if tomorrow |
| Interrupt | Yes, if due today AND urgency is high/critical |
After all detectors run, candidates pass through three stages:
Candidates in suppressed categories are removed. Remaining candidates get annoyance cost increased by dismissed_count × 0.05 based on the user's 7-day dismissal history.
Duplicates by fact_id are removed. Candidates whose base ID matches a previously-sent nudge get duplicate_penalty += 0.3.
Candidates are sorted by priority_score descending. The top candidates that fit within NudgeBudget constraints (total budget, interruption budget, category caps) are selected.
After opportunity detection, the runtime applies scope-based filtering:
| Request Scope | Visible Nudge Scopes |
|---|---|
individual | private, shared_family |
family | shared_family, care_team |
care_team | (full access — inherits from family scope in v0) |
This ensures private emotional data never appears in a family brief, and care-team data only appears for authorized members.
The BriefComposer transforms DailyState + scored NudgeCandidate list into a readable BriefOutput:
Events, urgent deadlines, school items, care items, high-priority tasks, and food gaps are collected. Each gets a priority integer (9 for care down to 3 for FYI).
Items are sorted by priority descending and truncated to max_items (default 5).
Context-aware greeting based on: item count, day of week, time of day, presence of urgent items, and member name.
The top-scored nudge candidate's suggested_action becomes the brief's single nudge.
BriefOutput.render_text() produces numbered items with time, action, and source annotations.
Generate a morning brief for a user.
Bearer token in Authorization header, validated by authenticate_request(). Falls back to user_id in request body.
{
"user_id": "sarah",
"scope": "individual", // "individual" | "family" | "care_team"
"today": "2026-05-29", // optional, defaults to today
"member_scope": { // optional
"member_id": "sarah",
"household_id": "hh_sarah",
"display_name": "Sarah",
"role": "adult",
"visible_scope": "private",
"timezone": "America/New_York",
"nudge_posture": "normal",
"suppressed_categories": []
}
}
{
"status": "ok",
"brief_type": "morning",
"scope": "individual",
"user_id": "sarah",
"date": "2026-05-29",
"message": "Good morning, Sarah — 3 things on your Thursday: ...",
"brief": { /* BriefOutput.to_dict() */ },
"daily_state": { /* DailyState.to_dict() */ },
"nudge_candidates": [ /* NudgeCandidate[].to_dict() */ ],
"elapsed_ms": 42,
"source_freshness": [ /* SourceFreshness[] */ ]
}
curl -X POST http://localhost:8000/rhythm/morning \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"user_id": "sarah",
"scope": "family",
"member_scope": {
"member_id": "sarah",
"household_id": "hh_sarah",
"display_name": "Sarah",
"nudge_posture": "normal"
}
}'
Generate an evening closeout.
{
"user_id": "sarah",
"scope": "individual",
"today": "2026-05-29",
"completed_today": ["Signed permission slip", "Grocery run"],
"member_scope": { /* same as morning */ }
}
Same structure as morning, but brief_type: "evening" and no nudge_candidates array.
curl -X POST http://localhost:8000/rhythm/evening \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"user_id": "sarah",
"completed_today": ["Signed permission slip"]
}'
Get the compiled DailyState without generating a brief. Useful for debugging and downstream consumers.
{
"user_id": "sarah",
"today": "2026-05-29"
}
{
"status": "ok",
"user_id": "sarah",
"date": "2026-05-29",
"daily_state": { /* full DailyState.to_dict() */ }
}
curl -X POST http://localhost:8000/rhythm/state \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{"user_id": "sarah"}'
Sarah is a working parent with two school-age children. Her son Liam has a nut allergy. She also helps coordinate elder care for her mother ("Amma") who takes blood pressure medication. It's a Thursday morning.
The SignalCollector queries all sources for user "sarah":
MealFact(dietary_notes="nut allergy")needs_care=true → CareFact(person_name="Amma")OpenLoopFact(carry_forward_count=3)TaskFactMealFact(meal_type="dinner", plan_status="unplanned")RoutineFactAll signals carry ProviderFact provenance with source and confidence.
Signals are deduplicated by fact_id and distributed to typed slots. Result:
DailyState(
date="2026-05-29",
meals=[dinner:unplanned, any:nut_allergy_pref],
open_loops=[plumber:carry_3],
tasks=[permission_slip:pending],
elder_care_items=[amma:general_care],
routines=[meditation:pending:streak_10],
has_food_gap=True, has_urgent_items=False,
nudge_budget=NudgeBudget(max=3, interrupt=1)
)
The OpportunityDetector runs all 7 detectors:
| Detector | Result | Score |
|---|---|---|
| Dinner gap | ✅ Unplanned dinner | ~0.15 |
| Open loops | ✅ Plumber (3x carry-forward) | ~0.08 |
| Habit miss | ✅ Meditation (10-day streak) | ~0.01 |
| School deadlines | — | (no school items with due dates) |
| Elder care | — | (not marked high/critical) |
| Calendar conflicts | — | (no events) |
| Urgent deadlines | — | (no deadlines) |
Ranked by score: dinner gap > open loop > habit. Budget allows 3 → all pass.
Scope is "individual" → keeps private and shared_family nudges. All three pass (dinner is shared_family, open loop and habit are private).
The BriefComposer assembles the morning brief:
Good morning, Sarah — clear day ahead, nothing pressing.
Want a quick dinner idea based on what you usually have?
(With no events, deadlines, or urgent items, the brief is minimal — just the top nudge about dinner.)
The OutcomeRecorder stores the brief and all delivered nudges to rhythm_outcomes and rhythm_nudge_log tables for the Reflection QA loop.
The full response includes the rendered message, structured brief, full daily_state, nudge_candidates with scores, elapsed_ms, and source_freshness — everything a consumer needs to render, audit, or debug.
tests/test_rhythm_runtime.py53 Tests · All Passing
| Test Class | Tests | Covers |
|---|---|---|
TestDailyState | 5 | Empty state, date setting, food gap detection, urgent item detection, serialization, total items count |
TestNudgeBudget | 4 | Basic budget, consumption, category caps, interruption budget |
TestNudgeCandidate | 3 | Priority score formula, penalty clamping, serialization with score |
TestMemberScope | 2 | Default scope, minimal posture |
TestSignalCollector | 5 | Empty DB, profile signals, open loop extraction, budget from member scope, deduplication |
TestOpportunityDetector | 9 | All 7 detectors individually, budget filtering, suppression, dedup with sent IDs |
TestBriefComposer | 9 | Empty brief, events, nudges, max items, member name, evening closeout, tomorrow prep, render_text |
TestOutcomeRecorder | 2 | Record and retrieve, nudge log |
TestRhythmRuntime | 7 | Morning integration, evening integration, family scope, get_daily_state, privacy scope, elapsed_ms, source_freshness |
TestEdgeCases | 5 | Missing profile, empty signals, mixed privacy scopes, negative score clamping, staleness detection |
cd /home/exedev/oimy-engine
python -m pytest tests/test_rhythm_runtime.py -v
# Or with unittest directly:
python -m unittest tests.test_rhythm_runtime -v
Automated code review across all 5 modules.
| Module | Result | Key Findings |
|---|---|---|
daily_state.py | PASS | All types compile, serialization works, privacy enums correct |
signal_collector.py | PASS (was PARTIAL) | Fixed: input validation added, _sources_checked reset per call, user_id sanitization |
opportunity_detector.py | PASS | Scoring formula correct, budget filtering works, suppression works |
brief_composer.py | PASS | Warm tone, max items respected, provenance attached |
rhythm_runtime.py | PASS (was PARTIAL) | Fixed: input validation, scope validation, privacy filtering for nudges |
_sanitize_user_id() with regex validation_sources_checked per call_validate_user_id() and _validate_scope()Average score: 4.69 / 5.0 across 7 simulated family profiles.
| Profile | Archetype | Relevance | Conciseness | Safety | Actionability | Tone | Avg |
|---|---|---|---|---|---|---|---|
| 1 | Two-parent, school-age kids | 5 | 5 | 5 | 5 | 4 | 4.8 |
| 2 | Solo parent | 4 | 5 | 5 | 4 | 5 | 4.6 |
| 3 | Multigenerational household | 5 | 4 | 5 | 5 | 4 | 4.6 |
| 4 | Power user, many integrations | 5 | 4 | 5 | 5 | 4 | 4.6 |
| 5 | Privacy-sensitive individual | 5 | 5 | 5 | 5 | 4 | 4.8 |
| 6 | New user (minimal data) | 5 | 5 | 5 | 4 | 5 | 4.8 |
| 7 | Elder caregiver | 5 | 4 | 5 | 5 | 4 | 4.6 |
Critical failures: 0 | Skill routing accuracy: 100%
Overall UX score: 7 / 10
| Dimension | Score | Notes |
|---|---|---|
| Warmth | 6 | Improved with context-aware greetings; room to grow |
| Cultural Sensitivity | 8 | Good use of family terms (e.g., "Amma") |
| Privacy | 6 (was 4) | Added source provenance transparency; deeper controls needed |
| Trust | 7 (was 5) | Source attribution and confidence indicators added |
| Embarrassment Risk | 7 | Safe outputs across all profiles |
| Tone | 7 (was 6) | Warmer greetings, less clinical language |
| Actionability | 8 | Clear next steps, time-sensitive highlighting |
| Onboarding | 9 | Excellent new-user experience — gentle, reassuring |
| # | Trigger | Fix | Status |
|---|---|---|---|
| 1 | QA: SQL injection risk | Added _sanitize_user_id() with regex | ✅ |
| 2 | QA: No privacy filtering | Added scope-based nudge filtering | ✅ |
| 3 | QA: Cross-user state leak | Reset _sources_checked per collect_all call | ✅ |
| 4 | QA: No input validation | Added _validate_user_id(), _validate_scope() | ✅ |
| 5 | UX: Low warmth score | Improved greeting patterns | ✅ |
| 6 | UX: Repeated care nudges | Differentiated care actions by type | ✅ |
| 7 | Regex escape bug | Fixed Python regex for user_id validation | ✅ |
| Verdict | Meaning |
|---|---|
| SHIPPABLE | All tests pass, QA scores PASS, simulation avg ≥4.5, UX ≥7, zero critical failures, privacy filtering implemented |
| CONDITIONAL | Core works but has unresolved issues that need fixes before deployment |
| NOT READY | Critical failures, test failures, or fundamental architecture issues |
Current verdict: SHIPPABLE
| Feature | Status | Priority |
|---|---|---|
| Calendar OAuth integration | Memory-only signals for now | High — Phase 2 |
| Email/newsletter ingestion | Not implemented | Medium — Phase 3 |
| Conversation history signal collection | Stub method exists | Medium — Phase 2 |
| Cross-member coordination | Single-member only in v0 | Medium — Phase 4 |
| Relationship cadence nudges | Detector not yet built | Low — Phase 3 |
| Weather integration | Not implemented | Low |
| UI-level privacy controls | Backend-only in v0 | High — Phase 2 |
| Quiet hours enforcement | Fields exist, enforcement not wired | Medium |
_detect_<name> on OpportunityDetectorDailyState and return List[NudgeCandidate]detect_all()NudgeCategory enum and NudgeBudget.category_capsTestOpportunityDetectordef _detect_weather_risk(self, state: DailyState) -> List[NudgeCandidate]:
"""Detect weather-related risks for outdoor events."""
candidates = []
# Check for outdoor events + bad weather forecast
# ... your logic here ...
return candidates
_collect_<source>_signals on SignalCollectorList[SignalFact] with appropriate fact_type and ProviderFact provenancecollect_all()self._sources_checked.append(SourceFreshness(...))daily_state.py and update _distribute_signal()household_id field on DailyState and MemberScope is already designed for thisHouseholdScope dataclass with member list, shared preferences, permissionsSignalCollector.collect_all() to accept household-level signalsThe nudge budget is controlled by MemberScope.nudge_posture:
| Posture | max_nudges_per_day | max_interruptions | Best for |
|---|---|---|---|
minimal | 1 | 0 | Users who want minimal proactive contact |
normal | 3 | 1 | Default — balanced proactive support |
detailed | 5 | 2 | Power users who want comprehensive updates |
Users control what they receive via MemberScope.suppressed_categories:
# User doesn't want food nudges
scope = MemberScope(
member_id="sarah",
household_id="hh_sarah",
suppressed_categories=["food", "habit"]
)
Suppressed categories are completely filtered out — not just scored lower.
Each detector sets its own scoring factors. To adjust:
| What to change | Where | Effect |
|---|---|---|
| Increase food nudge priority | _detect_dinner_gap: raise importance | Dinner nudges rank higher |
| Reduce habit nagging | _detect_habit_misses: raise annoyance_cost | Habit nudges score lower, filtered first |
| Make elder care always top | _detect_elder_care_risks: raise all factors | Care nudges always win budget |
| Reduce duplicate penalty | _deduplicate: lower the 0.3 constant | Repeated nudges more likely to show again |
| Adjust suppression sensitivity | _apply_suppression: change 0.05 multiplier | Dismissed categories recover faster/slower |
Currently, the Rhythm Runtime surfaces opportunities and composes briefs. Skill dispatch (e.g., invoking Food Intelligence when user accepts a dinner nudge) happens in the Agenda/Hermes layer. To add new mappings:
| Parameter | Default | Effect |
|---|---|---|
max_items | 5 | Maximum items in a brief (increase for power users) |
max_nudges | 1 | Maximum nudge suggestions per brief |
| Priority integers | 3-9 | Elder care(9) > deadline(8) > school(7) > task(6) > event(5) > FYI(3) |
OiMy Daily Rhythm Runtime · v0 · May 2026
Built with care for families who deserve less chaos and more confidence.