Google Fit REST API was closed to new sign-ups on 2024-05-01 and shuts down
end of 2026, surfacing as "Access blocked: this app's request is invalid"
when starting the OAuth flow.
- Swap the 10 fitness.* OAuth scopes for the 3 googlehealth.*.readonly
scopes (activity_and_fitness, health_metrics_and_measurements, sleep).
- Replace fitness/v1 dataset:aggregate + sessions calls with
health.googleapis.com/v4/users/me/dataTypes/{steps,total-calories,
heart-rate,sleep}/dataPoints, filtered to today's window.
- Read the v4 DataPoint union defensively (the per-type schema is sparsely
documented) and log the first raw sample at debug so we can refine field
paths after the first real OAuth.
- Output Signal contract is unchanged — agents and downstream consumers
see the same steps/activity/heart_rate/sleep signals.
Cloud Console still needs: enable Google Health API, add the 3 scopes to
the consent screen, add test user (all googlehealth scopes are Restricted).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Computes natal chart (Sun/Moon/Mercury/Venus/Mars/Jupiter/Saturn) from
birth_date and finds active transits (conjunction/sextile/square/trine/
opposition) between today's sky and the user's natal positions. Top 3
most-exact transits are passed to the orchestrator as interpretive themes
to colour the tip — grounded and actionable, not predictive.
Birth date sourced from agent_prefs (populated by a connected Google
data source); requires data:google-health consent. Agent self-silences
when birth_date is absent. pyswisseph added to ml/serving/requirements.txt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Draws 3 Major Arcana cards from a daily seed (user_id + date) so the
reading is stable within a day and unique per user. Card meanings and
action hints are precomputed in the agent; the orchestrator receives a
structured prompt snippet and is instructed to weave the themes into a
grounded, practical tip without explaining the cards.
No inferred params, no external data — requires only data:core consent.
TTL 6 h (refreshes at most twice daily).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The OAuth backend (signal source, /connect and /callback routes, token
refresh, consent grant) was already complete. This adds the missing UI:
a Google Health card in /connect with Connect/Disconnect actions, and
broadens the "See my tip →" CTA to appear when any integration is
connected (not only Todoist).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The integration-token gate returned 422 for users with no connected
sources, blocking them from any tip. Users with no integrations now go
through the full orchestrator pipeline; if it fails (or returns nothing
because agent outputs are also empty), randomFallbackTip() fires and
serves a generic advice tip instead of an error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Check getEligibleAgentIds per user in runCycle before calling
computeAndStore — agents without consented data sources, silenced by
active context, or disabled via preference are skipped rather than
computed unconditionally. Eligibility check failure skips the whole
user (fail-closed). Skipped count added to cycle-complete log line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Roadmap phase sections now show shipped summaries only; open work lives
in Gitea milestones. Eliminates duplicate source-of-truth between README
and issue tracker.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `ai` compose profile — Ollama + LiteLLM containers for local dev
when Agap shared services are unavailable; use with LITELLM_URL /
OLLAMA_URL env vars pointing ml-serving at localhost
- Mark #90 done (LLM schema validation + fallback shipped in 85a332b)
- Mark #80 superseded by ADR-0013 (multi-agent orchestrator is the pipeline)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Python (ml/serving):
- Validate tip item after JSON parse: non-empty content, valid kind
- Retry on schema failure with a targeted clarification prompt, same 2× retry budget
- JSON parse failures keep the existing retry suffix
TypeScript (recommender):
- Add TipSource 'fallback' to shared-types
- FALLBACK_TIPS: 12 general-purpose life tips (hardcoded, no DB read)
- fetchOrchestratorTip returns {ok} discriminated union instead of null
- On !res.ok or fetch error: serve a random fallback tip with rationale 'AI service issues'
- Update tests: 204 path removed; both failure cases now expect source='fallback'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- integrations.ts: grant data:<provider> on OAuth callback, revoke on disconnect
- Backfill migration: INSERT OR IGNORE data:<provider> for all active tokens
- Agent manifests: drop agent:<id> from required_consents (momentum, time-of-day,
overdue-task, recent-patterns, health-vitals) — per-agent control is a preference
- eligibility.ts: update comment to reflect data:-only consent model
- test_manifest.py: assert no agent: consents remain in any manifest
- migrations.test.ts: backfill idempotency tests for issue #127
- Dockerfile.api: drop --offline flag (fixes ERR_PNPM_NO_OFFLINE_META)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cluster_tasks now attaches enriched_description to each task dict.
focus-area reads enriched_description (falling back to raw content) when
building the area summary, so the orchestrator sees the expanded 3-sentence
descriptions instead of terse raw titles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The agent no longer picks a winner — it summarises every cluster so the
orchestrator can decide what's relevant. Scoring by overdue count overlapped
with the overdue-task agent. preferred_areas (project-ID based, broken label
matching) removed entirely.
Output format: numbered list of areas with task titles included.
Snapshot: {cluster_count, clusters: [{label, task_count, tasks}]}.
Version bumped to 3.0.0; inferred_params cleared.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If tasks haven't changed, the output is valid forever. If they changed,
always recompute regardless of age. TTL on focus-area restored to 24h —
it only controls recommender eligibility, not recompute frequency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
focus-area now recomputes at most once per hour, and only if the task list
actually changed since the last compute.
- focus-area TTL: 43200s → 3600s; version bumped to 2.1.0
- computeAndStore hashes sorted task contents (MD5) and checks the stored
_task_hash in the existing snapshot; skips the ml-serving call when the
hash matches and the output isn't expired
- ml-serving injects _task_hash into the snapshot so the next cycle can compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each unique task title is now enriched by LiteLLM once and cached in the DB.
Subsequent agent compute cycles (every 12h) fetch the cache before calling
ml-serving; only new titles hit the tip-generator.
- DB: task_enrichments(content_hash PK, description, model, created_at)
- TS: fetchEnrichmentCache / persistEnrichments helpers in agent-outputs.ts;
enrichment_cache passed in compute request, new_enrichments persisted from response
- Python: AgentComputeRequest.enrichment_cache / AgentComputeResponse.new_enrichments;
AgentInput.enrichment_cache; _enrich_batch returns (descriptions, new_entries);
cluster_tasks returns (clusters, new_enrichments)
- FocusAreaAgent stashes new_enrichments in signals_snapshot under _new_enrichments;
compute_agent endpoint pops it before storing the snapshot
Closes part of #129
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ported from taskpile experiments/clustering_eval (prompt v1, qwen2.5:1.5b).
The experiment showed ARI 0.22→0.77 and AUROC 0.76→0.91 on synthetic tasks
when embedding LLM-expanded descriptions instead of raw titles.
- Expand each task title via LiteLLM tip-generator before embedding
- Prefix with "clustering: " (nomic-embed-text task instruction prefix)
- Cache expansions in-memory by content hash within a compute cycle
- Falls back to raw title if enrichment fails; no change to fallback behaviour
Fixes#129
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The old code called Ollama's /api/embeddings one task at a time, which caused
silent fallback to project-based grouping when host.docker.internal:11434 was
unreachable from the ml-serving container.
- Switch to LiteLLM /embeddings (model alias "embedder") as primary path
- Batch all task contents in one request instead of N serial calls
- Fall back to Ollama /api/embed (updated to current API) when LITELLM_URL is absent
- Update tests to mock _embed_batch instead of the removed _embed
Fixes#123
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When fetchOrchestratorTip returned null (LiteLLM timeout, bad JSON, etc.)
the recommender silently fell back to randomPolicy, serving a raw Todoist
task with no rationale — explaining both reported symptoms.
- Remove randomPolicy/signalToCandidate; return 204 when orchestrator fails
so the UI shows "All clear" instead of a confusing Todoist task
- Pass recent_tip through the stack (frontend → POST /recommend →
fetchOrchestratorTip → ml/serving RecommendRequest → build_orchestrator_messages)
so after snooze the LLM is instructed not to repeat the snoozed content
Fixes#122
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The middleware was redirecting non-admins to /forbidden but /forbidden
wasn't excluded from the matcher, so the middleware ran again on that
page, saw a non-admin, and redirected again — infinite loop. Added
/forbidden to the pass-through list alongside /login.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OAuth2 flow with all 11 Google Fitness scopes (activity, body, sleep,
heart rate, nutrition, location, blood glucose/pressure/temperature,
oxygen saturation, reproductive health). Stores access + refresh tokens;
auto-refreshes on expiry.
GoogleHealthSignalSource fetches steps, sleep sessions, active minutes,
calories, and heart rate from the Fit aggregate + sessions APIs. Signals
flow into both the tip orchestrator and the health-vitals pre-compute
agent, which generates prompt snippets about step progress, sleep
deficit, sedentary time, and elevated heart rate.
Signal.kind extended with 'health'; IntegrationProvider extended with
'google-health'. Agent compute signal mapping enriched to include source,
kind, and all features so health-vitals can filter its own signals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Convert ml-serving from isolated MLflow runs to nested traces using
mlflow.start_span_no_context(). The recommend endpoint now emits a full
span tree: recommend (CHAIN) → build_context (TOOL), agent:* (AGENT) ×N,
llm_orchestrator (LLM). Compute and infer endpoints each emit a single span.
Supporting changes:
- mlflow-skinny>=3.1.0 added to requirements
- MLflow configured with --serve-artifacts + mlflow-artifacts:/ default root
for cross-container artifact proxy (spans now persist from ml-serving)
- --allowed-hosts extended to include mlflow:5000 (SDK includes port in Host)
- science_destiny slider wired through prompts.py and recommend endpoint
- Config page exposes science/destiny slider (0=data-driven, 100=intuitive)
- Tip page shows rationale inline on tap
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Small models (qwen2.5:1.5b) mirror the language of task title content
in the prompt. Adding an explicit English note to snippets that embed
raw task titles (focus-area, overdue-task) prevents language bleed.
Also added the instruction to the orchestrator system prompt and user
message as belt-and-suspenders.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pass MLFLOW_ADMIN_PASSWORD as fallback password credential
- Set host_header='localhost' to satisfy MLflow's --allowed-hosts check
(MLflow rejects Host: mlflow but accepts Host: localhost)
- Default MLFLOW_TRACKING_URI to http://mlflow:5000 in compose so the
env_file value is not silently overridden to empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs one MLflow run per /recommend (params, token metrics, latency,
full prompt + tip as artifacts) and per /agents/{id}/compute and
/infer call (signals snapshot, inferred prefs, latency).
Tracing is a no-op when MLFLOW_TRACKING_URI is unset; ml-serving
starts and serves tips correctly without MLflow configured.
Refs #118 (M4: remove from production / move off critical path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds invalidated_by: tuple[str, ...] to ProfileFeature, mirroring the
invalidatedBy bus subjects from registry.ts. Adds a test that parses the
TS source and asserts Python stays in sync — same drift-detection pattern
used for names and ttlSec.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- focus-area v2.0.0 completion in recent completions; remove from active work
- Update focus-area inferred params table row
- min_history gotcha: checked against events, not task_completions
- httpx trust_env=False rule for ml/ code
- Agent test command
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inference framework table updated: all agents at v1.2.0 with full param list
- Documents UserHistory.task_completions and AgentInferRequest.task_completions
- Marks #112/114/115/116 complete in recent completions
- Active work updated: #78 closed, #61 and #97/#113 as next priorities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds four InferredParams (all TTL=24h, min_history=50 except preferred_hour=10):
- quiet_start / quiet_end: longest contiguous below-baseline hour run (HH:MM)
- peak_hours: top-quartile done-event hours, sorted ascending
- tz: cold-start only ("UTC"); populated from auth provider, no inference function
compute() updated:
- in_quiet check (quiet window) takes precedence over peak hours
- in_peak emits "peak productivity hour" language when current hour is in peak_hours
- approaching peak (within 2h) surfaces for orchestrator timing
- tz surfaced in snippet header when not UTC
- snapshot adds peak_hours, in_quiet, in_peak, tz
- Agent bumped to v1.2.0
- 21 new tests: night-owl, early-bird, shift-worker, quiet/peak snippet rendering
- Fixed test_snapshot_keys in test_agents.py to include new snapshot fields
Closes#112
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the coarse density-bucket window_days with three InferredParams (all TTL=24h):
- lookback_days: min window containing ≥30 done events, capped at 30d (min_history=5)
- weekly_cycle: per-DOW peak-to-mean strength list (min_history=21, ≥3 weeks of signal)
- daily_cycle: per-hour peak-to-mean strength list (min_history=14)
compute() renders cycle hints when strength > 0.5:
"User tends to complete tips on Tuesdays and Saturdays."
"User is most active around 8pm."
Legacy window_days pref key still accepted as a fallback.
- window_days pref renamed lookback_days; backward-compat fallback in compute()
- Agent bumped to v1.2.0
- 19 new tests: weekend-warrior, weekday-only, evening-person, no-pattern,
legacy compat, snippet rendering with strong/weak signals
Closes#116
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds two InferredParams (TTL=7d) computed from 28-day rolling daily done counts:
- baseline_completions_per_day: mean done events/day over the window
- stdev: stdev of daily counts (floored at 0.1 to avoid division by zero)
MomentumAgent.compute() now calculates a z-score from recent done events in
inp.feedback_history vs the inferred baseline. Snippet language switches to
z-score framing ("above your usual pace", "slowing down") when |z| >= 1.0,
falling back to engagement_trend labels when in the normal range.
- engagement_trend InferredParam preserved for backward compatibility
- momentum_window pref added (default 7, user-overridable)
- 14 new tests covering power user, casual user, returning-from-break, and
relative stdev comparison; engagement_trend tests updated for z-score priority
- Agent bumped to v1.2.0
Closes#114
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces snooze-rate heuristic with p50 of actual task lateness (completedAt − dueAt).
Adds project_realness inference: projects with chronic lateness get realness < 1 and
the agent softens its snippet language from "overdue" to "past target date".
- TaskCompletion added to UserHistory with lateness_days computed property
- _infer_lateness_tolerance: p50 of task_completions, clipped at 0, float
- _infer_project_realness: per-project median lateness normalised by global median
- Both InferredParams use 7d TTL; cold_start = 0.0 / {}
- AgentInferRequest accepts task_completions; endpoint wires them through
- 12 new tests covering punctual/chronic/mixed users and language softening
- Agent bumped to v1.2.0
Closes#115
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backfills consent_given=1 rows into user_consents as data:core before
dropping the legacy columns. auth.ts now writes user_consents on signup;
POST /consent writes user_consents; admin/user routes cleaned of the old
fields. Migration is idempotent — DROP COLUMN is wrapped in try/catch so
it no-ops on fresh DBs that never had the columns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All four agents bumped to v1.1.0.
momentum (#114): infers engagement_trend ('up'|'stable'|'down') by comparing
done-rate in the last 7 days vs the prior 7 days. Agent surfaces the trend
in its snippet ("trending up — build on the momentum").
overdue-task (#115): infers lateness_tolerance_days (0/1/2) from snooze rate.
Agent now filters tasks against the tolerance so low-urgency users aren't
nagged about tasks that are only hours overdue.
recent-patterns (#116): infers window_days (7/14/30) from feedback event
density — sparse users get a wider window so the snippet isn't always empty.
focus-area (#113): no inferred params (project-level feedback linkage needed,
tracked under #78). preferred_areas pref was declared but ignored; agent now
honours it as a tiebreaker and mentions it in the snippet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each agent now exports a module-level MANIFEST declaring id, version,
pref_schema, required_consents, ttl_sec, and silenced_in_contexts. The
registry surfaces both the agent and its manifest, and rejects on
mismatch so the two cannot drift.
ml/serving exposes GET /agents/registry; services/api proxies it as
GET /api/agents/registry with a 60s in-process cache so admin pageviews
don't hammer upstream. Failures aren't cached.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds user_preferences, user_consents, user_contexts and the tone /
tip_kinds_json columns on users. Backfills consent_given=1 rows into
user_consents as data:core; INSERT OR IGNORE keeps it idempotent and
respects later revocations.
Migration body moves to db/migrations.ts so tests can apply it to a
fresh in-memory handle without opening the prod DB on import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Propose a shared substrate for per-user prefs, contexts, per-key
consents, and per-agent state so adding an agent stays a manifest
change. Updates CLAUDE.md, README, and architecture docs to reflect
the multi-agent pipeline (ADR-0013) and the registry direction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused `httpx` import from bench.ts (package does not exist)
- Add explicit `IRouter` type on `router` in agent-outputs.ts and bench.ts
to resolve TS2742 portable-type errors
- Remove `docs` from .dockerignore so Dockerfile.admin can copy it into
the runner image (DOCS_ROOT=/app/docs is read at runtime by the admin)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes shadowPolicies map, getShadowPolicies, setPolicyActive from
recommender.ts; removes /api/admin/policies routes from admin.ts; removes
getPolicies, togglePolicy, PolicyInfo from admin api.ts; removes the
policy toggle section from the ops page.
168 API tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /recommend now calls ml/serving /recommend with pre-computed agent
snippets + task context instead of /generate + /score/egreedy/v2. Falls
back to a random signal candidate when ml/serving is unavailable.
Removes: remotePolicy, fetchLlmCandidates, sendRewardWithRetry,
candidateCache, pickPromptVersion. Feedback handler keeps inferReward +
tipFeedback writes for observability; reward delivery to the bandit is gone.
tipScores.policy is now 'orchestrator'; promptVersion is 'v4-orchestrator'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>