Step 4 — /api/profile read-through API:
GET /api/profile → { user, prefs, consents, contexts }
PATCH /api/profile/prefs/:scope upsert user_preferences (source='user')
PATCH /api/profile/consents grant / revoke consent keys
PATCH /api/profile/contexts create / activate / deactivate contexts
Legacy consentGiven bit folded in as data:core fallback.
Step 5 — registry-driven eligibility filter:
fetchRegistry() exported from agent-registry.ts.
profile/eligibility.ts: getEligibleAgentIds(userId) — filters by required
consents, silenced_in_contexts, and user_preferences[enabled=false].
fetchOrchestratorTip filters agent_outputs to eligible set before calling
ml/serving /recommend. Fail-closed: registry unavailable → empty set.
Step 6 — shared context-inference framework (#111) + time-of-day proof (#112):
ml/agents/inference/: UserHistory, FeedbackEvent, run_inference().
Framework: cold-start, min_history gating, error fallback, structured logs.
TimeOfDayAgent v1.1.0: inferred_params=[preferred_hour]; also reads
quiet_start/quiet_end from agent_prefs. agent_prefs injected by TS caller.
AgentInput gains agent_prefs field.
ml/serving: POST /agents/{agent_id}/infer endpoint.
agent-outputs.ts computeAndStore: loads prefs before compute, calls /infer
after, persists results (source='inferred'); user overrides never touched.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
services/api
Express BFF that serves all client-facing routes, manages sessions, runs background signal sync, and proxies admin calls to ml/serving.
Contract
GET /health { ok: true }
POST /api/auth/login → redirect to Google OAuth
GET /api/auth/callback OAuth return URL
POST /api/auth/logout
GET /api/auth/session → { user? }
POST /api/auth/token { token } → set sid cookie (ADMIN_TOKEN auth)
GET /api/integrations list connected integrations
POST /api/integrations/todoist/connect start Todoist OAuth
GET /api/integrations/todoist/callback
DELETE /api/integrations/:provider disconnect
POST /api/recommend → { tip }
POST /api/tip/:id/feedback { action } → { ok }
GET /api/user/profile
DELETE /api/user account deletion
POST /api/push/subscribe
DELETE /api/push/subscribe
GET /api/admin/stats DAU/WAU, feedback breakdown
GET /api/admin/users user list with pagination
GET /api/user/:id user detail, consents, integrations
GET /api/admin/events recent event stream (ring buffer or NATS JetStream)
GET /api/admin/events/history historical event query (time range, filters)
GET /api/admin/sim/runs offline sim run list
POST /api/admin/sim/run launch offline sim with policy/judge params
GET /api/admin/sim/runs/:id/output tail sim stdout
GET /api/admin/features/:userId per-user profile features + freshness
GET /api/admin/features/:userId/context context features for last score call
POST /api/admin/policies list shadow policies + active policy
POST /api/admin/policies/:name/toggle enable/disable shadow policy
POST /api/admin/users/:id/actions revoke-integration, reset-bandit, rebuild-profile
GET /api/admin/health system health: api, ml/serving, db, bus, mlflow
GET /api/admin/docs admin documentation index
GET /api/ml/* admin-only proxy to ml/serving
Middleware stack (request order)
cors— origin limited toWEB_BASE_URLtracingMiddleware— reads or generates W3Ctraceparent; setsreq.traceId+req.traceparentpinoHttp— structured JSON request/response logs withtraceIdfield;/healthsuppressedexpress.json()/cookieParsersessionMiddleware— validatessidcookie, attachesreq.userId
Observability
Logs are structured JSON via pino. Every line includes traceId (extracted from the incoming W3C traceparent header, or generated fresh). The same traceparent is forwarded on all outbound HTTP calls to ml/serving so traces correlate end-to-end.
Sentry error capture is active when SENTRY_DSN is set.
Background tasks
- Todoist sync scheduler — runs every
TODOIST_SYNC_INTERVAL_MS(default 15 min); starts 10 s after boot to avoid startup surge. - Retention purge — deletes
tipScoresandtipFeedbackrows older than 30 days; runs on boot and daily. - Profile TTL invalidation — listens to
signals.task.syncedandsignals.tip.feedbackon the in-process Bus; invalidates cached user-level profile features so the next/recommendgets fresh values.
Config
| Env var | Default | Description |
|---|---|---|
PORT |
3001 |
Listen port |
NODE_ENV |
development |
Environment label |
DATABASE_PATH |
./data/oo.db |
SQLite file |
SESSION_SECRET |
required | Cookie signing secret |
GOOGLE_CLIENT_ID/SECRET |
required | OAuth |
TODOIST_CLIENT_ID/SECRET |
required | OAuth |
API_BASE_URL |
http://localhost:3001 |
Self-referential redirect URI |
WEB_BASE_URL |
http://localhost:3000 |
CORS + post-login redirect |
ML_SERVING_URL |
http://localhost:8000 |
ml/serving base URL |
NATS_URL |
`` | NATS broker; empty = in-process bus only |
TODOIST_SYNC_INTERVAL_MS |
900000 |
Background sync cadence |
TIP_PROMPT_VERSION |
`` | Prompt variant(s) for /generate |
LOG_LEVEL |
info |
pino log level |
SENTRY_DSN |
`` | Sentry DSN; empty = Sentry disabled |
VAPID_* |
Web push keys | |
ADMIN_TOKEN |
`` | Static token for service/Playwright admin auth; empty = disabled |
Health story
GET /health returns { ok: true }. No dependency checks — upstream deps (ml/serving, NATS) have their own health endpoints checked separately.
Extraction criteria
Extract to its own host when:
- Auth session management needs a dedicated Redis/PG session store, or
- Background sync load (Todoist, future connectors) displaces API serving on the shared host, or
- Team boundary emerges between auth/BFF and recommender orchestration.