feat(jarvis): add jarvis-agents, jarvis-sessions, jarvis-memory modules (63 tests)

This commit is contained in:
saravanakumardb1 2026-03-01 07:09:12 -08:00
parent a0157211f5
commit dcabe46de2
56 changed files with 4596 additions and 541 deletions

View File

@ -1,9 +1,9 @@
Last refresh: 2026-02-28T10:22:33Z (2026-02-28 02:22:33 PST)
Cascade conversations: 50 (548M)
Memories: 48
Last refresh: 2026-03-01T07:00:13Z (2026-02-28 23:00:13 PST)
Cascade conversations: 50 (495M)
Memories: 56
Implicit context: 20
Code tracker dirs: 196
File edit history: 1868 entries
Code tracker dirs: 182
File edit history: 2010 entries
Workspace storage: 28 workspaces
Repo docs: 13 files across 3 repos
Repo workflows: 23 files across 4 repos
Repo docs: 14 files across 3 repos
Repo workflows: 28 files across 5 repos

View File

@ -0,0 +1,239 @@
# Auth Cross-Product Analysis — Full Workspace Audit
> **Date:** 2026-02-28
> **Scope:** All 4 product repos + common platform
> **Question:** Do all apps share the same auth? Can a ChronoMind user sign in to NomGap? What's missing?
---
## 1. Backend Architecture (Single Source of Truth)
All products share **one** platform-service (port 4003) in `learning_ai_common_plat`.
### Auth endpoints available:
| Endpoint | Status | Notes |
| -------------------------------- | -------------- | --------------------------------------------------- |
| `POST /auth/login` | ✅ Implemented | Requires `{ email, password, productId }` |
| `POST /auth/register` | ✅ Implemented | Creates user + subscription + license |
| `POST /auth/refresh` | ✅ Implemented | Exchanges refresh token for new pair |
| `GET /auth/me` | ✅ Implemented | Returns user from Bearer token |
| `PUT /auth/profile` | ✅ Implemented | Self-service profile update |
| `POST /auth/sso` | ✅ Implemented | Microsoft/Google OAuth (find-or-create) |
| `POST /auth/verify` | ✅ Implemented | Service-to-service token check |
| `POST /auth/forgot-password` | ✅ Implemented | Generates reset token (logs it, no email sent) |
| `POST /auth/reset-password` | ✅ Implemented | Resets password with token |
| `POST /auth/verify-email` | ✅ Implemented | Verifies email with token |
| `POST /auth/resend-verification` | ✅ Implemented | Resends verification email (logs it, no email sent) |
| Admin CRUD (`/auth/users/*`) | ✅ Implemented | List, count, get, update, delete |
### Database: Single Cosmos DB
- **Container:** `users` — all users across all products
- **Partition key:** user `id`
- **Product isolation:** Every user doc has a `productId` field
- **Lookup:** `getByEmail(email, productId)` — queries by BOTH email AND productId
### JWT tokens
- **Issuer:** `bytelyst-platform`
- **Access token:** 1 hour, contains `{ sub, email, role, productId, plan }`
- **Refresh token:** 7 days, contains `{ sub, productId }`
- **Secret:** Single shared `JWT_SECRET` env var
---
## 2. The Cross-Product Sign-In Question
### Current design: Users are **per-product**
The `getByEmail()` function queries:
```sql
SELECT * FROM c WHERE c.productId = @productId AND c.email = @email
```
This means:
- **A user who registers on ChronoMind (productId: `chronomind`) is a DIFFERENT user than the same email on NomGap (productId: `nomgap`)**
- Same email can have separate accounts with different passwords on each product
- Each registration creates a separate subscription + license record per product
- JWT tokens are scoped to a productId — a ChronoMind token cannot be used for NomGap API calls
### Is this the right design?
**Yes, for now.** Here's why:
1. **Different products = different plans/subscriptions** — A user might be on Pro for ChronoMind but Free for NomGap
2. **Clean data isolation** — each product's user data doesn't leak across
3. **Independent license/device management** — device limits are per-product
4. **Simpler admin** — admin dashboard shows users per product
### Future consideration: ByteLyst Account (cross-product SSO)
If/when you want "sign in once, use all ByteLyst apps":
- Add a `byteLystAccountId` linking field to user docs
- Add a `/auth/link-account` endpoint
- This is a P3 feature, not needed now
---
## 3. Per-App Auth Inventory
### Legend
- ✅ = Implemented and working
- ⚠️ = Partially implemented (missing features)
- ❌ = Not implemented
### 3.1 LysnrAI (`learning_voice_ai_agent`)
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
| --------------------------- | ----- | --------------- | ---------------- | --------------- | ------------ | ----------------------- |
| **User Dashboard (web)** | ✅ | ✅ | ✅ (cookie) | ❌ | ❌ | ✅ (Google + Microsoft) |
| **Admin Dashboard (web)** | ✅ | ❌ (admin-only) | ✅ (cookie) | ❌ | ❌ | ❌ |
| **Tracker Dashboard (web)** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **iOS mobile** | ✅ | ✅ | ✅ (Keychain) | ❌ | ❌ | ✅ (Apple, Google) |
| **Android mobile** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ✅ (Google) |
| **Desktop (Python)** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
**productId:** `lysnrai`
### 3.2 ChronoMind (`learning_ai_clock`)
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
| ----------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- |
| **Web PWA** | ✅ | ✅ | ❌ (no auto-refresh) | ❌ | ❌ | ❌ |
| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ |
| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ |
**productId:** `chronomind`
### 3.3 NomGap (`learning_ai_fastgap`)
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
| ----------------------- | ---------- | ---------- | ----------------- | --------------- | ------------ | --- |
| **React Native (Expo)** | ✅ (store) | ✅ (store) | ⚠️ (hydrate only) | ❌ | ❌ | ❌ |
**productId:** `nomgap`
**Note:** Auth store actions + ProfileScreen UI are wired. `hydrateFromToken()` calls `/auth/me` but there's no proactive refresh timer. No dedicated login screen — auth is inline in ProfileScreen.
### 3.4 MindLyst (`learning_multimodal_memory_agents`)
| Surface | Login | Register | Refresh | Forgot Password | Email Verify | SSO |
| ----------------- | ----- | -------- | -------------------------- | --------------- | ------------ | --- |
| **Web (Next.js)** | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **iOS** | ✅ | ✅ | ✅ (Keychain, 45min timer) | ❌ | ❌ | ❌ |
| **Android** | ✅ | ✅ | ✅ (SharedPrefs) | ❌ | ❌ | ❌ |
**productId:** `mindlyst`
**Note:** MindLyst web has NO auth at all — API routes use in-memory fallback or direct Cosmos, no platform-service integration.
### 3.5 Dashboards (common platform)
| Dashboard | Login | Register | Refresh | Forgot Password | SSO |
| ----------------------- | ----- | -------- | ------- | --------------- | --- |
| **Admin (port 3001)** | ✅ | ❌ | ✅ | ❌ | ❌ |
| **Tracker (port 3003)** | ✅ | ✅ | ✅ | ❌ | ❌ |
---
## 4. Gaps — Prioritized Action List
### P0: Critical (all users hit these)
| # | Gap | Affected | Fix |
| ------ | ------------------------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **G1** | **No "Forgot Password" UI anywhere** | ALL 4 products, ALL surfaces | Backend endpoints exist (`/auth/forgot-password`, `/auth/reset-password`) but ZERO clients call them. Need: forgot password form + reset password page in every app. |
| **G2** | **No email delivery for password reset / email verification** | ALL | Backend generates tokens but only LOGS them (`req.log.info`). The `TODO: Send email via delivery module` comment is still there. Need: wire delivery module (SendGrid/SES) or at minimum an SMTP transport. |
| **G3** | **MindLyst web has NO auth** | MindLyst web | Web dashboard has no login/register at all. API routes bypass platform-service entirely. Need: add auth flow matching ChronoMind web pattern. |
### P1: Important (poor UX without these)
| # | Gap | Affected | Fix |
| ------ | ----------------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **G4** | **No email verification UI** | ALL | Backend has `/auth/verify-email` + `/auth/resend-verification` but no client calls them. Users register with `emailVerified: false` and it's never checked/enforced. |
| **G5** | **ChronoMind web missing token refresh** | ChronoMind web | Web stores token in localStorage but never refreshes it. After 1 hour the token expires silently. Need: add refresh logic (like the iOS 45min timer). |
| **G6** | **NomGap missing proactive token refresh** | NomGap mobile | `hydrateFromToken()` calls `/auth/me` on startup but there's no periodic refresh. Token expires after 1 hour. Need: add refresh timer or intercept 401s. |
| **G7** | **No "Change Password" in any settings screen** | ALL | Users can only reset password via forgot-password flow (which doesn't work yet per G2). Need: `PUT /auth/profile` or new endpoint for authenticated password change. |
### P2: Consistency (works but inconsistent)
| # | Gap | Affected | Fix |
| ------- | --------------------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **G8** | **Password validation inconsistent across clients** | ALL | Backend requires `min(8)`. iOS/Android enforce 8+ chars, uppercase, lowercase, digit. ChronoMind web has no client-side validation. NomGap ProfileScreen has no validation. Standardize. |
| **G9** | **Token storage inconsistent** | Mixed | LysnrAI iOS/Android: Keychain/EncryptedSharedPrefs. ChronoMind: Keychain/plain SharedPrefs. MindLyst: Keychain/plain SharedPrefs. NomGap: MMKV. ChronoMind web: localStorage. Dashboards: httpOnly cookies. Consider standardizing mobile to Keychain + EncryptedSharedPrefs. |
| **G10** | **No SSO on ChronoMind, NomGap, or MindLyst** | 3 products | Only LysnrAI has Google/Microsoft/Apple SSO. Backend supports `/auth/sso`. Could add SSO to other products later. |
| **G11** | **Inconsistent `x-product-id` header** | Various | iOS `PlatformSyncManager` for ChronoMind doesn't send `x-product-id`. Some Android clients send it lowercase, some uppercase. Standardize. |
| **G12** | **No "Delete Account" in any app** | ALL | GDPR/privacy requirement. Backend has `DELETE /auth/users/:id` (admin only). Need: self-service account deletion endpoint + UI. |
### P3: Nice-to-have
| # | Gap | Affected | Fix |
| ------- | -------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------ |
| **G13** | **No cross-product ByteLyst account linking** | Future | If same user uses ChronoMind + NomGap, they have 2 separate accounts. Could add account linking later. |
| **G14** | **No rate limiting on auth endpoints from clients** | ALL | Backend has rate limiting module but clients don't handle 429 gracefully. |
| **G15** | **No biometric auth (FaceID/TouchID) on any mobile app** | iOS/Android | Could add biometric unlock after initial login. |
---
## 5. Architecture Diagram — Current State
```
┌──────────────────────────────────────────────────────────────────┐
│ platform-service (:4003) │
│ │
│ /auth/login ← email + password + productId │
│ /auth/register ← email + password + displayName + productId │
│ /auth/refresh ← refreshToken │
│ /auth/me ← Bearer token │
│ /auth/sso ← email + productId + provider │
│ /auth/forgot-password ← email + productId (⚠️ no email sent) │
│ /auth/reset-password ← token + newPassword (⚠️ no UI calls) │
│ /auth/verify-email ← token (⚠️ no UI calls) │
│ │
│ Cosmos DB: users container (partitioned by id) │
│ ┌─────────────────────────────────────────────┐ │
│ │ { id, productId, email, passwordHash, ... } │ │
│ │ │ │
│ │ productId="lysnrai" → LysnrAI users │ │
│ │ productId="chronomind" → ChronoMind users │ │
│ │ productId="nomgap" → NomGap users │ │
│ │ productId="mindlyst" → MindLyst users │ │
│ └─────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
▲ ▲ ▲ ▲ ▲
│ │ │ │ │
LysnrAI ChronoMind NomGap MindLyst Dashboards
(all 6 (web+iOS (Expo (iOS+ (admin+
surfaces) +Android) RN) Android) tracker)
```
---
## 6. Recommended Fix Order
1. **G2 — Email delivery** (unblocks G1, G4) — Wire SendGrid/SES into platform-service delivery module
2. **G1 — Forgot Password UI** — Add to all apps (once email works)
3. **G3 — MindLyst web auth** — Add auth context + login form
4. **G5 — ChronoMind web token refresh** — Add refresh logic
5. **G6 — NomGap token refresh** — Add refresh timer
6. **G4 — Email verification UI** — Add verification prompt post-register
7. **G7 — Change Password** — Add endpoint + UI in all settings screens
8. **G8 — Password validation** — Standardize client-side rules
9. **G12 — Delete Account** — Self-service endpoint + UI
10. **G9G11** — Consistency cleanup
---
## 7. Summary Answer
> **Q: Can a ChronoMind user sign in directly to NomGap?**
> **A: No.** They must register separately. Each product has its own user namespace (`productId`). Same email = different accounts on different products. This is **by design** — each product has independent plans, subscriptions, and licenses. Cross-product account linking is a future P3 feature.
> **Q: Do all apps use the same backend?**
> **A: Yes.** All products call the same platform-service `/auth/*` endpoints, storing users in the same Cosmos DB `users` container, isolated by `productId`.
> **Q: What's the biggest gap?**
> **A: Password reset doesn't work end-to-end.** The backend endpoints exist but (a) no email delivery is wired, and (b) zero client apps have forgot-password UI. This is the #1 gap to fix.

View File

@ -1,7 +1,7 @@
# Platform Components Roadmap — What's Built, What's Missing, What's Next
> **Status:** Living document — brainstorm + gap analysis
> **Last updated:** 2026-02-17
> **Last updated:** 2026-03-15
> **Scope:** All infrastructure components relevant to admin, DevOps, and product operations across the ByteLyst platform.
> **Repos:** `learning_ai_common_plat` (platform-service, packages) · `learning_voice_ai_agent` (dashboards, clients)
@ -28,7 +28,7 @@
## 1. Current Inventory
### 1.1 Platform-Service Modules (25 modules)
### 1.1 Platform-Service Modules (30 modules)
| Category | Module | Endpoints | Description |
| ------------ | --------------- | --------- | --------------------------------------------------------------------------------------------------- |
@ -57,6 +57,11 @@
| **Ops** | `themes` | 7 routes | Platform theming (iOS, Android, Desktop) |
| **Ops** | `blob` | 5 routes | Azure Blob Storage SAS tokens, list, delete, info |
| **Registry** | `products` | 4 routes | Multi-product registry with full lifecycle (draft → pre_launch → beta → active → sunset → disabled) |
| **Ops** | `jobs` | 5 routes | Scheduled jobs: cron parser, registry, runner, 6 built-in jobs, manual trigger |
| **Ops** | `status` | 6 routes | Public status page: health checker, incidents CRUD, history |
| **Ops** | `delivery` | 6 routes | Transactional email: 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log |
| **Identity** | `auth` (reset) | 4 routes | Password reset (forgot/reset) + email verification (verify/resend) — added to auth module |
| **Infra** | `event-bus` | Singleton | In-memory typed pub/sub via @bytelyst/events — emits on register, password reset, email verified |
### 1.2 Shared Packages (13 packages)
@ -75,12 +80,13 @@
| `@bytelyst/extraction` | Extraction client + shared types |
| `@bytelyst/monitoring` | Health-check utilities |
| `@bytelyst/design-tokens` | Cross-platform token generator (JSON → CSS/TS/Kotlin/Swift) |
| `@bytelyst/events` | Typed in-memory event bus with error isolation (14 tests) |
### 1.3 Services
| Service | Port | Description |
| ---------------------- | ---- | ---------------------------------------------------- |
| **platform-service** | 4003 | Consolidated Fastify service (25 modules, 621 tests) |
| **platform-service** | 4003 | Consolidated Fastify service (30 modules, 988 tests) |
| **extraction-service** | 4005 | LangExtract text extraction + Python sidecar |
| **monitoring** | 4004 | Health-check aggregator (all services) |
@ -104,7 +110,13 @@
| **Feature flags** | ✅ | FNV-1a hash, percentage rollout, admin UI |
| **Client telemetry** | ✅ | All platforms instrumented, admin Client Logs page |
| **Rate limiting** | ✅ | In-memory sliding window + configurable rules per product |
| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); no subscription model, no retry, no HMAC signing |
| **Outbound webhooks** | ⚠️ Partial | Fire-and-forget POST for 3 events (`lib/webhooks.ts`); subscription model built in `modules/webhooks/` with HMAC signing + retry |
| **Event bus** | ✅ | `@bytelyst/events` package + singleton in platform-service; auth emits user.created, password_reset, email_verified |
| **Scheduled jobs** | ✅ | Cron parser, registry, in-process runner, 6 built-in jobs, admin API |
| **Email delivery** | ✅ | 8 templates, renderer, SendGrid/Postmark/console adapters, delivery log, event bus subscribers |
| **Password reset** | ✅ | forgot-password + reset-password endpoints, SHA-256 token hashing, anti-enumeration |
| **Email verification** | ✅ | verify-email + resend-verification endpoints, emailVerified field on UserDoc |
| **Status page** | ✅ | Health checker (3 services), incident management, public + admin endpoints |
| **Kill switch** | ✅ | Per-product, checked by all clients via `/settings/kill-switch` |
| **Audit logging** | ✅ | Records admin actions, queryable from admin dashboard |
| **Blob storage** | ✅ | 6 containers (audio, transcripts, attachments, avatars, releases, backups), SAS tokens, admin endpoints |

View File

@ -11,13 +11,13 @@
## Why Consolidate
| Problem | Impact |
| ---------------------------------------- | ---------------------------------------------- |
| 5 separate Node processes for 2 products | Unnecessary operational overhead |
| 5 ports to manage (40014005) | Complex docker-compose, run scripts, env files |
| 5 separate Cosmos connections | Wasted connection pool resources |
| 5 CI pipelines | Slow feedback, more config to maintain |
| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars |
| Problem | Impact |
|---------|--------|
| 5 separate Node processes for 2 products | Unnecessary operational overhead |
| 5 ports to manage (40014005) | Complex docker-compose, run scripts, env files |
| 5 separate Cosmos connections | Wasted connection pool resources |
| 5 CI pipelines | Slow feedback, more config to maintain |
| 5 config schemas with duplicate env vars | Inconsistent config, easy to miss vars |
**After consolidation:** 2 services — `platform-service` (port 4003) + `extraction-service` (port 4005)
@ -31,12 +31,12 @@
Services export product ID differently — modules reference different names:
| Service | Export Name | Source |
| -------------------- | -------------------- | ---------------------------------------------------------------------------- |
| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` |
| **growth-service** | `PRODUCT_ID` | same as platform ✅ |
| **billing-service** | `PRODUCT_ID` | same as platform ✅ |
| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()`**different name** ⚠️ |
| Service | Export Name | Source |
|---------|-----------|--------|
| **platform-service** | `PRODUCT_ID` | `loadProductIdentity().productId` from `@bytelyst/config` |
| **growth-service** | `PRODUCT_ID` | same as platform ✅ |
| **billing-service** | `PRODUCT_ID` | same as platform ✅ |
| **tracker-service** | `DEFAULT_PRODUCT_ID` | `process.env.DEFAULT_PRODUCT_ID \|\| getProductId()`**different name** ⚠️ |
**Fix:** When merging tracker modules, change all `DEFAULT_PRODUCT_ID` imports to `PRODUCT_ID` in the copied module files, and add `DEFAULT_PRODUCT_ID` env var support to platform-service's `product-config.ts` for backward compat.
@ -44,16 +44,15 @@ Services export product ID differently — modules reference different names:
Platform-service `package.json` is **missing** these deps needed by merged modules:
| Dep | Needed By | Currently In |
| ------------------------------- | ------------------------------------------- | ------------------------------- |
| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service |
| `@bytelyst/auth` (workspace:\*) | tracker modules (`extractAuth`) | tracker-service |
| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service |
| Dep | Needed By | Currently In |
|-----|-----------|-------------|
| `stripe` (^17.5.0) | billing modules (stripe webhooks, checkout) | billing-service, growth-service |
| `@bytelyst/auth` (workspace:*) | tracker modules (`extractAuth`) | tracker-service |
| `@fastify/rate-limit` (^10.3.0) | tracker rate limiting | tracker-service |
### Gap 3: Billing Internal Key Auth (Global Hook)
`billing-service/src/server.ts` has a **global** `onRequest` hook:
```typescript
app.addHook('onRequest', async (req, reply) => {
if (path === '/health' || path.includes('/stripe/webhook')) return;
@ -61,7 +60,6 @@ app.addHook('onRequest', async (req, reply) => {
if (key !== INTERNAL_KEY) reply.code(401).send(...)
});
```
This **cannot** be a global hook after merge — it would block auth, audit, tracker, etc. routes.
**Fix:** Convert to a Fastify plugin registered only on billing route prefixes, or add `x-internal-key` check inside each billing route handler.
@ -69,7 +67,6 @@ This **cannot** be a global hook after merge — it would block auth, audit, tra
### Gap 4: Growth Webhooks Library
`growth-service/src/lib/webhooks.ts` dispatches fire-and-forget HTTP callbacks on invitation redeem. References env vars:
- `WEBHOOK_INVITATION_REDEEMED_URL`
- `WEBHOOK_REFERRAL_STATUS_URL`
@ -85,26 +82,26 @@ Growth-service config requires `STRIPE_SECRET_KEY` as **required** (not optional
**Dashboard API clients (TypeScript):**
| File | Current Env Var | Current Default |
| -------------------------------------------------------------- | --------------------- | ---------------------------------- |
| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline |
| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) |
| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` |
| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` |
| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` |
| File | Current Env Var | Current Default |
|------|----------------|-----------------|
| `admin-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `admin-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
| `user-dashboard-web/src/lib/billing-client.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `user-dashboard-web/src/lib/growth-client.ts` | `GROWTH_SERVICE_URL` | `http://localhost:4001` |
| `user-dashboard-web/src/app/api/stripe/webhook/route.ts` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `admin-dashboard-web/src/app/api/stripe/config/route.ts` | — | `http://localhost:4002` inline |
| `admin-dashboard-web/src/lib/stripe-context.tsx` | — | `http://localhost:4002` (3 places) |
| `tracker-dashboard-web/src/app/api/tracker/[...path]/route.ts` | `TRACKER_API_URL` | `http://localhost:4004` |
| `tracker-dashboard-web/src/app/api/auth/login/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
| `tracker-dashboard-web/src/app/api/auth/me/route.ts` | `PLATFORM_API_URL` | `http://localhost:4003` ✅ |
**Python clients (desktop + backend):**
| File | Current Env Var | Current Default |
| --------------------------------------- | --------------------- | ----------------------- |
| File | Current Env Var | Current Default |
|------|----------------|-----------------|
| `backend/src/clients/billing_client.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `src/cloud/api_sync.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `src/cloud/plan_resolver.py` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`.
@ -115,12 +112,10 @@ All these must change to `PLATFORM_SERVICE_URL` / `http://localhost:4003`.
### Gap 8: Stripe Webhook Test Hardcodes Port
`user-dashboard-web/src/__tests__/stripe-webhook.test.ts` sets:
```typescript
process.env.BILLING_SERVICE_URL = 'http://localhost:4002';
expect(url).toBe('http://localhost:4002/api/stripe/webhook');
```
Must update to port 4003.
### Gap 9: Load Test Scripts
@ -138,7 +133,6 @@ Must update defaults to port 4003.
### Gap 11: LysnrAI Services Stubs
`learning_voice_ai_agent/services/` contains `.env.example` stubs for each service:
- `services/billing-service/.env.example`
- `services/growth-service/.env.example`
- `services/tracker-service/.env.example`
@ -160,7 +154,6 @@ Mobile apps call the Python backend (`localhost:8000`), which calls billing-serv
### Gap 14: Docker Compose `depends_on` for Tracker Dashboard
`learning_voice_ai_agent/docker-compose.yml` has:
```yaml
tracker-dashboard:
depends_on:
@ -169,23 +162,17 @@ tracker-dashboard:
platform-service:
condition: service_started
```
After merge, `tracker-service` container no longer exists. Must change `depends_on` to only `platform-service`.
### Gap 15: Admin Dashboard `docs.ts` Service Directory List
`admin-dashboard-web/src/lib/docs.ts` has a hardcoded list of service directories:
```typescript
const serviceDirs = [
'admin-dashboard-web',
'user-dashboard-web',
'mobile_app',
'services/billing-service',
'services/growth-service',
'admin-dashboard-web', 'user-dashboard-web', 'mobile_app',
'services/billing-service', 'services/growth-service',
];
```
Must update to remove old service names or replace with `services/platform-service`.
### Gap 16: MindLyst Docs Reference Old Services
@ -208,7 +195,6 @@ Platform-service's Dockerfile only copies `services/platform-service/` — it do
### Route Path Collision Check ✅
All services use unique route prefixes — **no collisions**:
- platform: `/auth/*`, `/audit/*`, `/notifications/*`, `/flags/*`, `/ratelimit/*`, `/blob/*`, `/devices/*`
- billing: `/subscriptions/*`, `/usage/*`, `/plans/*`, `/licenses/*`, `/payments/*`, `/stripe/*`
- growth: `/invitations/*`, `/referrals/*`, `/promos/*`
@ -258,12 +244,12 @@ services/
All containers served by one Cosmos client in platform-service:
| Origin | Containers |
| ----------------------- | ----------------------------------------------------------------------------------- |
| Origin | Containers |
|--------|-----------|
| **platform** (existing) | `users`, `audit_log`, `feature_flags`, `notification_devices`, `notification_prefs` |
| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` |
| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` |
| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` |
| **billing** → platform | `subscriptions`, `payments`, `plans`, `licenses`, `usage_daily` |
| **growth** → platform | `invitation_codes`, `referrals`, `promo_codes` |
| **tracker** → platform | `tracker_items`, `tracker_comments`, `tracker_votes` |
---
@ -404,7 +390,7 @@ All containers served by one Cosmos client in platform-service:
- [x] **3.3.1** Created `platform-service/src/lib/auth.ts` re-exporting from `@bytelyst/auth`
- [x] **3.3.2** Copied from tracker-service (identical content)
- [x] **3.3.3** Added `@bytelyst/auth` (workspace:\*) to package.json
- [x] **3.3.3** Added `@bytelyst/auth` (workspace:*) to package.json
- [x] **3.3.4** Added `@fastify/rate-limit` (^10.3.0) to package.json
- [x] **3.3.5** `jose` already in platform ✅
@ -574,30 +560,29 @@ Also fixed: monitoring/health.ts, AI.dev/SKILLS docs, MIGRATION_GUIDE.md [`81609
## Summary
| Phase | What | Effort | Tests Moved | Critical Gaps Addressed |
| --------- | ------------------------------------------- | ------------- | --------------- | ------------------------------------ |
| **0** | Preparation & backup | 30 min | — | — |
| **1** | Merge growth-service (3 modules) | 23 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) |
| **2** | Merge billing-service (5 modules) | 45 hrs | ~11 | Gap 3 (internal key auth) |
| **3** | Merge tracker-service (4 modules) | 34 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) |
| **4** | Update consumers (20+ files across 3 repos) | 45 hrs | — | Gaps 611, 1317 |
| **5** | Documentation & final verification | 23 hrs | — | — |
| **Total** | **5 services → 2** | **~45 days** | **~125+ tests** | **17 gaps addressed** |
| Phase | What | Effort | Tests Moved | Critical Gaps Addressed |
|-------|------|--------|-------------|------------------------|
| **0** | Preparation & backup | 30 min | — | — |
| **1** | Merge growth-service (3 modules) | 23 hrs | ~14 | Gap 4 (webhooks), Gap 5 (Stripe key) |
| **2** | Merge billing-service (5 modules) | 45 hrs | ~11 | Gap 3 (internal key auth) |
| **3** | Merge tracker-service (4 modules) | 34 hrs | ~45 | Gap 1 (product ID), Gap 2 (deps) |
| **4** | Update consumers (20+ files across 3 repos) | 45 hrs | — | Gaps 611, 1317 |
| **5** | Documentation & final verification | 23 hrs | — | — |
| **Total** | **5 services → 2** | **~45 days** | **~125+ tests** | **17 gaps addressed** |
## Port Allocation (After)
| Service | Port |
| -------------------------------------------- | -------- |
| **platform-service** | **4003** |
| **extraction-service** | **4005** |
| extraction-service python sidecar (internal) | 4006 |
| Service | Port |
|---------|------|
| **platform-service** | **4003** |
| **extraction-service** | **4005** |
| extraction-service python sidecar (internal) | 4006 |
Ports 4001, 4002, 4004 freed up.
## Rollback Strategy
Each phase has its own commit. If a phase breaks something:
1. `git revert <commit>` to undo that phase
2. The old service code is in git history
3. Backup branches created in Phase 0
@ -605,13 +590,13 @@ Each phase has its own commit. If a phase breaks something:
## Risks & Mitigations
| Risk | Mitigation |
| ---------------------------------------- | ----------------------------------------------------------------------------- |
| Route path collisions | Verified ✅ — all services use unique prefixes |
| Config schema gets large | Group env vars by domain with clear section comments |
| Stripe webhook raw body | Fastify handles this — verify after move |
| Risk | Mitigation |
|------|-----------|
| Route path collisions | Verified ✅ — all services use unique prefixes |
| Config schema gets large | Group env vars by domain with clear section comments |
| Stripe webhook raw body | Fastify handles this — verify after move |
| Billing internal key blocks other routes | Scoped Fastify plugin (Phase 2.2) isolates key check to billing prefixes only |
| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 |
| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code |
| Stripe webhook test fails | Explicit port update in Phase 4.4 |
| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 |
| Public tracker routes skip auth | Register outside scoped plugins — verify in Phase 3.5.3 |
| Python billing client breaks | Change env var name, keep same API paths — transparent to Python code |
| Stripe webhook test fails | Explicit port update in Phase 4.4 |
| Product ID mismatch | Alias `DEFAULT_PRODUCT_ID = PRODUCT_ID` in Phase 3.2.4 |

View File

@ -9,17 +9,17 @@
## 1. Project → Env File Map
| # | Project | Env File | Port |
| --- | ----------------------------------------------- | ---------------------------------- | ---- |
| 1 | Desktop app (`src/`) | `.env` (root) | — |
| 2 | Backend API (`backend/`) | `backend/.env` | 8000 |
| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 |
| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 |
| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 |
| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 |
| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 |
| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 |
| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 |
| # | Project | Env File | Port |
|---|---------|----------|------|
| 1 | Desktop app (`src/`) | `.env` (root) | — |
| 2 | Backend API (`backend/`) | `backend/.env` | 8000 |
| 3 | Admin Dashboard (`admin-dashboard-web/`) | `admin-dashboard-web/.env.local` | 3001 |
| 4 | User Dashboard (`user-dashboard-web/`) | `user-dashboard-web/.env.local` | 3002 |
| 5 | Tracker Dashboard (`tracker-dashboard-web/`) | `tracker-dashboard-web/.env.local` | 3003 |
| 6 | Billing Service (`services/billing-service/`) | `services/billing-service/.env` | 4002 |
| 7 | Growth Service (`services/growth-service/`) | `services/growth-service/.env` | 4001 |
| 8 | Platform Service (`services/platform-service/`) | `services/platform-service/.env` | 4003 |
| 9 | Tracker Service (`services/tracker-service/`) | `services/tracker-service/.env` | 4004 |
---
@ -36,63 +36,63 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod
### 3.1 Root `.env` (Desktop App)
| Variable | Status | Action |
| --------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview |
| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs |
| Variable | Status | Action |
|----------|--------|--------|
| `APPLICATIONINSIGHTS_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Application Insights → Overview |
| `ANH_CONNECTION_STRING` | ⚠️ Has `YOUR_KEY_HERE` placeholder | Replace with real SharedAccessKey from Azure Portal → Notification Hubs |
### 3.2 `backend/.env`
| Variable | Status | Action |
| ------------------------------- | -------- | ------------------------------------------------------------------------ |
| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys |
| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services |
| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback |
| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback |
| Variable | Status | Action |
|----------|--------|--------|
| `AZURE_EMAIL_CONNECTION_STRING` | ❌ Empty | Get from Azure Portal → Communication Services → Keys |
| `SMTP_HOST` | ❌ Empty | Configure if using SMTP fallback instead of Azure Communication Services |
| `SMTP_USER` | ❌ Empty | Configure if using SMTP fallback |
| `SMTP_PASS` | ❌ Empty | Configure if using SMTP fallback |
### 3.3 `admin-dashboard-web/.env.local`
| Variable | Status | Action |
| -------------------------- | -------- | ---------------------------------------------------------- |
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
| Variable | Status | Action |
|----------|--------|--------|
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | Get from PostHog → Project Settings (optional — analytics) |
### 3.4 `user-dashboard-web/.env.local`
| Variable | Status | Action |
| -------------------------- | -------- | ------------------------------------------------------------------- |
| Variable | Status | Action |
|----------|--------|--------|
| `ENTERPRISE_EMAIL_DOMAINS` | ❌ Empty | Set comma-separated list of domains that qualify for Enterprise SSO |
| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations |
| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above |
| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials |
| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above |
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
| `MICROSOFT_CLIENT_ID` | ❌ Empty | Register app in Azure Portal → Entra ID → App registrations |
| `MICROSOFT_CLIENT_SECRET` | ❌ Empty | Same as above |
| `GOOGLE_CLIENT_ID` | ❌ Empty | Register app in Google Cloud Console → Credentials |
| `GOOGLE_CLIENT_SECRET` | ❌ Empty | Same as above |
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
### 3.5 `tracker-dashboard-web/.env.local`
| Variable | Status | Action |
| -------------------------- | -------- | ---------------------------- |
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
| Variable | Status | Action |
|----------|--------|--------|
| `NEXT_PUBLIC_POSTHOG_KEY` | ❌ Empty | PostHog analytics (optional) |
| `NEXT_PUBLIC_POSTHOG_HOST` | ❌ Empty | PostHog analytics (optional) |
### 3.6 `services/growth-service/.env`
| Variable | Status | Action |
| --------------------------------- | -------- | ------------------------------------------------------------ |
| Variable | Status | Action |
|----------|--------|--------|
| `WEBHOOK_INVITATION_REDEEMED_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
| `WEBHOOK_REFERRAL_STATUS_URL` | ❌ Empty | Set to backend or platform-service webhook callback endpoint |
### 3.7 `services/billing-service/.env`
| Variable | Status | Action |
| ------------------ | -------- | --------------------------------------------------------------- |
| Variable | Status | Action |
|----------|--------|--------|
| `PLAN_LIMITS_JSON` | ❌ Empty | Optional — set JSON with per-plan limits if overriding defaults |
### 3.8 `services/platform-service/.env`
| Variable | Status | Action |
| ------------------------ | -------- | ------------------------------------------------------------------------ |
| Variable | Status | Action |
|----------|--------|--------|
| `RATE_LIMIT_CONFIG_JSON` | ❌ Empty | Optional — set JSON with per-endpoint rate limits if overriding defaults |
### 3.9 `services/tracker-service/.env`
@ -105,21 +105,21 @@ grep -rn 'MISSING_ENV_VALUE' --include='.env*' --include='*.env' . | grep -v nod
These were missing from `.env` files but had known values, so they were filled in:
| Project | Variable | Value Added |
| -------------------------------- | -------------------------- | --------------------------------------- |
| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` |
| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` |
| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` |
| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports |
| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) |
| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) |
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` |
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` |
| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) |
| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` |
| Project | Variable | Value Added |
|---------|----------|-------------|
| Root `.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
| Root `.env` | `LYSNR_API_URL` | `http://localhost:8000` |
| Root `.env` | `LYSNR_ADMIN_URL` | `http://localhost:3001` |
| Root `.env` | `LYSNR_DASHBOARD_URL` | `http://localhost:3002` |
| `backend/.env` | `BILLING_SERVICE_URL` | `http://localhost:4002` |
| `backend/.env` | `PLATFORM_SERVICE_URL` | `http://localhost:4003` |
| `backend/.env` | `CORS_ORIGINS` | Expanded to include all dashboard ports |
| `admin-dashboard-web/.env.local` | `STRIPE_PUBLISHABLE_KEY` | Test key (was missing) |
| `admin-dashboard-web/.env.local` | `STRIPE_WEBHOOK_SECRET` | Test key (was missing) |
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_PRO` | `price_1Szl2z...` |
| `admin-dashboard-web/.env.local` | `STRIPE_PRICE_ENTERPRISE` | `price_1Szl3D...` |
| `user-dashboard-web/.env.local` | `ENTERPRISE_EMAIL_DOMAINS` | Empty (needs config) |
| `services/billing-service/.env` | `USAGE_WARN_THRESHOLD` | `80` |
---
@ -134,14 +134,14 @@ These were missing from `.env` files but had known values, so they were filled i
These values **must be identical** across all services that use them:
| Secret | Used By |
| ------------------- | -------------------------------------------------------------------- |
| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend |
| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop |
| `COSMOS_KEY` | Same as above |
| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) |
| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard |
| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop |
| Secret | Used By |
|--------|---------|
| `JWT_SECRET` | All 4 Fastify services + all 3 dashboards + backend |
| `COSMOS_ENDPOINT` | All 4 Fastify services + admin + user dashboards + backend + desktop |
| `COSMOS_KEY` | Same as above |
| `COSMOS_DATABASE` | Same as above (must be `lysnrai`) |
| `STRIPE_SECRET_KEY` | billing-service, growth-service, admin-dashboard, user-dashboard |
| `AZURE_BLOB_*` | platform-service, admin-dashboard, user-dashboard, desktop |
---

View File

@ -8,60 +8,54 @@
## Key Metrics at a Glance
| Metric | Value | Assessment |
| ---------------------------- | --------------------------------- | ---------------------------------- |
| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output |
| **% new code by Windsurf** | 99% | Near-total AI-assisted development |
| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions |
| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented |
| **Credits used** | 8,069 | Heavy but productive usage |
| **Terminal messages sent** | 5,893 | Heavy command execution |
| **Workflows used** | 35 | Good use of custom workflows |
| **Memories used** | 20 | Context retention across sessions |
| **Web searches** | 9 | Minimal external lookup needed |
| **Previews** | 45 | Regular visual verification |
| **App deploys** | 1 | |
| **Commands used** | 0 | Slash commands not utilized |
| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage |
| Metric | Value | Assessment |
|--------|-------|------------|
| **Lines written by Cascade** | 227,885 (year) / 143,815 (period) | Extremely high output |
| **% new code by Windsurf** | 99% | Near-total AI-assisted development |
| **Cascade conversations** | 21 | ~0.7/day — focused, long sessions |
| **Cascade messages sent** | 1,470 (Write), 71 (Chat) | 95% Write mode — action-oriented |
| **Credits used** | 8,069 | Heavy but productive usage |
| **Terminal messages sent** | 5,893 | Heavy command execution |
| **Workflows used** | 35 | Good use of custom workflows |
| **Memories used** | 20 | Context retention across sessions |
| **Web searches** | 9 | Minimal external lookup needed |
| **Previews** | 45 | Regular visual verification |
| **App deploys** | 1 | |
| **Commands used** | 0 | Slash commands not utilized |
| **Tab acceptances** | 1 (Markdown) | Almost zero autocomplete usage |
---
## Model Distribution
| Model | Usage % | Role |
| ----------------------------------- | ------- | ---------------------------------- |
| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits |
| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture |
| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation |
| **Claude Opus 4.5** | 8.22% | Targeted complex tasks |
| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation |
| Model | Usage % | Role |
|-------|---------|------|
| **GPT-5.2 Low Reasoning** | 50.52% | Bulk code generation, simple edits |
| **Claude Opus 4.5 (Thinking)** | 22.19% | Complex reasoning, architecture |
| **GPT-5.2 Low Reasoning** (variant) | 16.97% | Additional generation |
| **Claude Opus 4.5** | 8.22% | Targeted complex tasks |
| **SWE-1.5 (Promo)** | 2.09% | Trial/evaluation |
---
## Strengths
### 1. Extremely productive output
143,815 lines in 30 days across 21 conversations is exceptional. That's ~6,848 lines per conversation and ~4,794 lines per day. This built out the entire LysnrAI monorepo (6 client apps, 5 backend services, 600+ tests) and the MindLyst KMP foundation.
### 2. Write-heavy workflow (95% Write vs 5% Chat)
1,470 Write messages vs 71 Chat messages shows a highly action-oriented approach — using Cascade primarily for implementation rather than Q&A. This is the most productive usage pattern.
### 3. Heavy terminal integration (5,893 messages)
~4 terminal commands per Cascade message indicates extensive build/test/verify cycles. This is a sign of rigorous development — not just generating code, but continuously validating it.
### 4. Good workflow adoption (35 uses)
Custom workflows for starting services, running tests, building releases, etc. are being used regularly. This reduces repetitive work and ensures consistency.
### 5. Memory utilization (20 memories)
Using persistent memory for project context (architecture decisions, rebranding mappings, service configs) avoids re-explaining context across the 21 conversations.
### 6. Focused activity pattern
The heatmap shows concentrated bursts of activity (DecFeb) rather than scattered usage. This aligns with the LysnrAI monorepo buildout and MindLyst kickoff — deep focused sprints.
---
@ -69,50 +63,40 @@ The heatmap shows concentrated bursts of activity (DecFeb) rather than scatte
## Areas for Improvement
### 1. Tab completions nearly unused (1 acceptance)
**Current:** Only 1 Markdown tab acceptance in the entire period.
**Recommendation:** Enable and use Windsurf's inline tab completions for:
- Boilerplate code (imports, function signatures, test setup)
- Repetitive patterns (Cosmos CRUD, Fastify route scaffolds)
- Variable/method name completion
This alone could save significant keystroke overhead for the ~1% of code you write manually.
This alone could save significant keystroke overhead for the ~1% of code you write manually.
### 2. Zero slash commands used
**Current:** 0 commands used despite having 10+ custom workflows defined.
**Recommendation:** Use slash commands (e.g., `/start-all-services`, `/debug-service`, `/test-ios-app`) directly in chat to trigger workflows. Currently, workflows are used (35 times) but commands are at 0 — this suggests workflows are being triggered via other means or the slash command integration isn't configured.
### 3. Chat mode underutilized (5%)
**Current:** 71 Chat messages vs 1,470 Write messages.
**Recommendation:** Use Chat mode for:
- **Architecture discussions** before implementing (e.g., "should I use KMP or native for mobile?")
- **Code review** — paste code and ask for review before committing
- **Debugging strategy** — discuss approach before diving into Write mode
A healthy ratio might be 80/20 Write/Chat rather than 95/5.
A healthy ratio might be 80/20 Write/Chat rather than 95/5.
### 4. Web search barely used (9 searches)
**Current:** Only 9 web searches in 30 days.
**Recommendation:** Use web search for:
- Checking latest API docs (Azure Speech SDK, Stripe, Fastify 5)
- Verifying deprecation notices before adopting patterns
- Finding community solutions for edge cases
This would reduce reliance on training data which may be outdated.
This would reduce reliance on training data which may be outdated.
### 5. MCP integrations not used (0 invocations)
**Current:** No MCP (Model Context Protocol) tool invocations.
**Recommendation:** If you have MCP servers configured (e.g., for Azure, GitHub, Cosmos DB), using them could provide real-time data access during development sessions.
### 6. Model selection could be more strategic
**Current:** 50% GPT-5.2 Low Reasoning + 22% Claude Opus 4.5 Thinking.
**Recommendation:**
- Use **Claude Opus 4.5 (Thinking)** for: architecture decisions, complex debugging, multi-file refactors, security reviews
- Use **GPT-5.2** for: bulk code generation, simple CRUD, test writing, documentation
- The current 50/22 split seems reasonable, but consider bumping Claude usage for critical paths (auth, billing, data integrity) where reasoning depth matters more
@ -121,16 +105,16 @@ The heatmap shows concentrated bursts of activity (DecFeb) rather than scatte
## Usage Efficiency Score
| Category | Score | Notes |
| ------------------------ | -------- | ------------------------------------------------ |
| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary |
| **Write/Chat balance** | 7/10 | Could use more Chat for planning |
| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification |
| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 |
| **Tab completions** | 1/10 | Nearly unused — biggest improvement area |
| **Memory usage** | 7/10 | 20 memories is adequate; could store more |
| **Web search** | 4/10 | 9 searches is very low for this volume |
| **Overall** | **7/10** | Highly productive; optimize completions + search |
| Category | Score | Notes |
|----------|-------|-------|
| **Output volume** | 10/10 | 143K lines in 30 days is extraordinary |
| **Write/Chat balance** | 7/10 | Could use more Chat for planning |
| **Terminal integration** | 10/10 | 5,893 commands shows rigorous verification |
| **Workflow adoption** | 8/10 | 35 uses is good; slash commands at 0 |
| **Tab completions** | 1/10 | Nearly unused — biggest improvement area |
| **Memory usage** | 7/10 | 20 memories is adequate; could store more |
| **Web search** | 4/10 | 9 searches is very low for this volume |
| **Overall** | **7/10** | Highly productive; optimize completions + search |
---
@ -147,7 +131,6 @@ The heatmap shows concentrated bursts of activity (DecFeb) rather than scatte
## What You Built in This Period
With these 21 conversations and 143K lines, you shipped:
- Complete LysnrAI monorepo (347 commits, 6 apps, 5 services, 600+ tests)
- MindLyst KMP foundation (shared module, 3 platform UIs, design system)
- Full microservices extraction (6 phases)

View File

@ -76,3 +76,4 @@ Global toggles:
- Add per-user quotas (requires auth).
- Add SSRF protection to `/api/triage` URL enrichment (block localhost/private IPs, limit redirects, cap response bytes).
- Add structured request validation (zod) and centralize API middleware.

View File

@ -104,3 +104,4 @@ bash scripts/secret-scan-repo.sh
bash scripts/check.sh
make check
```

View File

@ -23,7 +23,6 @@ npm run test
### 2. Build the TypeScript
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
npx tsc
@ -32,7 +31,6 @@ npx tsc
### 3. Bundle into single JS file (for SEA)
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
npx esbuild dist/main.js --bundle --platform=node --outfile=dist/agent-bundle.js --external:sharp --external:better-sqlite3
@ -152,7 +150,6 @@ jobs:
### 8. Verify the binary
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_agent_monitoring_fx/apps/agent
./dist/agentlens-macos --server http://localhost:3001 --help 2>&1 || echo "(expected: agent starts or shows usage)"

View File

@ -0,0 +1,53 @@
---
description: Backup main branches then push all repos to origin in sequence
---
# Backup & Push All Repos
Combines `/repo_backup-main-branch` and `/repo_push-repos` into a single sequential workflow. Ideal for end-of-session save-all.
## Step 1: Backup main branches
Creates timestamped backup branches with smart duplicate detection.
// turbo
Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory
## Step 2: Push all repos to origin
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ Pushing $repo ━━━"
(cd ~/code/mygh/$repo && git push origin main 2>&1)
done
echo ""
echo "✨ All repos pushed!"
```
## What it does:
1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates
2. **Push** — pushes `main` to `origin/main` for all 5 repos
## Repositories:
- learning_ai_common_plat
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:
- End of a work session
- Before switching machines
- After a batch of commits across repos
- Anytime you want a safe checkpoint + sync to remote
## Notes:
- Backup runs first so the backup branch includes the latest local commits
- Push only pushes `main` — backup branches are pushed by the backup script itself
- If push fails (diverged remote), run `/repo_sync-repos` first to pull

View File

@ -0,0 +1,34 @@
---
description: Smart backup of main branches with duplicate detection
---
# Backup Main Branch
Creates smart backups of main branches across all repositories.
// turbo
Run `bash ~/code/mygh/learning_ai_common_plat/scripts/backup-main.sh` from any directory
## What it does:
1. Checks each repository for changes
2. Skips backup if main hasn't changed since last backup
3. Creates timestamped backup branch
4. Cleans up old backups (keeps 7 days)
5. Returns to main branch
## Repositories covered:
- learning_ai_common_plat
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## Features:
- Smart duplicate detection
- Automatic cleanup of old backups
- Multi-repo support
- Safe operations (always returns to main)
- Color-coded output for clarity

View File

@ -0,0 +1,50 @@
---
description: Commit all workspace changes in logical order with intelligent messages
---
# Commit Workspace
Commits all pending changes across all 5 workspace repos in logical order.
## Step 1: Check status of all repos
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ $repo ━━━"
(cd ~/code/mygh/$repo && git status --short)
echo ""
done
```
## Step 2: Stage and commit each repo
For each repo with changes:
1. Review the diff
2. Stage all changes with `git add -A`
3. Write a conventional commit message: `type(scope): description`
4. Commit
## Commit message conventions:
- `feat(scope):` — new feature
- `fix(scope):` — bug fix
- `refactor(scope):` — code restructuring
- `docs(scope):` — documentation only
- `test(scope):` — adding/updating tests
- `chore(scope):` — maintenance tasks
## Order:
1. **learning_ai_common_plat** — shared packages first (others may depend on it)
2. **learning_voice_ai_agent** — LysnrAI product repo
3. **learning_multimodal_memory_agents** — MindLyst product repo
4. **learning_ai_clock** — ChronoMind product repo
5. **learning_ai_fastgap** — NomGap product repo
## Notes:
- Always commit common_plat first since other repos may reference its packages
- Use `--no-verify` only if pre-commit hooks are blocking on non-code issues
- After committing, run `/repo_push-repos` to push all to origin

View File

@ -0,0 +1,42 @@
---
description: Push local main branch to origin for all 5 workspace repos
---
# Push Repos
Pushes local `main` to `origin/main` for all workspace repositories.
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ $repo ━━━"
(cd ~/code/mygh/$repo && git push origin main)
done
```
## What it does:
1. Iterates over all 5 workspace repos
2. Runs `git push origin main` in each
3. Fails fast if a repo has diverged from remote (resolve with rebase manually)
## Repositories:
- learning_ai_common_plat
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:
- After committing a batch of changes locally
- After running `/repo_commit-workspace`
- To sync local work to GitHub before switching machines
## Notes:
- Only pushes `main` — does not push other branches
- Will fail safely if remote has diverged — run `/repo_sync-repos` first then rebase
- Use `/repo_sync-repos` to pull before pushing if you've been working on another machine

View File

@ -0,0 +1,42 @@
---
description: Pull latest from origin main across all 5 workspace repos
---
# Sync Repos
Pulls the latest changes from `origin/main` for all workspace repositories.
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ $repo ━━━"
(cd ~/code/mygh/$repo && git pull --ff-only origin main)
done
```
## What it does:
1. Iterates over all 5 workspace repos
2. Runs `git pull --ff-only origin main` in each
3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case)
## Repositories:
- learning_ai_common_plat
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:
- Starting a new work session
- After pushing changes from another machine
- Before running `/repo_backup-main-branch`
## Notes:
- Uses `--ff-only` to prevent accidental merge commits
- If a repo has uncommitted changes, `git pull` will still work (fast-forward only)
- If a repo has diverged from origin, the pull will fail safely — resolve manually

View File

@ -18,7 +18,7 @@ Run `bash scripts/backup-main.sh` from any repository root
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ Pushing $repo ━━━"
(cd ~/code/mygh/$repo && git push origin main 2>&1)
done
@ -29,7 +29,7 @@ echo "✨ All repos pushed!"
## What it does:
1. **Backup** — creates timestamped backup branches, cleans up old ones (7 days), skips duplicates
2. **Push** — pushes `main` to `origin/main` for all 3 repos
2. **Push** — pushes `main` to `origin/main` for all 5 repos
## Repositories:
@ -37,6 +37,7 @@ echo "✨ All repos pushed!"
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:

View File

@ -23,6 +23,7 @@ Run `bash scripts/backup-main.sh` from any repository root
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## Features:

View File

@ -1,5 +1,5 @@
---
description: Push local main branch to origin for all 4 workspace repos
description: Push local main branch to origin for all 5 workspace repos
---
# Push Repos
@ -9,7 +9,7 @@ Pushes local `main` to `origin/main` for all workspace repositories.
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ $repo ━━━"
(cd ~/code/mygh/$repo && git push origin main)
done
@ -17,7 +17,7 @@ done
## What it does:
1. Iterates over all 4 workspace repos
1. Iterates over all 5 workspace repos
2. Runs `git push origin main` in each
3. Fails fast if a repo has diverged from remote (resolve with rebase manually)
@ -27,6 +27,7 @@ done
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:

View File

@ -1,5 +1,5 @@
---
description: Pull latest from origin main across all 4 workspace repos
description: Pull latest from origin main across all 5 workspace repos
---
# Sync Repos
@ -9,7 +9,7 @@ Pulls the latest changes from `origin/main` for all workspace repositories.
// turbo
```bash
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock; do
for repo in learning_ai_common_plat learning_voice_ai_agent learning_multimodal_memory_agents learning_ai_clock learning_ai_fastgap; do
echo "━━━ $repo ━━━"
(cd ~/code/mygh/$repo && git pull --ff-only origin main)
done
@ -17,7 +17,7 @@ done
## What it does:
1. Iterates over all 4 workspace repos
1. Iterates over all 5 workspace repos
2. Runs `git pull --ff-only origin main` in each
3. Fails fast if there are local divergent commits (use `git pull --rebase` manually in that case)
@ -27,6 +27,7 @@ done
- learning_voice_ai_agent
- learning_multimodal_memory_agents
- learning_ai_clock
- learning_ai_fastgap
## When to use:

View File

@ -14,7 +14,6 @@ description: Verify iOS/mobile code compiles, all files are in Xcode targets, an
### Step 1.1 — Verify git clean
// turbo
```bash
cd $HOME/code/mygh/learning_multimodal_memory_agents && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes"
```
@ -39,7 +38,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradle
### Step 1.4 — Web build check
// turbo
```bash
cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx next build 2>&1 | tail -10
```
@ -51,7 +49,6 @@ cd $HOME/code/mygh/learning_multimodal_memory_agents/mindlyst-native/web && npx
### Step 2.1 — Verify git clean
// turbo
```bash
cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cached --quiet && echo "Clean" || echo "WARNING: uncommitted changes"
```
@ -59,7 +56,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && git diff --quiet && git diff --cac
### Step 2.2 — Install CocoaPods
// turbo
```bash
cd $HOME/code/mygh/learning_voice_ai_agent/mobile_app/ios && pod install
```
@ -85,7 +81,6 @@ cd $HOME/code/mygh/learning_voice_ai_agent && xcodebuild build \
### Step 2.4 — Verify pbxproj ↔ filesystem consistency
// turbo
```bash
cd $HOME/code/mygh/learning_voice_ai_agent && \
echo "=== Checking LysnrKeyboard ===" && \
@ -111,7 +106,6 @@ else echo "$missing file(s) may be missing"; fi
### Step 2.5 — Check for common Swift issues
// turbo
```bash
cd $HOME/code/mygh/learning_voice_ai_agent && \
echo "=== Hardcoded secrets ===" && \
@ -126,7 +120,6 @@ echo "Keyboard extension: $count print() calls (should be 0)"
### Step 2.6 — Verify Info.plist + entitlements
// turbo
```bash
cd $HOME/code/mygh/learning_voice_ai_agent && \
for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do
@ -143,32 +136,32 @@ else echo "ERROR: App Group entitlement MISSING"; fi
### What This Catches
| Check | Prevents |
| --------------------- | -------------------------------------------------- |
| KMP compile | Shared business logic errors |
| Build all iOS targets | Missing source files in Xcode project |
| pbxproj consistency | New .swift files on disk but not in target |
| Secret scan | Hardcoded API keys in Swift source |
| print() check | Debug logging in production keyboard extension |
| Info.plist check | Missing privacy descriptions → App Store rejection |
| Entitlements check | Missing App Group → keyboard can't share data |
| Check | Prevents |
|-------|----------|
| KMP compile | Shared business logic errors |
| Build all iOS targets | Missing source files in Xcode project |
| pbxproj consistency | New .swift files on disk but not in target |
| Secret scan | Hardcoded API keys in Swift source |
| print() check | Debug logging in production keyboard extension |
| Info.plist check | Missing privacy descriptions → App Store rejection |
| Entitlements check | Missing App Group → keyboard can't share data |
### Key Files
| File | Purpose |
| ----------------------------------------------------------------------------- | -------------------------- |
| `mindlyst-native/shared/` | KMP shared business logic |
| `mindlyst-native/web/` | Next.js web dashboard |
| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config |
| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps |
| File | Purpose |
|------|---------|
| `mindlyst-native/shared/` | KMP shared business logic |
| `mindlyst-native/web/` | Next.js web dashboard |
| `../learning_voice_ai_agent/mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project config |
| `../learning_voice_ai_agent/mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
| `../learning_voice_ai_agent/mobile_app/ios/Podfile` | CocoaPods deps |
### Troubleshooting
| Problem | Fix |
| ------------------------- | ------------------------------------------------------------------------ |
| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts |
| Xcode build fails | Use `.xcworkspace`, run `pod install` first |
| Problem | Fix |
|---------|-----|
| KMP compile fails | Check `gradle/libs.versions.toml` for version conflicts |
| Xcode build fails | Use `.xcworkspace`, run `pod install` first |
| File missing from pbxproj | Add to PBXFileReference + PBXGroup + PBXBuildFile + PBXSourcesBuildPhase |
| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps |
| Simulator not found | Run `xcrun simctl list devices` to see available simulators |
| Android SDK missing | Set `sdk.dir` in `local.properties` or skip Android steps |
| Simulator not found | Run `xcrun simctl list devices` to see available simulators |

View File

@ -49,7 +49,6 @@ See `iosApp/README_SETUP.md` for more details.
### 4. Verify Gradle/KMP builds
// turbo
```bash
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
```
@ -67,7 +66,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosArm64
```
For simulator testing:
```bash
cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
```
@ -75,7 +73,6 @@ cd mindlyst-native && ./gradlew :shared:compileKotlinIosSimulatorArm64
### 2. Embed the shared framework in Xcode
If not already linked:
```bash
cd mindlyst-native && ./gradlew :shared:embedAndSignAppleFrameworkForXcode
```
@ -90,7 +87,6 @@ Or manually: open Xcode → Target → General → Build.
### 4. Clean build folder
// turbo
```bash
xcodebuild clean -project mindlyst-native/MindLyst.xcodeproj -scheme MindLyst -configuration Release 2>&1 | tail -3
```
@ -144,24 +140,24 @@ The `app-store-connect` export method auto-uploads the IPA.
### Key Paths
| Path | Purpose |
| ------------------------------------------- | ---------------------------------------- |
| `mindlyst-native/iosApp/` | Swift UI source files |
| `mindlyst-native/shared/` | KMP shared module (business logic) |
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
| Path | Purpose |
|------|---------|
| `mindlyst-native/iosApp/` | Swift UI source files |
| `mindlyst-native/shared/` | KMP shared module (business logic) |
| `mindlyst-native/shared/build.gradle.kts` | KMP build config (iOS targets) |
| `mindlyst-native/gradle/libs.versions.toml` | Version catalog |
| `mindlyst-native/MindLyst.xcodeproj/` | Xcode project (must be created manually) |
| `scripts/MindLystExportOptions.plist` | Export options for TestFlight upload |
### Build Identity
| Field | Value |
| ------------- | ----------------------- |
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.mindlyst.MindLyst` |
| Signing | Automatic |
| KMP Framework | `shared` (static) |
| Min iOS | 16.0 |
| Field | Value |
|-------|-------|
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.mindlyst.MindLyst` |
| Signing | Automatic |
| KMP Framework | `shared` (static) |
| Min iOS | 16.0 |
### KMP Architecture
@ -177,12 +173,12 @@ All business logic lives in `shared/src/commonMain/`. iOS code in `iosApp/` is a
### Troubleshooting
| Problem | Fix |
| ----------------------------- | ------------------------------------------------------------------------ |
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| "Build number already exists" | Increment build number in step 3 |
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
| Problem | Fix |
|---------|-----|
| "No signing certificate" | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| "Provisioning profile" error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| "Build number already exists" | Increment build number in step 3 |
| KMP build fails | Check Java 17: `java -version`. Install: `brew install openjdk@17` |
| "shared.framework not found" | Run `./gradlew :shared:embedAndSignAppleFrameworkForXcode` |
| Gradle SSL proxy error | Build on home network (corporate proxy blocks Gradle repos) |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |

View File

@ -52,69 +52,52 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under
7. **Run verification commands** (non-destructive, read-only):
// turbo
- `cd mindlyst-native && ./gradlew projects` — verify module structure
// turbo
// turbo
- `cd mindlyst-native/web && cat package.json | head -20` — verify web deps
8. **Generate WINDSURF_CONTEXT.md** at the repo root with the following sections:
```markdown
# WINDSURF_CONTEXT.md
> Auto-generated by /scan-repo workflow. Last updated: <YYYY-MM-DD HH:MM>
> Re-run with: /scan-repo-and-update-windsurf-context
## Project Summary
<1-paragraph summary of MindLyst, current state, and what phase we're in>
## Architecture
<Brief architecture description: KMP shared + native UI shells>
## Module Map
<Table of all modules/directories with purpose and key files>
## Shared Logic (KMP commonMain)
<List every class/file in shared module with one-line description>
## Platform UI State
### Android
<List screens, components, what's implemented vs stubbed>
### iOS
<List screens, components, what's implemented vs stubbed>
### Web
<List pages, what's implemented vs stubbed>
## Design Tokens
<Current color palette, fonts, spacing from MindLystTokens.kt>
## Dependencies
<Key dependency versions from libs.versions.toml>
## Build Status
<What builds, what doesn't, known issues>
## Implementation Progress
<Phase checklist from IMPLEMENTATION_PLAN.md copy current status>
## Open Issues / TODOs
<Any TODOs found in code, known blockers, next steps>
## Key Files Quick Reference
<Table: "When you need to..." "Edit this file">
```
@ -123,13 +106,11 @@ This workflow scans the entire MindLyst repository, builds a comprehensive under
10. **Verify** the generated file is complete and accurate by re-reading it.
11. **Commit and push**:
- `git add WINDSURF_CONTEXT.md`
- `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"`
- `git push`
- `git add WINDSURF_CONTEXT.md`
- `git commit -m "docs: update WINDSURF_CONTEXT.md via /scan-repo-and-update-windsurf-context"`
- `git push`
## Notes
- This workflow creates `WINDSURF_CONTEXT.md` if it doesn't exist, or fully replaces it if it does
- It commits and pushes the updated file automatically
- Run this periodically to keep context fresh, especially after major changes

View File

@ -9,13 +9,12 @@ Follow these steps to diagnose and fix a failing service or endpoint.
### 1. Identify the failing service
Check which service is affected:
- Backend API (Python/FastAPI) → `backend/src/`
- Platform Service (Fastify) → `../learning_ai_common_plat/services/platform-service/src/`
- Extraction Service (Fastify) → `../learning_ai_common_plat/services/extraction-service/src/`
- Admin Dashboard (Next.js) → `admin-dashboard-web/src/`
- Admin Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/admin-web/src/`
- User Dashboard (Next.js) → `user-dashboard-web/src/`
- Tracker Dashboard (Next.js) → `tracker-dashboard-web/src/`
- Tracker Dashboard (Next.js) → `../learning_ai_common_plat/dashboards/tracker-web/src/`
### 2. Check health
@ -29,7 +28,6 @@ For local dev:
Run `tail -50 .logs/backend.log .logs/platform-service.log 2>/dev/null | head -100`
For Docker:
```bash
docker compose logs --tail=50 <service-name>
```
@ -54,7 +52,6 @@ docker compose logs --tail=50 <service-name>
Run `python -m pytest tests/ backend/tests/ -v --tb=short -x`
For TypeScript services:
```bash
cd ../learning_ai_common_plat && pnpm --filter @lysnrai/<service-name> test
```

View File

@ -10,9 +10,8 @@ Use this on a home network or CI where there is no corporate proxy.
- Docker Desktop running
- `.env` at repo root (Cosmos DB, JWT, Stripe, Azure credentials)
- `admin-dashboard-web/.env.local` (Cosmos DB, JWT)
- `user-dashboard-web/.env.local` (Cosmos DB, JWT)
- `tracker-dashboard-web/.env.local` (Cosmos DB, JWT)
- Admin + Tracker dashboards now live in `../learning_ai_common_plat/dashboards/` and use shared `.env`
### Start
@ -24,29 +23,25 @@ docker compose logs -f # tail all logs
### Services
| Service | Container | Port | Image / Dockerfile |
| ----------------- | ------------------- | -------- | ----------------------------------------------------------------- |
| Loki | `loki` | 3100 | `grafana/loki:3.3.2` |
| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` |
| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` |
| Backend API | `backend` | 8000 | `backend/Dockerfile` |
| Admin Dashboard | `admin-dashboard` | 3001 | `admin-dashboard-web/Dockerfile` |
| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` |
| Tracker Dashboard | `tracker-dashboard` | 3003 | `tracker-dashboard-web/Dockerfile` |
| Growth Service | `growth-service` | 4001 | `../learning_ai_common_plat/services/growth-service/Dockerfile` |
| Billing Service | `billing-service` | 4002 | `../learning_ai_common_plat/services/billing-service/Dockerfile` |
| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` |
| Tracker Service | `tracker-service` | 4004 | `../learning_ai_common_plat/services/tracker-service/Dockerfile` |
| Service | Container | Port | Image / Dockerfile |
|---------|-----------|------|--------------------|
| Loki | `loki` | 3100 | `grafana/loki:3.3.2` |
| Grafana | `grafana` | 3000 | `grafana/grafana:11.4.0` |
| Traefik Gateway | `gateway` | 80, 8080 | `traefik:v3.3` |
| Backend API | `backend` | 8000 | `backend/Dockerfile` |
| Admin Dashboard | `admin-dashboard` | 3001 | `../learning_ai_common_plat/dashboards/admin-web/` |
| User Dashboard | `user-dashboard` | 3002 | `user-dashboard-web/Dockerfile` |
| Tracker Dashboard | `tracker-dashboard` | 3003 | `../learning_ai_common_plat/dashboards/tracker-web/` |
| Platform Service | `platform-service` | 4003 | `../learning_ai_common_plat/services/platform-service/Dockerfile` |
| Extraction Service | `extraction-service` | 4005 | `../learning_ai_common_plat/services/extraction-service/Dockerfile` |
### Traefik Routing (port 80)
| PathPrefix | Routes to |
| ------------------------------------------------------------------------------------------------- | ---------------- |
| `/api` (catch-all), `/health` | Backend API |
| `/api/invitations`, `/api/referrals`, `/api/promos` | Growth Service |
| `/api/subscriptions`, `/api/payments`, `/api/usage`, `/api/plans`, `/api/licenses`, `/api/stripe` | Billing Service |
| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit` | Platform Service |
| `/api/items`, `/api/tracker` | Tracker Service |
| PathPrefix | Routes to |
|------------|-----------|
| `/api` (catch-all), `/health` | Backend API |
| `/api/auth`, `/api/audit`, `/api/notifications`, `/api/flags`, `/api/ratelimit`, `/api/subscriptions`, `/api/invitations`, `/api/stripe`, `/public` | Platform Service (consolidated) |
| `/api/extract`, `/api/tasks` | Extraction Service |
### Stop

View File

@ -7,37 +7,35 @@ description: Regenerate all app store artwork (icons, screenshots, feature graph
All 73 store assets are generated programmatically from a single Python script.
// turbo
1. Run `python3 assets/generate-store-assets.py` from the repo root
### Output (73 PNGs)
| Category | Count | Directory |
| --------------- | ----- | ---------------------------------------------------------------------------------- |
| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) |
| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light |
| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` |
| Splash Screens | 4 | `assets/store/splash/` |
| Category | Count | Directory |
|----------|-------|-----------|
| App Icons | 36 | `assets/store/icons/` — iOS (13), Android (6), macOS (7), Windows (5), Favicon (5) |
| Screenshots | 32 | `assets/store/screenshots/{ios,android,mac,windows}/` — 4 screens × dark+light |
| Feature Graphic | 1 | `assets/store/feature/feature-graphic-1024x500.png` |
| Splash Screens | 4 | `assets/store/splash/` |
### Customizing Design
Edit the color palette at the top of `assets/generate-store-assets.py`:
| Variable | Default | Purpose |
| --------------- | --------- | ------------------------------------- |
| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers |
| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights |
| `DARK_BG` | `#1E1E2E` | Dark mode background |
| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar |
| `MUTED` | `#6272A4` | Secondary text, timestamps |
| `CYAN` | `#8BE9FD` | Transcript card accents |
| Variable | Default | Purpose |
|----------|---------|---------|
| `GREEN_PRIMARY` | `#2E7D32` | Icon circle, badges, section headers |
| `GREEN_ACCENT` | `#50FA7B` | Glowing text, active tabs, highlights |
| `DARK_BG` | `#1E1E2E` | Dark mode background |
| `DARK_SURFACE` | `#282A36` | Cards, inputs, tab bar |
| `MUTED` | `#6272A4` | Secondary text, timestamps |
| `CYAN` | `#8BE9FD` | Transcript card accents |
After changing colors, re-run step 1 to regenerate all assets.
### Icon Design
The app icon is a **waveform-in-circle** with a green glow on dark background. Customize in `generate_app_icon()`:
- Circle radius: `size * 0.28`
- Waveform bars: `num_bars=7`, heights pattern `[0.3, 0.5, 0.75, 1.0, 0.75, 0.5, 0.3]`
- Corner radius: `size * 0.22`
@ -46,7 +44,6 @@ The app icon is a **waveform-in-circle** with a green glow on dark background. C
### Screenshot Content
Each screen function has hardcoded sample data (user name, transcript titles, word counts). Edit:
- `generate_screenshot_home()` — greeting, stats, activity list
- `generate_screenshot_record()` — timer, live text preview
- `generate_screenshot_history()` — transcript cards with dates

View File

@ -12,7 +12,6 @@ or after adding/renaming Swift files.
### Step 1 — Verify git working tree is clean
// turbo
```bash
git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || echo "WARNING: uncommitted changes"
```
@ -20,7 +19,6 @@ git diff --quiet && git diff --cached --quiet && echo "Working tree clean" || ec
### Step 2 — Install CocoaPods (if needed)
// turbo
```bash
cd mobile_app/ios && pod install --repo-update && cd ../..
```
@ -49,7 +47,6 @@ xcodebuild build \
Check that every `.swift` file on disk in the keyboard extension directory is listed in the Xcode project.
// turbo
```bash
echo "=== Checking LysnrKeyboard target source consistency ==="
for f in mobile_app/ios/LysnrKeyboard/*.swift; do
@ -80,7 +77,6 @@ fi
### Step 5 — Check for common Swift issues
// turbo
```bash
echo "=== Checking for hardcoded secrets ==="
grep -rn --include='*.swift' -E '(AZURE_SPEECH_KEY|api_key|secret|password)\s*=' mobile_app/ios/ \
@ -104,7 +100,6 @@ grep -rn --include='*.swift' '[^?]![^=]' mobile_app/ios/LysnrKeyboard/*.swift \
### Step 6 — Verify Info.plist keys
// turbo
```bash
echo "=== Checking required Info.plist keys ==="
for key in NSMicrophoneUsageDescription NSSpeechRecognitionUsageDescription NSExtensionPointIdentifier RequestsOpenAccess; do
@ -129,22 +124,22 @@ fi
### What This Catches
| Check | Prevents |
| ------------------- | ----------------------------------------------------------------- |
| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) |
| pbxproj consistency | New .swift files added to disk but not to Xcode target |
| Secret scan | Hardcoded API keys in Swift source |
| print() check | Debug logging in production keyboard extension |
| Force-unwrap check | Runtime crashes from unsafe unwraps |
| Info.plist check | Missing privacy descriptions → App Store rejection |
| Entitlements check | Missing App Group → keyboard can't share data with main app |
| Check | Prevents |
|-------|----------|
| Build all targets | Missing source files in Xcode project (e.g. LysnrTelemetry.swift) |
| pbxproj consistency | New .swift files added to disk but not to Xcode target |
| Secret scan | Hardcoded API keys in Swift source |
| print() check | Debug logging in production keyboard extension |
| Force-unwrap check | Runtime crashes from unsafe unwraps |
| Info.plist check | Missing privacy descriptions → App Store rejection |
| Entitlements check | Missing App Group → keyboard can't share data with main app |
### Key Files
| File | Purpose |
| --------------------------------------------------------- | ---------------------------------------------- |
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) |
| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement |
| `mobile_app/ios/Podfile` | CocoaPods dependencies |
| File | Purpose |
|------|---------|
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Xcode project (targets, sources, build phases) |
| `mobile_app/ios/LysnrKeyboard/` | Keyboard extension sources |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
| `mobile_app/ios/LysnrKeyboard/LysnrKeyboard.entitlements` | App Group entitlement |
| `mobile_app/ios/Podfile` | CocoaPods dependencies |

View File

@ -8,7 +8,7 @@ date: 2025-02-12
> Runs comprehensive checks across all 3 repos, fixes failures as they occur, commits + pushes incrementally.
> **NOTE**: GitHub Actions are temporarily disabled due to billing issues. Please run these checks manually.
> Order: common_plat → voice_agent → mindlyst (dependency-safe).
>
>
> See MANUAL_CI.md in each repo for quick pre-push checks.
## Phase 1: learning_ai_common_plat (shared packages + services)
@ -56,9 +56,8 @@ for pkg in packages/*/dist; do node -e "require('./$pkg/index.js')" 2>/dev/null
```bash
# 1. Install dashboards (requires common_plat built)
cd $HOME/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm install
cd ../user-dashboard-web && npm install
cd ../tracker-dashboard-web && npm install
# NOTE: admin-dashboard-web and tracker-dashboard-web moved to learning_ai_common_plat/dashboards/
cd $HOME/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm install
# 2. Type-check all dashboards
npx tsc --noEmit
@ -124,11 +123,10 @@ cd ..
bash scripts/build.sh
# If fails: fix, then git add . && git commit -m "fix(desktop): build fixes" && git push
# 16. E2E tests for all dashboards
cd admin-dashboard-web && npm run test:e2e
cd ../user-dashboard-web && npm run test:e2e
cd ../tracker-dashboard-web && npm run test:e2e
# If fails: fix, commit push per dashboard
# 16. E2E tests for user dashboard
cd user-dashboard-web && npm run test:e2e
# If fails: fix, commit push
# NOTE: admin + tracker E2E tests now run from learning_ai_common_plat/dashboards/
```
## Phase 3: learning_multimodal_memory_agents (MindLyst)
@ -197,24 +195,23 @@ npx tsx services/monitoring/health-check.ts
## Enhanced Coverage Summary (Post-Quick Wins)
| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security |
| ------------------------ | ---------- | ---- | ------ | ---------- | -------- | ----- | ------ | --- | -------- |
| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Component | Type-Check | Lint | Format | Unit Tests | Coverage | Build | Bundle | E2E | Security |
|-----------|------------|------|--------|------------|----------|-------|--------|-----|----------|
| **common_plat packages** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| **common_plat services** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| **admin-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **user-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **tracker-dashboard** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **desktop app** | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
| **backend API** | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ |
| **mindlyst shared** | ✅ | 📱 | 📱 | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst web** | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst android** | ✅ | 📱 | 📱 | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **mindlyst ios** | ❌ | 📱 | 📱 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
📱 = Available via dedicated mobile workflow (.windsurf/workflows/mobile-code-quality.md)
**All Quick Wins Implemented:**
- ✅ Prettier formatting (consistent across all repos)
- ✅ ESLint for common_plat (12 projects now linted)
- ✅ Test coverage with 80% threshold enforcement
@ -227,7 +224,7 @@ npx tsx services/monitoring/health-check.ts
## Notes
- **Commit message pattern**:
- **Commit message pattern**:
- `fix(scope): type-check fixes`
- `test(scope): fix failing tests`
- `fix(python): lint/format fixes`

View File

@ -38,20 +38,19 @@ cp .env ~/.lysnrai/.env # create config dir + env file
Edit `~/.lysnrai/.env` and fill in real values for:
| Variable | Where to get it |
| ------------------------- | ------------------------------------ |
| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys |
| `AZURE_SPEECH_REGION` | e.g. `eastus` |
| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint |
| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys |
| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` |
| Variable | Where to get it |
|----------|-----------------|
| `AZURE_SPEECH_KEY` | Azure Portal → Speech Service → Keys |
| `AZURE_SPEECH_REGION` | e.g. `eastus` |
| `AZURE_OPENAI_ENDPOINT` | Azure Portal → OpenAI → Endpoint |
| `AZURE_OPENAI_KEY` | Azure Portal → OpenAI → Keys |
| `AZURE_OPENAI_DEPLOYMENT` | e.g. `gpt-4o-mini` |
### 4. (Optional) Set up Apple code signing
Only needed if you want to distribute the app to others (notarized).
**a) Create a "Developer ID Application" certificate:**
1. Go to [developer.apple.com/account/resources/certificates](https://developer.apple.com/account/resources/certificates/list)
2. Click **+** → select **Developer ID Application**
3. Create a CSR: open **Keychain Access** → Certificate Assistant → Request a Certificate From a Certificate Authority → Save to Disk
@ -59,7 +58,6 @@ Only needed if you want to distribute the app to others (notarized).
5. Verify: `security find-identity -v -p codesigning | grep "Developer ID"`
**b) Generate an app-specific password** (for notarization):
1. Go to [appleid.apple.com](https://appleid.apple.com) → Sign-In and Security → App-Specific Passwords
2. Generate a new password, label it "LysnrAI Notarization"
3. Keep it handy — the codesign script will prompt for it securely (never stored)
@ -73,7 +71,6 @@ Run these checks before building any platform to catch issues early.
### 0a. Verify clean working tree
// turbo
```bash
git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: Uncommitted changes — commit or stash first" && exit 1)
```
@ -81,7 +78,6 @@ git diff --quiet && git diff --cached --quiet && echo "Clean" || (echo "ERROR: U
### 0b. Activate venv + verify deps
// turbo
```bash
source .venv/bin/activate && pip install -e ".[dev]" --quiet
```
@ -89,7 +85,6 @@ source .venv/bin/activate && pip install -e ".[dev]" --quiet
### 0c. Lint with ruff
// turbo
```bash
source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -20
```
@ -103,7 +98,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai
### 0e. Verify imports (quick syntax check)
// turbo
```bash
source .venv/bin/activate && python -c "import src.main; print('OK: src.main imports cleanly')"
```
@ -115,7 +109,6 @@ source .venv/bin/activate && python -c "import src.main; print('OK: src.main imp
### Step 1 — Build the .app bundle
// turbo
```bash
bash scripts/build.sh
```
@ -130,7 +123,6 @@ bash scripts/codesign_macos.sh dist/LysnrAI.app
```
The script interactively prompts for:
- **Apple ID** (default: `saravanakumardb@gmail.com`)
- **Team ID** (default: `748N7QPX7J`)
- **App-specific password** (secure input, not echoed or stored)
@ -224,37 +216,37 @@ Share these instructions with anyone receiving the app.
### Keyboard Shortcuts
| Action | Windows / Linux | macOS |
| -------------- | ------------------ | ------------------------- |
| Action | Windows / Linux | macOS |
|--------|----------------|-------|
| Dictate (hold) | `Ctrl+Shift+Space` | `Cmd+Shift+Space` or `Fn` |
| History | `Ctrl+Shift+H` | `Cmd+Shift+H` |
| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` |
| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` |
| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` |
| History | `Ctrl+Shift+H` | `Cmd+Shift+H` |
| Undo paste | `Ctrl+Shift+Z` | `Cmd+Shift+Z` |
| Stats | `Ctrl+Shift+S` | `Cmd+Shift+S` |
| Shortcuts help | `Ctrl+Shift+K` | `Cmd+Shift+K` |
### Key Files
| File | Purpose |
| ------------------------------ | ------------------------------------------------------ |
| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) |
| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) |
| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) |
| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization |
| `scripts/codesign_windows.ps1` | Windows Authenticode signing |
| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) |
| `.env` | Template for Azure credentials |
| File | Purpose |
|------|---------|
| `scripts/build.sh` | macOS build (PyInstaller + dylibs + Info.plist) |
| `scripts/build_windows.ps1` | Windows build (PyInstaller + ZIP + optional installer) |
| `scripts/build_linux.sh` | Linux build (PyInstaller + tar.gz + optional AppImage) |
| `scripts/codesign_macos.sh` | macOS code signing + interactive notarization |
| `scripts/codesign_windows.ps1` | Windows Authenticode signing |
| `scripts/install_macos.sh` | Full macOS install (build + /Applications + launcher) |
| `.env` | Template for Azure credentials |
### Troubleshooting
| Problem | Fix |
| -------------------------------- | ------------------------------------------------------------------------------ |
| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` |
| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) |
| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application |
| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" |
| Problem | Fix |
|---------|-----|
| macOS "App is damaged" | Not notarized. Run `xattr -cr /Applications/LysnrAI.app` |
| macOS no Accessibility | Must launch via `.command` file (Terminal inherits Accessibility) |
| macOS no Developer ID cert | Create at developer.apple.com → Certificates → + → Developer ID Application |
| Windows SmartScreen blocks | Code sign the .exe, or click "More info" → "Run anyway" |
| Windows VCRUNTIME140.dll missing | Install [VC++ Redistributable](https://aka.ms/vs/17/release/vc_redist.x64.exe) |
| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) |
| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted |
| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` |
| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review |
| Tests fail before build | Fix failing tests first — never ship with red tests |
| Linux no system tray | Install `gnome-shell-extension-appindicator` (GNOME) |
| Linux no audio | Check PulseAudio/PipeWire is running, mic not muted |
| PyInstaller build fails | Activate `.venv` first: `source .venv/bin/activate && pip install -e ".[dev]"` |
| Ruff lint errors | Run `ruff check src/ --fix` to auto-fix, then review |
| Tests fail before build | Fix failing tests first — never ship with red tests |

View File

@ -45,7 +45,6 @@ cd learning_voice_ai_agent/mobile_app/ios && pod install
### 0a. Read current build state
// turbo
```bash
cat mobile_app/ios/BUILD_STATE.md
```
@ -55,7 +54,6 @@ Note the **Current Build** number. The next release will be `current + 1`.
### 0b. Verify clean working tree
// turbo
```bash
git status && git diff --quiet && git diff --cached --quiet && echo "✅ Clean" || (echo "❌ ERROR: Uncommitted changes — commit or stash first" && exit 1)
```
@ -80,7 +78,6 @@ Must end with `BUILD SUCCEEDED`. If it fails, fix errors before continuing.
### 0d. Verify pbxproj ↔ filesystem consistency
// turbo
```bash
for f in mobile_app/ios/LysnrKeyboard/*.swift; do
fname=$(basename "$f")
@ -93,7 +90,6 @@ done && echo "✅ All keyboard .swift files are in the Xcode project"
### 0e. Verify BUILD_STATE.md matches project.pbxproj
// turbo
```bash
state_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
pbx_builds=$(grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj | awk '{print $3}' | tr -d ';' | sort -u)
@ -113,7 +109,6 @@ If `BUILD_STATE.md` marks the current build as **Pending Upload Retry**, do NOT
Retry **step 5 only** with the existing archive path `/tmp/LysnrAI_<current>.xcarchive`.
// turbo
```bash
if grep -q "Current / Pending Upload Retry" mobile_app/ios/BUILD_STATE.md; then
current_build=$(grep -Eo 'CURRENT_PROJECT_VERSION = [0-9]+' mobile_app/ios/BUILD_STATE.md | head -1 | awk '{print $3}')
@ -135,7 +130,6 @@ occurrences of `CURRENT_PROJECT_VERSION = N;` with `CURRENT_PROJECT_VERSION = N+
Verify the bump took effect on all 6:
// turbo
```bash
grep "CURRENT_PROJECT_VERSION" mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj
```
@ -155,7 +149,6 @@ Commit BEFORE archiving so the build number in git always matches what was uploa
### 3. Install CocoaPods dependencies
// turbo
```bash
cd mobile_app/ios && pod install && cd ../..
```
@ -189,7 +182,6 @@ xcodebuild -exportArchive \
The `app-store-connect` export method auto-uploads the IPA.
**Expected output:**
```
Uploaded LysnrAI
** EXPORT SUCCEEDED **
@ -205,7 +197,6 @@ Go back to step 1 and bump again.
### 6. Update BUILD_STATE.md
After a successful upload, Cascade updates `mobile_app/ios/BUILD_STATE.md`:
- Set **Current Build** to the new number
- Add a row to the Build History table with the build number, status `Released`, and key changes
- Update Active Issues table if any were fixed or newly found
@ -240,38 +231,38 @@ Internal testing group has auto-distribute enabled. After Apple finishes process
### Key Files
| File | Purpose |
| ----------------------------------------------------------- | ------------------------------------------------------------------ |
| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues |
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) |
| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) |
| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) |
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys |
| File | Purpose |
|------|---------|
| `mobile_app/ios/BUILD_STATE.md` | **Source of truth** — current build number, history, active issues |
| `mobile_app/ios/LysnrAI.xcodeproj/project.pbxproj` | Build settings (6× CURRENT_PROJECT_VERSION) |
| `scripts/ExportOptions.plist` | Export options (method: app-store-connect, team: 748N7QPX7J) |
| `mobile_app/ios/Podfile` | CocoaPods deps (Azure Speech SDK) |
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Keyboard extension config + privacy keys |
### Build Identity
| Field | Value |
| -------------------- | ------------------------------- |
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.bytelyst.LysnrAI` |
| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` |
| App Group | `group.com.bytelyst.LysnrAI` |
| Signing | Automatic |
| Archive path pattern | `/tmp/LysnrAI_<N>.xcarchive` |
| Field | Value |
|-------|-------|
| Team ID | `748N7QPX7J` |
| Bundle ID | `com.bytelyst.LysnrAI` |
| Keyboard Bundle ID | `com.bytelyst.LysnrAI.keyboard` |
| App Group | `group.com.bytelyst.LysnrAI` |
| Signing | Automatic |
| Archive path pattern | `/tmp/LysnrAI_<N>.xcarchive` |
### Troubleshooting
| Problem | Fix |
| ----------------------------------- | --------------------------------------------------------------------------- |
| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics |
| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase |
| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources |
| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` |
| `bundle version must be higher` | Build number already uploaded — bump again from step 1 |
| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) |
| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth |
| Problem | Fix |
|---------|-----|
| `BUILD FAILED` in step 0c | Fix compile errors; run `/mobile-code-quality` for full diagnostics |
| `.swift file not in pbxproj` | Add file to Xcode target Sources build phase |
| `LysnrTelemetry not found` | Verify `LysnrTelemetry.swift` is in LysnrKeyboard target Sources |
| `ARCHIVE FAILED` | Check full output: `2>&1 \| grep error:` |
| `bundle version must be higher` | Build number already uploaded — bump again from step 1 |
| `Upload limit reached` | Apple daily limit hit — wait ~24h, re-run step 5 only (archive is reusable) |
| `No signing certificate` | Xcode → Settings → Accounts → Manage Certificates → + Apple Distribution |
| `Provisioning profile` error | Xcode → Target → Signing → Enable "Automatically manage signing" |
| Processing >30 min | Check [Apple system status](https://developer.apple.com/system-status/) |
| Build number drift (git vs pbxproj) | Read `BUILD_STATE.md` — it is the source of truth |

View File

@ -8,24 +8,24 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
## Repos Covered
| Repo | Path | Scope |
| ------------------------------------- | --------------------------------------------------- | ------------------------------------------------------- |
| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) |
| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/_ packages + @lysnrai/_ microservices |
| Repo | Path | Scope |
|------|------|-------|
| **learning_voice_ai_agent** | `$HOME/code/mygh/learning_voice_ai_agent` | LysnrAI product code (desktop, backend, dashboards) |
| **learning_ai_common_plat** | `$HOME/code/mygh/learning_ai_common_plat` | Shared @bytelyst/* packages + @lysnrai/* microservices |
| **learning_multimodal_memory_agents** | `$HOME/code/mygh/learning_multimodal_memory_agents` | MindLyst native app (KMP + SwiftUI + Compose + Next.js) |
## Files Updated Per Repo
| File | Tool | Format |
| --------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------- |
| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership |
| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) compact quick-reference summary |
| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules |
| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists |
| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system |
| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations |
| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands |
| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules |
| File | Tool | Format |
|------|------|--------|
| `AGENTS.md` | Universal (OpenAI Codex, all agents) | Detailed markdown — full onboarding, structure, conventions, patterns, ownership |
| `CLAUDE.md` | Claude Code (Anthropic) | Short markdown (<50 lines) compact quick-reference summary |
| `.cursorrules` | Cursor AI | Plain text — inline completion + chat rules |
| `.github/copilot-instructions.md` | GitHub Copilot | Markdown — code generation always/never lists |
| `.windsurfrules` | Windsurf / Codeium Cascade | Plain text — project rules for Windsurf memory system |
| `.clinerules` | Cline / Roo Code (VS Code) | Plain text — mandatory rules + key file locations |
| `.aider.conf.yml` | Aider | YAML — context files, conventions pointer, lint commands |
| `.editorconfig` | All editors / JetBrains AI | INI — indent, charset, line ending, trim rules |
---
@ -36,21 +36,22 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
1. **Scan learning_voice_ai_agent structure:**
// turbo
- Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 2 -type f -name "package.json" -not -path "*/node_modules/*" | head -20` to find all JS projects
// turbo
// turbo
- Run `find $HOME/code/mygh/learning_voice_ai_agent -maxdepth 1 -type f -name "*.py" -o -name "*.toml" -o -name "Makefile" | head -20` to find Python config
- Read `AGENTS.md`, `README_MONO_REPO.md`, `docker-compose.yml`, `pyproject.toml`
- Read each dashboard's `package.json` to check current `@bytelyst/*` dependencies
- List `admin-dashboard-web/src/lib/`, `user-dashboard-web/src/lib/`, `tracker-dashboard-web/src/lib/` to see which lib files exist
- Read `admin-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/\* packages are wired
- List `user-dashboard-web/src/lib/` to see which lib files exist
- NOTE: admin-dashboard-web and tracker-dashboard-web moved to `../learning_ai_common_plat/dashboards/`
- Read `user-dashboard-web/src/lib/cosmos.ts` and `auth-server.ts` to verify which @bytelyst/* packages are wired
- Count Python tests: `find tests/ -name "test_*.py" | wc -l`
- Count API routes: `find admin-dashboard-web/src/app/api -name "route.ts" | wc -l`
- Count API routes: `find user-dashboard-web/src/app/api -name "route.ts" | wc -l`
- Read `.github/workflows/` to count CI workflows
- Read `run-local-all-services.sh` header to understand service topology
2. **Scan learning_ai_common_plat structure:**
// turbo
- Run `find $HOME/code/mygh/learning_ai_common_plat/packages -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all packages
// turbo
// turbo
- Run `find $HOME/code/mygh/learning_ai_common_plat/services -maxdepth 2 -name "package.json" -not -path "*/node_modules/*"` to list all services
- Read `AGENTS.md`, `README.md`, `pnpm-workspace.yaml`, `tsconfig.base.json`
- Read each package's `src/index.ts` to catalog exports
@ -83,16 +84,16 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
6. **Update `AGENTS.md`** — the most comprehensive file. Ensure these sections are current:
- **Project Identity** — product name, IDs, prefixes
- **Monorepo Layout** — directory tree with descriptions, including sibling repo structure
- **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/\* wiring)
- **Tech Stack Rules** — Python, TypeScript services, TypeScript dashboards (with @bytelyst/* wiring)
- **Coding Conventions** — MUST/MUST NOT rules
- **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/\* mappings)
- **File Ownership Map** — table mapping domains → services → key files (include all @bytelyst/* mappings)
- **How to Run Things** — start services, run tests, docker compose, seed
- **Common Patterns** — adding modules, pages, service clients, backend routes, debugging
- **Environment Variables** — required vars per service, env file locations
- **Key Documents** — table of "when you need to..." → "read this"
7. **Update `CLAUDE.md`** — compact summary (<50 lines):
- Identity, key commands, critical rules, current @bytelyst/\* wiring state
- Identity, key commands, critical rules, current @bytelyst/* wiring state
8. **Update `.cursorrules`** — Cursor inline completion + chat rules:
- Project context, code generation patterns, naming conventions, import patterns
@ -102,7 +103,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
- Always/never lists, commit format, import patterns, type patterns
10. **Update `.windsurfrules`** — Windsurf/Cascade project rules:
- Architecture, key paths, conventions, @bytelyst/\* wiring, build verification commands
- Architecture, key paths, conventions, @bytelyst/* wiring, build verification commands
11. **Update `.clinerules`** — Cline/Roo Code mandatory rules:
- Numbered mandatory rules, key file locations, verify command
@ -118,7 +119,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
14. **Update all 8 agent doc files** in common platform, ensuring:
- `AGENTS.md`: Package exports, service modules, file ownership (27+ domains), dependency graph, consumer info, test count
- `CLAUDE.md`: Compact summary
- `.cursorrules`: Completion rules for @bytelyst/_ and @lysnrai/_
- `.cursorrules`: Completion rules for @bytelyst/* and @lysnrai/*
- `.github/copilot-instructions.md`: Generation rules
- `.windsurfrules`: pnpm workspace rules, ESM conventions
- `.clinerules`: Mandatory rules for package/service development
@ -141,7 +142,7 @@ Scans all three workspace repos, builds a comprehensive understanding of the cur
16. **Verify consistency across repos:**
- Shared conventions (commit format, productId rule, etc.) should match across all 3 AGENTS.md files
- @bytelyst/\* package descriptions should be consistent between common platform and voice agent docs
- @bytelyst/* package descriptions should be consistent between common platform and voice agent docs
- Design token flow should be consistent between common platform and MindLyst docs
17. **Commit and push each repo** (stage all 8 files):

View File

@ -7,15 +7,14 @@ description: Start all backend services locally (FastAPI + 2 microservices + Adm
Run the local dev startup script to launch all 7 services:
// turbo
1. Run `./run-local-all-services.sh start` from the repo root
2. Verify all services are up:
// turbo
// turbo
- Run `./run-local-all-services.sh status`
3. Quick health check:
// turbo
// turbo
- Run `curl -s http://127.0.0.1:8000/health` — should return `{"status":"ok"}`
- Run `curl -s http://127.0.0.1:4003/health` — should return `{"status":"ok"}`
- Run `curl -s http://127.0.0.1:4005/health` — should return `{"status":"ok"}`
@ -26,15 +25,15 @@ Run the local dev startup script to launch all 7 services:
### Service URLs
| Service | URL | Log File |
| ---------------------------- | --------------------- | ------------------------------ |
| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` |
| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` |
| Service | URL | Log File |
|---------|-----|----------|
| Backend API (FastAPI) | http://localhost:8000 | `.logs/backend.log` |
| Platform Service (Fastify) | http://localhost:4003 | `.logs/platform-service.log` |
| Extraction Service (Fastify) | http://localhost:4005 | `.logs/extraction-service.log` |
| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` |
| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` |
| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` |
| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` |
| Extraction Sidecar (Python) | http://localhost:4006 | `.logs/extraction-sidecar.log` |
| Admin Dashboard | http://localhost:3001 | `.logs/admin-dashboard.log` |
| User Dashboard | http://localhost:3002 | `.logs/user-dashboard.log` |
| Tracker Dashboard | http://localhost:3003 | `.logs/tracker-dashboard.log` |
### Stop

View File

@ -20,7 +20,6 @@ Generate detailed test coverage reports for all three workspace repos and produc
### Step 1: Python tests — LysnrAI desktop + backend
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_voice_ai_agent && python -m pytest tests/ backend/tests/ -v --tb=short --co -q 2>/dev/null | tail -5
```
@ -45,7 +44,6 @@ Cascade: Record the Python coverage summary (total lines, covered, missed, %).
### Step 2: Common Platform — all packages + services (Vitest)
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_ai_common_plat && pnpm build 2>&1 | tail -5
```
@ -70,20 +68,20 @@ Cascade: Record each package/service coverage summary (statements, branches, fun
---
### Step 3: LysnrAI Dashboards — Admin, User, Tracker (Vitest)
### Step 3: LysnrAI Dashboards (Vitest)
Run coverage for each dashboard:
```bash
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/admin-dashboard-web && npm run test:coverage 2>&1 | tail -60
```
Run coverage for user dashboard (admin + tracker moved to `learning_ai_common_plat/dashboards/`):
```bash
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/user-dashboard-web && npm run test:coverage 2>&1 | tail -60
```
```bash
cd /Users/sd9235/code/mygh/learning_voice_ai_agent/tracker-dashboard-web && npm run test:coverage 2>&1 | tail -60
cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/admin-web && npm run test:coverage 2>&1 | tail -60
```
```bash
cd /Users/sd9235/code/mygh/learning_ai_common_plat/dashboards/tracker-web && npm run test:coverage 2>&1 | tail -60
```
Cascade: Record each dashboard's coverage summary.
@ -103,7 +101,6 @@ Cascade: Record MindLyst web coverage summary.
### Step 5: MindLyst KMP Shared (Kotlin)
// turbo
```bash
cd /Users/sd9235/code/mygh/learning_multimodal_memory_agents/mindlyst-native && ./gradlew :shared:test 2>&1 | tail -30
```
@ -169,11 +166,11 @@ If there is nothing new to commit (same content/timestamp not created), report t
### Troubleshooting
| Problem | Fix |
| ------------------------------- | -------------------------------------------------- |
| `pytest-cov` not found | `pip install pytest-cov` |
| Problem | Fix |
|---------|-----|
| `pytest-cov` not found | `pip install pytest-cov` |
| `@vitest/coverage-v8` not found | `npm install -D @vitest/coverage-v8` (per project) |
| Common-plat coverage fails | Run `pnpm build` first, then retry |
| Dashboard coverage fails | Run `npm install` first in that dashboard |
| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first |
| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest |
| Common-plat coverage fails | Run `pnpm build` first, then retry |
| Dashboard coverage fails | Run `npm install` first in that dashboard |
| KMP tests fail to compile | Run `./gradlew :shared:compileKotlinJvm` first |
| Timeout on large test suites | Add `--reporter=verbose --pool=forks` to vitest |

View File

@ -5,7 +5,6 @@ description: Run and test the macOS desktop app locally
## Test Desktop App (macOS)
### Prerequisites
- Backend running on port 8000 (`./run-local-all-services.sh start`)
- Python 3.12+ with venv
- Azure Speech SDK credentials in `~/.LysnrAI/.env`
@ -23,7 +22,6 @@ pip install -e ".[dev]" --quiet
### Step 2 — Run automated quality checks
// turbo
```bash
source .venv/bin/activate && python -m ruff check src/ --select E,F,W --no-fix 2>&1 | tail -10
```
@ -35,7 +33,6 @@ source .venv/bin/activate && python -m pytest tests/ -v --tb=short -q 2>&1 | tai
### Step 3 — Verify env file exists
// turbo
```bash
ls ~/.LysnrAI/.env && echo "OK: env file found" || echo "ERROR: create ~/.LysnrAI/.env with Azure credentials"
```
@ -57,17 +54,16 @@ source .venv/bin/activate && python3 -m src.main
### Key Files
| File | Purpose |
| --------------------------------- | --------------------------------------------- |
| `src/main.py` | App entry point, LysnrEngine, license restore |
| `src/licensing/license_client.py` | License activation via backend API |
| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener |
| `src/cloud/` | Azure Speech SDK integration |
| `src/audio/` | Audio recording + processing |
| `src/paste/` | Clipboard paste into active app |
| File | Purpose |
|------|---------|
| `src/main.py` | App entry point, LysnrEngine, license restore |
| `src/licensing/license_client.py` | License activation via backend API |
| `src/hotkey/fn_listener.py` | macOS Fn/Globe key listener |
| `src/cloud/` | Azure Speech SDK integration |
| `src/audio/` | Audio recording + processing |
| `src/paste/` | Clipboard paste into active app |
### Important Notes
- macOS: Set System Settings → Keyboard → Press Globe key → "Do Nothing"
- Corporate proxy: Backend must be started with proxy bypass (run-local script handles this)
- The app connects to `http://localhost:8000/api` for license activation

View File

@ -5,7 +5,6 @@ description: Build and test the iOS app in Xcode Simulator
## Test iOS App
### Prerequisites
- All services running (`./run-local-all-services.sh start`)
- Backend healthy: `curl http://127.0.0.1:8000/health`
- CocoaPods installed: `pod install` in `mobile_app/ios/`
@ -55,14 +54,13 @@ Open **`mobile_app/ios/LysnrAI.xcworkspace`** (NOT `.xcodeproj` — CocoaPods re
### Key Files
| File | Purpose |
| ----------------------------------------------------------- | -------------------------------------------- |
| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard |
| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views |
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
| File | Purpose |
|------|---------|
| `mobile_app/ios/LysnrAI/Auth/AuthService.swift` | Auth state, credential sharing with keyboard |
| `mobile_app/ios/LysnrAI/ContentView.swift` | Tab navigation, passes authService to views |
| `mobile_app/ios/LysnrKeyboard/KeyboardViewController.swift` | Keyboard extension main controller |
| `mobile_app/ios/LysnrKeyboard/LysnrTelemetry.swift` | Keyboard telemetry client |
| `mobile_app/ios/LysnrKeyboard/Info.plist` | Extension config + privacy descriptions |
### API Endpoint
iOS Simulator connects to `http://127.0.0.1:8000` (configured in AuthService.swift)

View File

@ -26,6 +26,9 @@ STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_PRO=price_...
STRIPE_PRICE_ENTERPRISE=price_...
# ── Azure Key Vault (optional — resolves secrets at startup) ──
AZURE_KEYVAULT_URL=
# ── Seed (development only) ──
SEED_SECRET=your-seed-secret

View File

@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client';
const billingApi = createApiClient({
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
defaultHeaders: {
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
},
});
// ── Subscriptions (Admin) ────────────────────────────────────

View File

@ -10,6 +10,9 @@ import { createApiClient } from '@bytelyst/api-client';
const growthApi = createApiClient({
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
defaultHeaders: {
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
},
});
// ── Invitations ──────────────────────────────────────────────────

View File

@ -9,6 +9,9 @@ import { createApiClient } from '@bytelyst/api-client';
const platformApi = createApiClient({
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
defaultHeaders: {
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
},
});
// ── Audit ───────────────────────────────────────────────────────

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,726 @@
# Cloud Provider Migration Analysis — ByteLyst Ecosystem
> **Author:** AI Analysis (Cascade)
> **Date:** 2026-03-01
> **Scope:** All 7 repos — LysnrAI, MindLyst, ChronoMind, NomGap, PeakPulse, Common Platform, JarvisJr
> **Purpose:** Evaluate current Azure investment, assess migration feasibility to AWS / GCP / MongoDB Atlas / multi-cloud, and provide actionable recommendations.
---
## Table of Contents
1. [Executive Summary](#1-executive-summary)
2. [Current Azure Investment Inventory](#2-current-azure-investment-inventory)
3. [Dependency Depth Analysis](#3-dependency-depth-analysis)
4. [Migration Target Comparison](#4-migration-target-comparison)
5. [Per-Service Migration Analysis](#5-per-service-migration-analysis)
6. [Migration Scenario Scoring](#6-migration-scenario-scoring)
7. [Cost Comparison](#7-cost-comparison)
8. [Abstraction Layer Assessment](#8-abstraction-layer-assessment)
9. [Risk Analysis](#9-risk-analysis)
10. [Recommendations](#10-recommendations)
11. [Migration Playbook (If Chosen)](#11-migration-playbook-if-chosen)
12. [Appendix A: File-Level Azure Dependency Map](#appendix-a-file-level-azure-dependency-map)
13. [Appendix B: SDK & Package Inventory](#appendix-b-sdk--package-inventory)
---
## 1. Executive Summary
The ByteLyst ecosystem is **moderately coupled** to Azure. The coupling is concentrated in **3 packages** (`@bytelyst/cosmos`, `@bytelyst/blob`, `@bytelyst/config`) and **2 Python modules** (`azure_stt.py`, `cosmos_client.py`). The architecture already uses an internal abstraction layer — most application code never imports Azure SDKs directly.
### Key Findings
| Dimension | Assessment |
|-----------|-----------|
| **Overall Azure lock-in** | **Medium** — concentrated in ~15 files, but those files are foundational |
| **Easiest to migrate** | Blob Storage, Key Vault, OpenAI, Application Insights |
| **Hardest to migrate** | Cosmos DB (SQL API queries in 56+ repository files), Azure Speech SDK |
| **Best alternative DB** | MongoDB Atlas (closest query model to Cosmos SQL API) |
| **Best alternative cloud** | AWS (broadest service parity, mature SDK ecosystem) |
| **Estimated migration effort** | 48 weeks for full cloud swap (Cosmos DB is the long pole) |
| **Recommendation** | **Stay on Azure** for now, but invest in abstraction layers to reduce future switching cost |
### Azure Services Used (8 total)
| # | Azure Service | Monthly Cost | Lock-in Risk | Files Affected |
|---|--------------|-------------|-------------|----------------|
| 1 | **Cosmos DB** (SQL/NoSQL API) | ~$410 | **HIGH** | 56+ repository files, 3 databases, ~45 containers |
| 2 | **Blob Storage** | ~$0.20 | LOW | 2 packages + 1 Python module |
| 3 | **Azure OpenAI** | ~$510 | LOW | 3 files (already supports OpenAI fallback) |
| 4 | **Speech Services** | $0 (F0) | **HIGH** | 2 files (deep SDK integration, streaming) |
| 5 | **Key Vault** | ~$0.06 | LOW | 2 files (1 TS, 1 Python) |
| 6 | **Notification Hubs** | $0 (Free) | MEDIUM | Planned, not yet deeply integrated |
| 7 | **Application Insights** | $0 (5GB free) | LOW | 1 file (custom telemetry already built) |
| 8 | **Azure Identity** (DefaultAzureCredential) | $0 | LOW | Used by Key Vault + Secrets Manager |
---
## 2. Current Azure Investment Inventory
### 2.1 Azure Resources (from Azure Portal)
| Resource | Azure Name | Region | SKU | Status |
|----------|-----------|--------|-----|--------|
| Resource Group | `rg-mywisprai` | East US | — | Active |
| Cosmos DB | `cosmos-mywisprai` | West US 2 | Serverless | Active — 3 DBs, ~45 containers |
| Blob Storage | `bytelystblobs` | West US 2 | StorageV2, RAGRS | Active — 9+ containers |
| Azure OpenAI | `mywisprai-openai-sweden` | Sweden Central | S0 | Active — gpt-4o-mini deployment |
| Speech Service | `mywisprai-speech` | East US | F0 (Free) | Active |
| Key Vault | `kv-mywisprai` | East US | Standard | Active — ~25 secrets |
| Notification Hubs | `lysnnai` namespace | East US | Free | Active — 2 hubs |
| App Insights | `bytelyst-appinsights` | East US | Classic | Active |
### 2.2 Cosmos DB Databases & Containers
| Database | Containers | Products Using |
|----------|-----------|----------------|
| `lysnrai` | ~27 containers (users, subscriptions, feature_flags, audit_log, tracker_items, telemetry_events, etc.) | LysnrAI, platform-service (all products) |
| `mindlyst` | ~20 containers (brains, memory_items, streaks, reflections, etc.) | MindLyst |
| `mywisprai` | 10 containers (legacy, pre-rebrand) | Legacy / migration target |
**Total: ~57 containers across 3 databases**, all using Cosmos SQL (NoSQL) API with SQL-like queries (`SELECT`, `WHERE`, `ORDER BY`, `OFFSET/LIMIT`, aggregate functions).
### 2.3 Code Investment by Language
| Language | Azure SDK Packages | Files Using Azure | Lines of Azure-Specific Code |
|----------|-------------------|-------------------|------------------------------|
| **TypeScript** | `@azure/cosmos`, `@azure/storage-blob`, `@azure/identity`, `@azure/keyvault-secrets` | ~65 files | ~500 lines |
| **Python** | `azure-cognitiveservices-speech`, `azure-cosmos`, `azure-storage-blob`, `azure-identity`, `azure-keyvault-secrets`, `openai` (AzureOpenAI) | ~8 files | ~400 lines |
| **Swift** | `MicrosoftCognitiveServicesSpeech` (SPX framework) | ~3 files | ~150 lines |
| **Kotlin** | None directly (uses platform-service REST API) | 0 files | 0 lines |
---
## 3. Dependency Depth Analysis
### 3.1 Cosmos DB — DEEP (56+ files)
This is the **most deeply embedded** Azure dependency. Every repository module follows the pattern:
```
types.ts → repository.ts → routes.ts
Uses @azure/cosmos SDK
SQL queries: SELECT c.id, c.name FROM c WHERE c.productId = @pid
```
**Touchpoints:**
- `packages/cosmos/` — shared client singleton (`@azure/cosmos` peer dep)
- `services/platform-service/src/modules/*/repository.ts`**56 repository files** with Cosmos SQL queries
- `services/extraction-service/src/modules/*/repository.ts` — 2 repository files
- `dashboards/admin-web/src/lib/cosmos.ts` — direct `@azure/cosmos` import
- `dashboards/admin-web/src/lib/repositories/*.ts` — 4 repository files
- `mindlyst-native/web/src/lib/cosmos.ts` — direct `@azure/cosmos` import
- `learning_voice_ai_agent/src/cloud/cosmos_client.py` — Python Cosmos client
- `learning_voice_ai_agent/backend/src/cloud/cosmos.py` — Python backend Cosmos client
**Query patterns used:**
- `container.items.query()` with parameterized SQL
- `container.items.create()`, `.replace()`, `.delete()`, `.read()`
- `container.items.upsert()`
- Partition key routing (`/userId`, `/productId`, `/id`)
- Cross-partition queries (admin/analytics)
- `SELECT VALUE COUNT(1)` aggregates
- `OFFSET ... LIMIT` pagination
- `ORDER BY` sorting
- `ARRAY_CONTAINS()` for array queries
### 3.2 Azure Speech SDK — DEEP (3 files, streaming integration)
The Speech SDK is used for **real-time streaming speech-to-text** with features that are tightly coupled to the Azure SDK's event-driven architecture:
- `src/audio/azure_stt.py` — 248 lines. Uses `PushAudioInputStream`, `SpeechRecognizer`, continuous recognition with `recognizing`/`recognized`/`canceled`/`session_stopped` event callbacks, `PhraseListGrammar`, auto-language detection (10 languages), auto-reconnect
- `src/ui/settings.py` + `src/ui/unified_window.py` — connection testing
- `mindlyst-native/iosApp/Services/AzureSpeechTranscriber.swift` — iOS Swift SPX framework
- `mobile_app/ios/LysnrAI/` — iOS keyboard extension uses SPX framework
### 3.3 Blob Storage — SHALLOW (3 files)
- `packages/blob/src/blob.ts` — 162 lines, singleton client, SAS URL generation
- `src/cloud/blob_client.py` — 190 lines, Python equivalent
- `services/platform-service/src/modules/blob/` — REST API wrapper
### 3.4 Azure OpenAI — SHALLOW (3 files, already abstracted)
- `src/llm/text_cleaner.py` — uses `openai.AzureOpenAI` (OpenAI SDK with Azure endpoint)
- `backend/src/clients/openai_client.py` — uses `openai.AsyncAzureOpenAI`
- `mindlyst-native/web/src/lib/llm.ts`**already has OpenAI fallback** (resolves provider dynamically)
The `openai` Python/JS SDK supports both Azure and OpenAI endpoints with minimal config change. MindLyst web already handles this automatically.
### 3.5 Key Vault — SHALLOW (2 files)
- `packages/config/src/keyvault.ts` — 90 lines, `resolveKeyVaultSecrets()` with graceful fallback
- `src/secrets/keyvault.py` — 69 lines, `SecretResolver` class with env var fallback
Both implementations already fall back to environment variables when Key Vault is unavailable. Migration = just stop using Key Vault and use the env var path.
### 3.6 Notification Hubs — NOT YET INTEGRATED
Planned but not deeply wired. Only namespace/hub exists in Azure. Mobile apps use `BLPlatformClient` (REST) to talk to platform-service, which would route push notifications.
### 3.7 Application Insights — SHALLOW (1 file)
- `opencensus-ext-azure` in Python requirements (optional telemetry)
- Custom telemetry system already built (`@bytelyst/telemetry-client`, platform-service telemetry module with Cosmos storage)
The custom telemetry system means App Insights is supplementary, not critical.
---
## 4. Migration Target Comparison
### 4.1 Database: Cosmos DB → Alternatives
| Feature | Azure Cosmos DB (current) | MongoDB Atlas | AWS DynamoDB | Google Firestore | PostgreSQL (Supabase/Neon) |
|---------|--------------------------|---------------|-------------|-----------------|---------------------------|
| **Data model** | Document (JSON) | Document (JSON) | Key-Value + Document | Document (JSON) | Relational + JSONB |
| **Query language** | SQL-like | MQL (MongoDB Query) | PartiQL / API | GQL-like API | SQL |
| **Partition keys** | Required | Shard keys (optional) | Required | Collection groups | Not applicable |
| **Serverless** | Yes | Yes (Atlas Serverless) | Yes | Yes | Yes (Neon) |
| **SQL queries** | `SELECT c.id FROM c WHERE c.x = @y` | `db.collection.find({x: y})` | `SELECT id FROM table WHERE x = ?` | Client SDK queries | Standard SQL |
| **Aggregates** | Basic (`COUNT`, `SUM`, `AVG`) | Full (`$group`, `$match`, `$lookup`) | Limited | Limited | Full SQL |
| **Cross-partition** | Yes (expensive) | Yes (scatter-gather) | Scan (expensive) | Yes | N/A |
| **Change feed** | Yes | Change Streams | DynamoDB Streams | Real-time listeners | Logical replication |
| **Global distribution** | Built-in multi-region | Atlas Global Clusters | Global Tables | Multi-region | Manual / Citus |
| **Max doc size** | 2 MB | 16 MB | 400 KB | 1 MB | Unlimited (JSONB) |
| **Free tier** | 1000 RU/s + 25 GB | 512 MB | 25 GB + 25 WCU/RCU | 1 GiB + 50K reads/day | 0.5 GB (Neon) |
| **Migration effort** | — | **Medium** (query rewrite) | **Hard** (paradigm shift) | **Hard** (no SQL) | **Hard** (schema design) |
### 4.2 Object Storage: Blob → Alternatives
| Feature | Azure Blob (current) | AWS S3 | GCP Cloud Storage | Cloudflare R2 | MinIO (self-hosted) |
|---------|---------------------|--------|-------------------|---------------|---------------------|
| **API compatibility** | Azure Blob API | S3 API | GCS API / S3-compat | S3-compatible | S3-compatible |
| **SAS tokens** | Yes (Azure SAS) | Pre-signed URLs | Signed URLs | Pre-signed URLs | Pre-signed URLs |
| **CDN integration** | Azure CDN | CloudFront | Cloud CDN | Built-in | Manual |
| **Cost (per GB)** | $0.018 (Cool) | $0.023 (Standard) | $0.020 | $0.015 (no egress) | Self-hosted |
| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** |
### 4.3 Speech-to-Text: Azure Speech → Alternatives
| Feature | Azure Speech (current) | AWS Transcribe | Google Speech-to-Text | Deepgram | Whisper (local) |
|---------|----------------------|----------------|----------------------|----------|-----------------|
| **Streaming STT** | Yes (push stream) | Yes (WebSocket) | Yes (streaming) | Yes (WebSocket) | No (batch only) |
| **Languages** | 100+ | 100+ | 125+ | 36+ | 99+ |
| **Auto-detect lang** | Up to 10 at-once | Yes | Yes | Yes | Yes |
| **Custom vocabulary** | PhraseListGrammar | Custom vocabulary | Speech adaptation | Keywords | No |
| **Native SDK** | Python, Swift (SPX), JS | Python, no iOS SDK | Python, iOS, JS | REST/WebSocket | Python only |
| **iOS native SDK** | SPX framework (ObjC) | No native SDK | Yes (gRPC) | No native SDK | No |
| **Free tier** | 5 hrs/month (F0) | 60 min/month | 60 min/month | None | Free (local GPU) |
| **Latency** | ~200ms | ~300ms | ~200ms | ~100ms | ~500ms+ (local) |
| **Migration effort** | — | **Hard** (no iOS SDK) | **Medium** (has iOS SDK) | **Medium** (REST only) | **Hard** (no streaming) |
### 4.4 LLM / AI: Azure OpenAI → Alternatives
| Feature | Azure OpenAI (current) | OpenAI API (direct) | Google Gemini | AWS Bedrock | Anthropic Claude |
|---------|----------------------|--------------------|--------------|-----------| -----------------|
| **Models** | GPT-4o, GPT-4o-mini | Same models | Gemini 2.5 | Claude, Llama, Titan | Claude 3.5/4 |
| **API compatibility** | OpenAI SDK (azure mode) | OpenAI SDK (native) | Google SDK | AWS SDK | Anthropic SDK |
| **Data residency** | Azure regions | US only | Google regions | AWS regions | US/EU |
| **Cost (GPT-4o-mini)** | $0.15/$0.60 per M tokens | $0.15/$0.60 per M tokens | ~$0.10/$0.40 (Flash) | Varies | ~$0.25/$1.25 (Haiku) |
| **Migration effort** | — | **Trivial** (change endpoint) | **Easy** (SDK swap) | **Medium** | **Easy** (SDK swap) |
### 4.5 Secrets Management: Key Vault → Alternatives
| Feature | Azure Key Vault (current) | AWS Secrets Manager | GCP Secret Manager | HashiCorp Vault | Doppler / Infisical |
|---------|--------------------------|--------------------|--------------------|-----------------|---------------------|
| **Cost** | $0.03/10K ops | $0.40/secret/month | $0.06/10K ops | Free (OSS) | Free tier |
| **SDK** | `@azure/keyvault-secrets` | `@aws-sdk/client-secrets-manager` | `@google-cloud/secret-manager` | HTTP API | SDK / CLI |
| **Migration effort** | — | **Easy** | **Easy** | **Medium** | **Easy** |
**Note:** The codebase already falls back to env vars when Key Vault is unavailable. This means Key Vault can be replaced by **any** secrets manager or simply .env files without code changes to application logic.
### 4.6 Push Notifications: Notification Hubs → Alternatives
| Feature | Azure NH (current) | AWS SNS | Firebase Cloud Messaging | OneSignal | Expo Push |
|---------|-------------------|---------|--------------------------|-----------|-----------|
| **APNs + FCM** | Yes | Yes | FCM only (APNs via FCM) | Yes | Yes |
| **Free tier** | 1M pushes/month | 1M publishes | Unlimited | 10K subscribers | Unlimited |
| **Migration effort** | — | **Easy** | **Easy** | **Easy** | **Easy** (NomGap uses Expo) |
---
## 5. Per-Service Migration Analysis
### 5.1 Cosmos DB → MongoDB Atlas
**Difficulty: MEDIUM-HIGH** | **Effort: 35 weeks** | **Risk: MEDIUM**
This is the **single largest migration task**. Here's why:
#### What needs to change
| Layer | Current (Cosmos SQL API) | Target (MongoDB) | Files |
|-------|--------------------------|-------------------|-------|
| Client package | `@azure/cosmos``CosmosClient` | `mongodb``MongoClient` | `packages/cosmos/src/client.ts` |
| Container registry | `getContainer(name)` | `db.collection(name)` | `packages/cosmos/src/containers.ts` |
| All repository files | `container.items.query('SELECT...')` | `collection.find({...})` | **56+ files** in platform-service |
| Dashboard Cosmos clients | `@azure/cosmos` direct | `mongodb` direct | 2 files (admin, MindLyst) |
| Python clients | `azure.cosmos.CosmosClient` | `pymongo.MongoClient` | 2 files |
| Query syntax | SQL-like (`SELECT c.id FROM c WHERE c.productId = @pid AND c.userId = @uid ORDER BY c.createdAt DESC OFFSET 0 LIMIT 20`) | MQL (`collection.find({productId: pid, userId: uid}).sort({createdAt: -1}).skip(0).limit(20)`) | All repository files |
| Partition keys | Explicit partition key in every query | Shard key (auto-routed) | All repository files |
| Upsert | `container.items.upsert(doc)` | `collection.updateOne({_id: id}, {$set: doc}, {upsert: true})` | ~20 files |
| Read by ID | `container.item(id, partitionKey).read()` | `collection.findOne({_id: id})` | All repository files |
#### What stays the same
- Document structure (JSON documents with `id`, `productId`, partition keys)
- Data model (no schema changes needed — MongoDB is also schemaless)
- Partition key concept maps to shard key
- Serverless pricing model available on both
#### Key migration steps
1. Update `@bytelyst/cosmos` package to export MongoDB-compatible API
2. Rewrite all SQL queries to MQL (56+ files)
3. Replace `container.items.query()``collection.find()`
4. Replace `container.item(id, pk).read()``collection.findOne({_id: id})`
5. Replace `container.items.create()``collection.insertOne()`
6. Replace `container.items.replace()``collection.replaceOne()`
7. Replace `container.items.upsert()``collection.updateOne({upsert: true})`
8. Update Python clients similarly
9. Migrate data (use Azure Data Factory or custom script)
10. Update all test mocks
#### Why MongoDB Atlas is the best DB alternative
- **Closest query model** to Cosmos SQL API (both are document DBs)
- **MongoDB has a Cosmos DB compatibility mode** (but going native is better)
- Cosmos DB was originally inspired by MongoDB's document model
- MongoDB's `find()` queries map closely to Cosmos SQL `SELECT` queries
- Both support partition/shard keys, TTL indexes, change streams
- MongoDB Atlas Serverless pricing is competitive
- MongoDB has excellent TypeScript and Python SDKs
### 5.2 Azure Speech → Google Cloud Speech-to-Text
**Difficulty: HIGH** | **Effort: 23 weeks** | **Risk: HIGH**
#### Why this is hard
- The Azure Speech SDK uses a **push-stream architecture** (`PushAudioInputStream`) that is deeply integrated into the audio pipeline
- The `SpeechRecognizer` has event-driven callbacks (`recognizing`, `recognized`, `canceled`, `session_stopped`) that the code relies on for real-time partial/final transcript delivery
- Custom vocabulary via `PhraseListGrammar` is Azure-specific
- Auto-language detection config is Azure-specific
- The **iOS SPX framework** (Objective-C) is used in LysnrAI keyboard extension and MindLyst — there's no direct equivalent for most alternatives
#### Best alternative: Google Cloud Speech-to-Text
- Has streaming recognition with similar event model
- Has an iOS SDK (gRPC-based)
- Supports custom vocabulary (speech adaptation)
- Supports auto-language detection
- Similar pricing and free tier
#### What needs to change
- `src/audio/azure_stt.py` — complete rewrite (~248 lines)
- `iosApp/Services/AzureSpeechTranscriber.swift` — complete rewrite
- `LysnrAI/LysnrKeyboard/` — keyboard extension STT integration
- Audio format handling (may differ between providers)
- Connection test code in settings UI
### 5.3 Blob Storage → AWS S3 or Cloudflare R2
**Difficulty: LOW** | **Effort: 23 days** | **Risk: LOW**
#### Why this is easy
- `@bytelyst/blob` package is a thin wrapper (162 lines)
- Only 3 files need changes
- S3 API is the de facto standard — R2, MinIO, GCS all support S3-compatible API
- SAS tokens → Pre-signed URLs (same concept, different implementation)
#### What needs to change
- `packages/blob/src/blob.ts` — swap `@azure/storage-blob``@aws-sdk/client-s3` + `@aws-sdk/s3-request-presigner`
- `src/cloud/blob_client.py` — swap `azure.storage.blob``boto3`
- `services/platform-service/src/modules/blob/` — update routes for pre-signed URL format
- Environment variables: `AZURE_BLOB_*``AWS_S3_*` or `S3_*`
### 5.4 Azure OpenAI → OpenAI API (direct) or Gemini
**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW**
#### Why this is trivial
- The `openai` Python SDK supports both Azure and OpenAI endpoints — just change config
- MindLyst web `llm.ts` **already auto-detects** Azure vs OpenAI and builds the correct URL
- LysnrAI desktop uses `AzureOpenAI` class from `openai` SDK — switch to `OpenAI` class
- Same models, same API shape, same pricing
#### What needs to change
- Set `OPENAI_API_KEY` instead of `AZURE_OPENAI_*` env vars
- Change `AzureOpenAI(azure_endpoint=..., api_key=..., api_version=...)``OpenAI(api_key=...)`
- Change `AsyncAzureOpenAI(...)``AsyncOpenAI(...)`
- Remove `api_version` parameter
- That's it. The `openai` SDK handles the rest.
### 5.5 Key Vault → Environment Variables / Any Secrets Manager
**Difficulty: TRIVIAL** | **Effort: < 1 day** | **Risk: VERY LOW**
Both `keyvault.ts` and `keyvault.py` already implement graceful fallback:
- If `AZURE_KEYVAULT_URL` is not set → uses env vars directly
- If Key Vault is unreachable → falls back to env vars
**To migrate:** Simply stop setting `AZURE_KEYVAULT_URL`. Everything works via env vars. Then optionally adopt any other secrets manager (AWS Secrets Manager, Doppler, Infisical, etc.).
### 5.6 Notification Hubs → Firebase Cloud Messaging
**Difficulty: LOW** | **Effort: 12 days** | **Risk: LOW**
Not yet deeply integrated. The platform-service notification module sends via REST API. Swap the push provider client.
### 5.7 Application Insights → Self-hosted / Grafana
**Difficulty: TRIVIAL** | **Effort: Already done** | **Risk: NONE**
The ecosystem already has:
- Custom telemetry system (`@bytelyst/telemetry-client` → platform-service → Cosmos)
- Loki + Grafana in `services/monitoring/`
- App Insights is supplementary, can be dropped with zero code changes
---
## 6. Migration Scenario Scoring
### Scenario A: Stay on Azure (Status Quo)
| Dimension | Score (1-5) | Notes |
|-----------|-------------|-------|
| Migration effort | **5** (none) | No work needed |
| Cost | **4** | ~$15/month at current scale, competitive |
| Vendor diversity | **1** | Single cloud vendor |
| Feature parity | **5** | Everything works today |
| **Total** | **15/20** | |
### Scenario B: Full Migration to AWS
| Dimension | Score (1-5) | Notes |
|-----------|-------------|-------|
| Migration effort | **2** | 68 weeks, Cosmos→DynamoDB is painful |
| Cost | **3** | Similar or slightly higher at small scale |
| Vendor diversity | **1** | Still single cloud, just different |
| Feature parity | **3** | No native iOS Speech SDK, DynamoDB query model is very different |
| **Total** | **9/20** | |
### Scenario C: Multi-Cloud (MongoDB Atlas + OpenAI + R2 + Google STT)
| Dimension | Score (1-5) | Notes |
|-----------|-------------|-------|
| Migration effort | **2** | 57 weeks, Cosmos→MongoDB is medium |
| Cost | **4** | MongoDB Atlas free tier, R2 no egress fees |
| Vendor diversity | **5** | No single-vendor dependency |
| Feature parity | **4** | MongoDB is a better document DB than Cosmos in many ways |
| **Total** | **15/20** | |
### Scenario D: Stay Azure + Add Abstraction Layers
| Dimension | Score (1-5) | Notes |
|-----------|-------------|-------|
| Migration effort | **4** | 12 weeks to add repository interface pattern |
| Cost | **4** | No change |
| Vendor diversity | **3** | Ready to switch, but still on Azure |
| Feature parity | **5** | Everything works today |
| **Total** | **16/20** | **Winner** |
### Scenario E: Migrate DB Only (Cosmos → MongoDB Atlas, keep rest on Azure)
| Dimension | Score (1-5) | Notes |
|-----------|-------------|-------|
| Migration effort | **3** | 35 weeks for DB migration |
| Cost | **4** | MongoDB Atlas Serverless may be cheaper |
| Vendor diversity | **3** | DB is independent, other services still Azure |
| Feature parity | **5** | MongoDB is very capable |
| **Total** | **15/20** | |
---
## 7. Cost Comparison
### Current Azure Costs (MVP / Low Usage)
| Service | Monthly Cost | Notes |
|---------|-------------|-------|
| Cosmos DB (Serverless) | ~$410 | 3 databases, ~45 containers |
| Blob Storage (Cool, RAGRS) | ~$0.20 | 9+ containers |
| Azure OpenAI (GPT-4o-mini) | ~$510 | Pay per token |
| Speech (F0) | $0 | 5 hrs/month free |
| Key Vault | ~$0.06 | ~25 secrets |
| Notification Hubs (Free) | $0 | 1M pushes/month |
| App Insights | $0 | 5 GB/month free |
| **Total** | **~$1020/month** | |
### Equivalent AWS Costs
| Service | AWS Equivalent | Monthly Cost |
|---------|---------------|-------------|
| Cosmos DB → DynamoDB (On-Demand) | DynamoDB | ~$515 |
| Blob → S3 Standard | S3 | ~$0.25 |
| Azure OpenAI → OpenAI API | Same pricing | ~$510 |
| Speech → Transcribe | Transcribe | ~$13 |
| Key Vault → Secrets Manager | Secrets Manager | ~$10 (per-secret pricing) |
| Notification Hubs → SNS | SNS | ~$0.50 |
| App Insights → CloudWatch | CloudWatch | ~$3 |
| **Total** | | **~$2542/month** |
### Equivalent Multi-Cloud Costs
| Service | Provider | Monthly Cost |
|---------|---------|-------------|
| Cosmos DB → MongoDB Atlas Serverless | MongoDB | ~$38 |
| Blob → Cloudflare R2 | Cloudflare | ~$0.15 (no egress) |
| Azure OpenAI → OpenAI API (direct) | OpenAI | ~$510 |
| Speech → Google STT | Google Cloud | ~$13 |
| Key Vault → Doppler (free tier) | Doppler | $0 |
| Push → Firebase FCM | Google | $0 |
| Monitoring → Grafana Cloud (free) | Grafana | $0 |
| **Total** | | **~$1022/month** |
### Cost Summary
| Scenario | Monthly Cost | vs Current |
|----------|-------------|-----------|
| **Azure (current)** | ~$1020 | Baseline |
| **Full AWS** | ~$2542 | +50110% |
| **Multi-cloud** | ~$1022 | ~Same |
| **MongoDB Atlas + Azure rest** | ~$1018 | ~Same |
**Verdict:** At current scale, cost is not a compelling reason to migrate. All options are under $50/month. Cost becomes more significant at scale (10K+ users), where MongoDB Atlas and R2 would likely be cheaper due to no egress fees and better serverless pricing.
---
## 8. Abstraction Layer Assessment
### Current State: Partially Abstracted
The codebase already has meaningful abstraction:
| Layer | Abstraction Level | Notes |
|-------|-------------------|-------|
| **Cosmos DB** | **Partial**`@bytelyst/cosmos` package | Application code still writes raw SQL queries and uses `@azure/cosmos` types |
| **Blob Storage** | **Good**`@bytelyst/blob` package | Thin wrapper, easy to swap internals |
| **OpenAI/LLM** | **Good** — MindLyst has provider auto-detection | LysnrAI desktop/backend hardcodes `AzureOpenAI` |
| **Key Vault** | **Excellent** — graceful fallback to env vars | Already cloud-agnostic in practice |
| **Speech** | **None** — raw SDK usage | Deep Azure SDK coupling in 3 files |
| **Auth (JWT)** | **Excellent** — uses `jose` library | No cloud dependency |
| **Push notifications** | **Good** — platform-service abstraction | Swap provider client only |
### What's Missing: Repository Interface Pattern
The biggest gap is that repository files directly use `@azure/cosmos` types and SQL query syntax. To make the DB layer swappable, you'd need:
```typescript
// Proposed: packages/cosmos/src/repository.ts
export interface DocumentRepository<T> {
findById(id: string, partitionKey: string): Promise<T | null>;
findMany(filter: Record<string, unknown>, opts?: QueryOptions): Promise<T[]>;
create(doc: T): Promise<T>;
replace(id: string, doc: T, partitionKey: string): Promise<T>;
upsert(doc: T): Promise<T>;
delete(id: string, partitionKey: string): Promise<void>;
count(filter: Record<string, unknown>): Promise<number>;
}
```
This would allow swapping Cosmos → MongoDB → PostgreSQL behind the interface without touching 56+ repository files.
**Effort to add:** 12 weeks. This is the **highest-ROI investment** regardless of migration decision.
---
## 9. Risk Analysis
### 9.1 Risks of Staying on Azure
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|-----------|
| Azure pricing increases | Low | Medium | Add abstraction layer for future portability |
| Azure outage | Low | High | Multi-region already possible (Cosmos global distribution) |
| Feature stagnation | Very Low | Low | Azure is investing heavily in AI services |
| Vendor lock-in deepens over time | Medium | Medium | Add abstraction layers proactively |
### 9.2 Risks of Migrating
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|-----------|
| Data loss during migration | Low | Critical | Test migration on staging first, keep Azure as backup |
| Query performance differences | Medium | Medium | Benchmark before committing |
| Feature gaps in new provider | Medium | Medium | Prototype critical features first |
| Wasted engineering time | Medium | High | Only migrate if there's a clear business driver |
| Regression bugs in 56+ repository files | High | Medium | Comprehensive test suite (1,029 tests) catches most issues |
| Speech quality degradation | Medium | High | A/B test both providers before committing |
### 9.3 Azure-Specific Lock-in Risks (ranked)
| # | Component | Lock-in Level | Escape Hatch |
|---|-----------|--------------|-------------|
| 1 | **Cosmos DB SQL API** | High | Rewrite queries to MongoDB MQL or add repository interface |
| 2 | **Azure Speech SDK (streaming)** | High | Google STT has comparable streaming API |
| 3 | **Azure Identity (DefaultAzureCredential)** | Medium | Only used by Key Vault, which is already optional |
| 4 | **Blob Storage SAS tokens** | Low | Pre-signed URLs are equivalent across all providers |
| 5 | **Azure OpenAI** | Very Low | OpenAI SDK works with both — 1-line config change |
| 6 | **Key Vault** | Very Low | Already has env var fallback |
| 7 | **Notification Hubs** | Very Low | Not deeply integrated yet |
| 8 | **Application Insights** | None | Custom telemetry already built |
---
## 10. Recommendations
### Recommended Strategy: **Stay on Azure + Invest in Abstraction** (Scenario D)
This is the highest-scoring approach. Here's the prioritized action plan:
#### Phase 1: Add Repository Interface (12 weeks)
- Create `DocumentRepository<T>` interface in `@bytelyst/cosmos`
- Implement `CosmosDocumentRepository<T>` that wraps current `@azure/cosmos` calls
- Gradually migrate the 56 repository files to use the interface
- This makes future DB migration a matter of implementing `MongoDocumentRepository<T>` — no application code changes needed
#### Phase 2: Normalize LLM Abstraction (23 days)
- Move LysnrAI desktop/backend from `AzureOpenAI` → auto-detecting provider pattern (like MindLyst web already does)
- Support `OPENAI_PROVIDER=azure|openai|gemini` across all repos
- This makes LLM provider swappable via config
#### Phase 3: Speech Abstraction Layer (1 week, optional)
- Create `SpeechTranscriber` protocol/interface
- Implement `AzureSpeechTranscriber` (current code, extracted)
- Prepare `GoogleSpeechTranscriber` stub for future use
- This is lower priority since Azure Speech F0 tier is free
#### Phase 4: Document Decision Criteria for Future Migration
- Define triggers that would justify migration (e.g., cost > $X/month, Azure outage > Y hours, need for feature Z)
- Review annually
### Why NOT Migrate Now
1. **Cost is negligible** — ~$1020/month doesn't justify weeks of engineering
2. **No business driver** — Azure isn't blocking any feature development
3. **Risk/reward is unfavorable** — 48 weeks of migration work for ~$0 cost savings
4. **Test coverage is good but not perfect** — 1,029 tests cover most paths, but query-level changes in 56 files still risk regressions
5. **Azure free tiers are generous** — Speech F0, Notification Hubs Free, App Insights free tier
### When Migration WOULD Make Sense
- **Cosmos DB costs exceed $100/month** → Consider MongoDB Atlas Serverless
- **Azure Speech quality is insufficient** → Evaluate Google STT or Deepgram
- **Enterprise customer requires specific cloud** → Build the repository interface, then implement their cloud backend
- **Azure has extended outage affecting your region** → Multi-region or multi-cloud
- **You want to go fully open-source** → PostgreSQL (Supabase) + Whisper + MinIO (significant rewrite)
---
## 11. Migration Playbook (If Chosen)
If you decide to migrate in the future, here's the execution order (shortest critical path):
### Week 12: Database Abstraction
1. Create `DocumentRepository<T>` interface
2. Implement `CosmosDocumentRepository<T>` (wraps current code)
3. Migrate all 56 repository files to use interface
4. Verify all 1,029 tests pass
### Week 34: Database Migration (Cosmos → MongoDB)
1. Implement `MongoDocumentRepository<T>`
2. Set up MongoDB Atlas Serverless cluster
3. Write data migration script (Cosmos → MongoDB)
4. Run migration on staging, verify data integrity
5. Switch repository implementation via config flag
6. Run full test suite against MongoDB
### Week 5: Storage + Secrets
1. Swap `@bytelyst/blob` internals to S3-compatible client
2. Migrate blobs (azcopy → aws s3 sync or similar)
3. Replace Key Vault with new secrets manager (or just env vars)
4. Update all environment variable names
### Week 6: LLM + Speech (if needed)
1. Switch OpenAI from Azure endpoint to direct (config change only)
2. If migrating Speech: rewrite `azure_stt.py` and Swift `AzureSpeechTranscriber`
3. A/B test new speech provider against Azure
### Week 78: Cleanup + Verification
1. Remove all `@azure/*` npm packages
2. Remove all `azure-*` pip packages
3. Update Docker configs, CI/CD
4. Update documentation
5. Monitor production for 2 weeks
---
## Appendix A: File-Level Azure Dependency Map
### TypeScript — `@azure/cosmos` (CRITICAL)
| File | Repo | Direct Import |
|------|------|---------------|
| `packages/cosmos/src/client.ts` | common-plat | `@azure/cosmos` |
| `packages/cosmos/src/containers.ts` | common-plat | `@azure/cosmos` |
| `services/platform-service/src/modules/*/repository.ts` (56 files) | common-plat | Via `@bytelyst/cosmos` |
| `services/extraction-service/src/modules/*/repository.ts` (2 files) | common-plat | Via `@bytelyst/cosmos` |
| `dashboards/admin-web/src/lib/cosmos.ts` | common-plat | `@azure/cosmos` |
| `dashboards/admin-web/src/lib/repositories/*.ts` (4 files) | common-plat | Via cosmos.ts |
| `mindlyst-native/web/src/lib/cosmos.ts` | MindLyst | `@azure/cosmos` |
### TypeScript — `@azure/storage-blob`
| File | Repo | Direct Import |
|------|------|---------------|
| `packages/blob/src/blob.ts` | common-plat | `@azure/storage-blob` |
### TypeScript — `@azure/identity` + `@azure/keyvault-secrets`
| File | Repo | Direct Import |
|------|------|---------------|
| `packages/config/src/keyvault.ts` | common-plat | Dynamic import (both) |
| `dashboards/admin-web/src/app/api/ops/secrets/route.ts` | common-plat | Both (Secrets Manager UI) |
### Python — Azure SDKs
| File | Repo | SDK |
|------|------|-----|
| `src/audio/azure_stt.py` | LysnrAI | `azure.cognitiveservices.speech` |
| `src/cloud/cosmos_client.py` | LysnrAI | `azure.cosmos` |
| `src/cloud/blob_client.py` | LysnrAI | `azure.storage.blob` |
| `src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` |
| `backend/src/secrets/keyvault.py` | LysnrAI | `azure.identity`, `azure.keyvault.secrets` |
| `backend/src/cloud/cosmos.py` | LysnrAI | `azure.cosmos` |
| `src/llm/text_cleaner.py` | LysnrAI | `openai.AzureOpenAI` |
| `backend/src/clients/openai_client.py` | LysnrAI | `openai.AsyncAzureOpenAI` |
### Swift — Azure Speech SDK
| File | Repo | SDK |
|------|------|-----|
| `iosApp/Services/AzureSpeechTranscriber.swift` | MindLyst | `MicrosoftCognitiveServicesSpeech` |
| `LysnrAI/LysnrKeyboard/KeyboardViewController.swift` | LysnrAI | SPX framework (via CocoaPods) |
---
## Appendix B: SDK & Package Inventory
### npm packages (TypeScript)
| Package | Version | Used By | Swappable |
|---------|---------|---------|-----------|
| `@azure/cosmos` | ≥4.0.0 | `@bytelyst/cosmos`, admin-web, MindLyst web | Medium (query rewrite) |
| `@azure/storage-blob` | ≥12.0.0 | `@bytelyst/blob` | Easy (S3 compat) |
| `@azure/identity` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) |
| `@azure/keyvault-secrets` | latest | `@bytelyst/config`, admin-web secrets | Easy (remove) |
### pip packages (Python)
| Package | Version | Used By | Swappable |
|---------|---------|---------|-----------|
| `azure-cognitiveservices-speech` | ≥1.42.0 | Desktop STT | Hard (deep SDK integration) |
| `azure-cosmos` | latest | Desktop + backend Cosmos client | Medium (pymongo swap) |
| `azure-storage-blob` | ≥12.24.0 | Desktop blob client | Easy (boto3 swap) |
| `azure-identity` | ≥1.19.0 | Key Vault auth | Easy (remove) |
| `azure-keyvault-secrets` | ≥4.9.0 | Secrets resolver | Easy (remove) |
| `openai` | ≥1.60.0 | `AzureOpenAI` / `AsyncAzureOpenAI` | Trivial (change class name) |
| `opencensus-ext-azure` | ≥1.1.0 | Optional telemetry | Trivial (remove) |
### Swift packages / CocoaPods
| Package | Used By | Swappable |
|---------|---------|-----------|
| `MicrosoftCognitiveServicesSpeech` (SPX) | LysnrAI iOS, MindLyst iOS | Hard (need alternative streaming STT) |
---
*Document generated by automated codebase analysis. Numbers are accurate as of 2026-03-01. Update as the codebase evolves.*

View File

@ -0,0 +1,181 @@
# Azure Connection Audit — Full Workspace Report
> **Date:** 2026-02-22
> **Scope:** `learning_ai_common_plat`, `learning_voice_ai_agent`, `learning_multimodal_memory_agents`, `learning_ai_clock`, `learning_ai_fastgap`
> **Auditor:** Cascade (AI)
---
## Executive Summary
| Category | Issues Found | Fixed (session 1) | Fixed (session 2) | Remaining |
|----------|-------------|-------------------|-------------------|-----------|
| `x-request-id` missing | 12 clients | 2 (MindLyst) | **9** (root cause + feature-flags) | 0 ✅ |
| `x-product-id` missing | 6 clients | 0 | **6** (admin + user dashboards + Python) | 0 ✅ |
| Cosmos PK mismatch | 1 container | 0 (flagged) | 0 | 1 (needs migration) |
| `.env.example` gaps | 4 files | 1 (MindLyst) | **3** (ChronoMind, user-dash, admin-dash) | 0 ✅ |
| Hardcoded productId | 2 instances | 0 | **2** (telemetry.ts, platform_client.py) | 0 ✅ |
| Python client gaps | 1 file | 0 | **1** (headers + config) | 0 ✅ |
---
## 1. `x-request-id` Header — Root Cause
### Finding
**`@bytelyst/api-client` does NOT auto-inject `x-request-id`.**
The `createApiClient()` factory in `packages/api-client/src/client.ts` only sets `Content-Type`, auth token (via `getToken`), and caller-supplied `defaultHeaders`. No `x-request-id` is generated. This means **every consumer** that relies on `@bytelyst/api-client` without explicitly adding the header is missing request tracing.
### Root Cause Fix
Add `x-request-id: crypto.randomUUID()` to `buildHeaders()` in `packages/api-client/src/client.ts`. This single change propagates to all consumers automatically.
### Affected Clients (missing `x-request-id`)
| Repo | File | Client Pattern |
|------|------|---------------|
| `common_plat` | `dashboards/admin-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
| `common_plat` | `dashboards/admin-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
| `common_plat` | `dashboards/admin-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
| `common_plat` | `dashboards/tracker-web/src/lib/tracker-client.ts` | `createApiClient` — no `x-request-id` |
| `common_plat` | `packages/extraction/src/client.ts` | `createApiClient` — no `x-request-id` |
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | `createApiClient` — no `x-request-id` |
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | `createApiClient` — no `x-request-id` |
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | `createApiClient` — no `x-request-id` |
| `voice_ai_agent` | `user-dashboard-web/src/lib/feature-flags.ts` | Custom `fetch` — no `x-request-id` |
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | `httpx` — no `x-request-id` |
### Already Fixed (previous session)
| Repo | File | Status |
|------|------|--------|
| `multimodal_memory` | `web/src/lib/billing-client.ts` | ✅ Added via `defaultHeaders` |
| `multimodal_memory` | `web/src/lib/feature-flags.ts` | ✅ Added manually |
### Already Correct
| Repo | File | Status |
|------|------|--------|
| `ai_fastgap` (NomGap) | `src/api/client.ts` | ✅ Custom client with `crypto.randomUUID()` |
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts` | ✅ Custom client with `crypto.randomUUID()` |
| `voice_ai_agent` | `backend/src/main.py` | ✅ Middleware propagates/generates |
| `voice_ai_agent` | `backend/src/clients/extraction_client.py` | ✅ Passes `request_id` param |
---
## 2. `x-product-id` Header Gaps
### Clients Missing `x-product-id`
| Repo | File | Impact |
|------|------|--------|
| `common_plat` | `admin-web/src/lib/billing-client.ts` | Server can't filter by product |
| `common_plat` | `admin-web/src/lib/growth-client.ts` | Server can't filter by product |
| `voice_ai_agent` | `user-dashboard-web/src/lib/billing-client.ts` | Server can't filter by product |
| `voice_ai_agent` | `user-dashboard-web/src/lib/growth-client.ts` | Server can't filter by product |
| `voice_ai_agent` | `user-dashboard-web/src/lib/platform-client.ts` | Passes in body, not header |
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | Passes in body/params, not header |
### Already Correct
| Repo | File |
|------|------|
| `ai_fastgap` (NomGap) | `src/api/client.ts``x-product-id: API_CONFIG.productId` |
| `ai_clock` (ChronoMind) | `web/src/lib/platform-sync.ts``x-product-id` header |
| `multimodal_memory` (MindLyst) | `web/src/lib/billing-client.ts` — via `defaultHeaders` |
| `multimodal_memory` (MindLyst) | `web/src/lib/feature-flags.ts` — explicit header |
| `common_plat` | `tracker-web/src/lib/tracker-client.ts` — from `localStorage` |
---
## 3. Cosmos DB Partition Key Mismatch
### `referrals` Container — 3-way Mismatch
| Location | Partition Key |
|----------|--------------|
| `platform-service/src/lib/cosmos-init.ts` | `/id` |
| MindLyst `web/src/lib/cosmos.ts` | `/userId` |
| Admin dashboard `admin-web/src/lib/cosmos.ts` | `/referrerId` |
| User dashboard `user-dashboard-web/src/lib/cosmos.ts` | `/referrerId` |
**Status:** Flagged in previous session. Cannot be fixed without data migration. Comment added to `cosmos-init.ts`.
**Risk:** Cross-partition queries will silently succeed but may return incomplete results or fail on point reads if the wrong partition key is specified.
---
## 4. Missing Environment Variables in `.env.example` Files
### ChronoMind `web/.env.example`
Currently only has:
```
NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003/api
```
**Missing:**
- `NEXT_PUBLIC_PRODUCT_ID=chronomind` — used implicitly by `platform-sync.ts` (hardcoded there, but should be env-driven for consistency)
### LysnrAI `user-dashboard-web/.env.example`
**Missing:**
- `NEXT_PUBLIC_PRODUCT_ID=lysnrai` — referenced by `feature-flags.ts` line 10
- `NEXT_PUBLIC_PLATFORM_SERVICE_URL=http://localhost:4003` — referenced by `feature-flags.ts` line 11
Has `PLATFORM_SERVICE_URL` (server-side) but not the `NEXT_PUBLIC_` variant (client-side).
### LysnrAI root `.env.example`
**Missing:**
- `NEXT_PUBLIC_PRODUCT_ID` — not needed at root level (desktop app), so this is informational only.
### Admin dashboard `.env.example`
**Missing:**
- `AZURE_KEYVAULT_URL` — referenced by `instrumentation.ts` but not in `.env.example`
---
## 5. Hardcoded `productId` Values
| Repo | File | Line | Value | Should Use |
|------|------|------|-------|-----------|
| `multimodal_memory` | `web/src/lib/telemetry.ts` | 19 | `productId: 'mindlyst'` | `process.env.NEXT_PUBLIC_PRODUCT_ID` |
| `voice_ai_agent` | `backend/src/clients/platform_client.py` | 86, 101 | `product_id: str = "lysnrai"` | `settings.PRODUCT_ID` or config |
---
## 6. Python Backend Client Gaps (`platform_client.py`)
The `PlatformClient` class in `backend/src/clients/platform_client.py` has several issues:
1. **No `x-request-id` header** on any request
2. **No `x-product-id` header** on any request
3. **Creates new `httpx.AsyncClient` per request** — no connection pooling
4. **Hardcoded `product_id="lysnrai"` defaults** — should use config
---
## 7. Previously Fixed (Session 1)
| Fix | Repo | File |
|-----|------|------|
| Added `x-request-id` to billing client | `multimodal_memory` | `web/src/lib/billing-client.ts` |
| Added `x-request-id` to feature flags | `multimodal_memory` | `web/src/lib/feature-flags.ts` |
| Added 13 MindLyst containers to cosmos-init | `common_plat` | `services/platform-service/src/lib/cosmos-init.ts` |
| Added Blob Storage creds to Python config | `voice_ai_agent` | `backend/src/config.py` |
| Added missing env vars to MindLyst | `multimodal_memory` | `web/.env.example` |
---
## 8. Recommended Fix Order
1. **P0 — Root cause:** Add `x-request-id` auto-generation to `@bytelyst/api-client` `buildHeaders()` → fixes 9 TS clients at once
2. **P0 — LysnrAI feature-flags:** Add `x-request-id` to the custom `fetch` call in `user-dashboard-web/src/lib/feature-flags.ts`
3. **P1 — Python backend:** Add `x-request-id` and `x-product-id` headers to `platform_client.py`
4. **P1 — Env vars:** Add missing `NEXT_PUBLIC_*` vars to ChronoMind, LysnrAI user-dashboard, admin-dashboard `.env.example` files
5. **P2 — `x-product-id`:** Add to admin/user dashboard clients via `defaultHeaders` in `createApiClient` config
6. **P2 — Hardcoded productId:** Replace in `telemetry.ts` and `platform_client.py`
7. **P3 — Referrals PK mismatch:** Requires data migration strategy (separate task)

View File

@ -42,6 +42,9 @@ export function createApiClient(config: ApiClientConfig): ApiClient {
function buildHeaders(options?: RequestInit): HeadersInit {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'x-request-id': typeof globalThis.crypto?.randomUUID === 'function'
? globalThis.crypto.randomUUID()
: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`,
...defaultHeaders,
};

View File

@ -96,6 +96,10 @@ const CONTAINER_DEFS: Record<string, ContainerConfig> = {
changelog: { partitionKeyPath: '/productId' },
// Push notification triggers (NomGap)
push_triggers: { partitionKeyPath: '/productId', defaultTtl: 30 * 86400 },
// JarvisJr modules (agents, sessions, memory)
jarvis_agents: { partitionKeyPath: '/userId' },
jarvis_sessions: { partitionKeyPath: '/userId' },
jarvis_memory: { partitionKeyPath: '/agentId' },
};
export async function initCosmosIfNeeded(): Promise<void> {

View File

@ -0,0 +1,223 @@
/**
* JarvisJr agents module unit tests validates schema parsing, constants, and type guards.
*/
import { describe, it, expect } from 'vitest';
import {
CreateAgentSchema,
UpdateAgentSchema,
ListAgentsQuerySchema,
DIFFICULTY_LEVELS,
PRIVACY_LEVELS,
COACHING_FRAMEWORKS,
} from './types.js';
// ── Constants ──
describe('JarvisJr agent constants', () => {
it('has expected difficulty levels', () => {
expect(DIFFICULTY_LEVELS).toEqual(['beginner', 'intermediate', 'advanced', 'adaptive']);
});
it('has expected privacy levels', () => {
expect(PRIVACY_LEVELS).toEqual(['standard', 'local_only']);
});
it('has expected coaching frameworks', () => {
expect(COACHING_FRAMEWORKS).toContain('socratic');
expect(COACHING_FRAMEWORKS).toContain('star');
expect(COACHING_FRAMEWORKS).toContain('scamper');
expect(COACHING_FRAMEWORKS).toContain('immersive');
expect(COACHING_FRAMEWORKS).toContain('freeform');
expect(COACHING_FRAMEWORKS.length).toBe(7);
});
});
// ── CreateAgentSchema ──
describe('CreateAgentSchema', () => {
const validMinimal = {
name: 'Coach',
role: 'Communication coach',
systemPrompt: 'You are a communication coach who uses the Socratic method.',
voiceId: 'alloy',
};
it('accepts minimal valid input with defaults', () => {
const result = CreateAgentSchema.safeParse(validMinimal);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe('Coach');
expect(result.data.coachingFramework).toBe('freeform');
expect(result.data.accentColor).toBe('#7C6BFF');
expect(result.data.sessionLength).toBe(15);
expect(result.data.difficultyLevel).toBe('adaptive');
expect(result.data.language).toBe('en');
expect(result.data.privacyLevel).toBe('standard');
expect(result.data.checkInSchedule).toBeNull();
expect(result.data.isTemplate).toBe(false);
expect(result.data.templateSource).toBeNull();
}
});
it('accepts full input with all fields', () => {
const result = CreateAgentSchema.safeParse({
...validMinimal,
coachingFramework: 'socratic',
accentColor: '#5A8CFF',
welcomeMessage: 'Ready to practice?',
sessionLength: 30,
difficultyLevel: 'intermediate',
language: 'es',
privacyLevel: 'local_only',
checkInSchedule: '0 9 * * 1-5',
isTemplate: true,
templateSource: 'agent_abc123',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.coachingFramework).toBe('socratic');
expect(result.data.sessionLength).toBe(30);
expect(result.data.language).toBe('es');
expect(result.data.checkInSchedule).toBe('0 9 * * 1-5');
}
});
it('rejects missing name', () => {
const result = CreateAgentSchema.safeParse({
role: 'Coach',
systemPrompt: 'You are a coach.',
voiceId: 'alloy',
});
expect(result.success).toBe(false);
});
it('rejects missing role', () => {
const result = CreateAgentSchema.safeParse({
name: 'Coach',
systemPrompt: 'You are a coach.',
voiceId: 'alloy',
});
expect(result.success).toBe(false);
});
it('rejects missing systemPrompt', () => {
const result = CreateAgentSchema.safeParse({
name: 'Coach',
role: 'Coach',
voiceId: 'alloy',
});
expect(result.success).toBe(false);
});
it('rejects missing voiceId', () => {
const result = CreateAgentSchema.safeParse({
name: 'Coach',
role: 'Coach',
systemPrompt: 'You are a coach.',
});
expect(result.success).toBe(false);
});
it('rejects invalid accent color format', () => {
const result = CreateAgentSchema.safeParse({
...validMinimal,
accentColor: 'red',
});
expect(result.success).toBe(false);
});
it('rejects name exceeding max length', () => {
const result = CreateAgentSchema.safeParse({
...validMinimal,
name: 'A'.repeat(101),
});
expect(result.success).toBe(false);
});
it('rejects sessionLength out of range', () => {
expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 0 }).success).toBe(false);
expect(CreateAgentSchema.safeParse({ ...validMinimal, sessionLength: 121 }).success).toBe(
false,
);
});
it('rejects invalid difficulty level', () => {
const result = CreateAgentSchema.safeParse({
...validMinimal,
difficultyLevel: 'expert',
});
expect(result.success).toBe(false);
});
it('rejects invalid coaching framework', () => {
const result = CreateAgentSchema.safeParse({
...validMinimal,
coachingFramework: 'unknown',
});
expect(result.success).toBe(false);
});
});
// ── UpdateAgentSchema ──
describe('UpdateAgentSchema', () => {
it('accepts partial update with single field', () => {
const result = UpdateAgentSchema.safeParse({ name: 'Updated Coach' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.name).toBe('Updated Coach');
expect(result.data.role).toBeUndefined();
}
});
it('accepts empty update (no fields)', () => {
const result = UpdateAgentSchema.safeParse({});
expect(result.success).toBe(true);
});
it('rejects invalid accent color in update', () => {
const result = UpdateAgentSchema.safeParse({ accentColor: 'not-a-color' });
expect(result.success).toBe(false);
});
it('accepts nullable checkInSchedule', () => {
const result = UpdateAgentSchema.safeParse({ checkInSchedule: null });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.checkInSchedule).toBeNull();
}
});
});
// ── ListAgentsQuerySchema ──
describe('ListAgentsQuerySchema', () => {
it('applies defaults for empty query', () => {
const result = ListAgentsQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(50);
expect(result.data.offset).toBe(0);
}
});
it('accepts custom limit and offset', () => {
const result = ListAgentsQuerySchema.safeParse({ limit: '10', offset: '5' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(10);
expect(result.data.offset).toBe(5);
}
});
it('rejects limit exceeding max', () => {
const result = ListAgentsQuerySchema.safeParse({ limit: 101 });
expect(result.success).toBe(false);
});
it('rejects negative offset', () => {
const result = ListAgentsQuerySchema.safeParse({ offset: -1 });
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,90 @@
/**
* JarvisJr agents repository Cosmos DB CRUD.
* Container: jarvis_agents, partition key: /userId
*/
import { getContainer } from '../../lib/cosmos.js';
import type { JarvisAgentDoc, ListAgentsQuery } from './types.js';
function container() {
return getContainer('jarvis_agents');
}
export async function listByUser(
userId: string,
query: ListAgentsQuery,
): Promise<{ agents: JarvisAgentDoc[]; total: number }> {
const countResult = await container()
.items.query<number>({
query: 'SELECT VALUE COUNT(1) FROM c WHERE c.userId = @userId',
parameters: [{ name: '@userId', value: userId }],
})
.fetchAll();
const total = countResult.resources[0] ?? 0;
const { resources } = await container()
.items.query<JarvisAgentDoc>({
query:
'SELECT * FROM c WHERE c.userId = @userId ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit',
parameters: [
{ name: '@userId', value: userId },
{ name: '@offset', value: query.offset },
{ name: '@limit', value: query.limit },
],
})
.fetchAll();
return { agents: resources, total };
}
export async function getById(id: string, userId: string): Promise<JarvisAgentDoc | null> {
try {
const { resource } = await container().item(id, userId).read<JarvisAgentDoc>();
return resource ?? null;
} catch {
return null;
}
}
export async function create(doc: JarvisAgentDoc): Promise<JarvisAgentDoc> {
const { resource } = await container().items.create(doc);
return resource as JarvisAgentDoc;
}
export async function update(
id: string,
userId: string,
updates: Partial<JarvisAgentDoc>,
): Promise<JarvisAgentDoc | null> {
try {
const { resource: existing } = await container().item(id, userId).read<JarvisAgentDoc>();
if (!existing) return null;
const merged = { ...existing, ...updates, updatedAt: new Date().toISOString() };
const { resource } = await container().item(id, userId).replace(merged);
return resource as JarvisAgentDoc;
} catch {
return null;
}
}
export async function remove(id: string, userId: string): Promise<boolean> {
try {
await container().item(id, userId).delete();
return true;
} catch {
return false;
}
}
export async function incrementSessionCount(
id: string,
userId: string,
): Promise<void> {
const agent = await getById(id, userId);
if (agent) {
await update(id, userId, {
totalSessions: agent.totalSessions + 1,
lastSessionAt: new Date().toISOString(),
});
}
}

View File

@ -0,0 +1,131 @@
/**
* JarvisJr agent REST endpoints.
*
* GET /jarvis/agents list user's agents
* POST /jarvis/agents create agent
* GET /jarvis/agents/:id get single agent
* PUT /jarvis/agents/:id update agent
* DELETE /jarvis/agents/:id delete agent
* POST /jarvis/agents/:id/duplicate duplicate agent
*/
import type { FastifyInstance } from 'fastify';
import crypto from 'node:crypto';
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
import { extractAuth } from '../../lib/auth.js';
import * as repo from './repository.js';
import {
CreateAgentSchema,
UpdateAgentSchema,
ListAgentsQuerySchema,
type JarvisAgentDoc,
} from './types.js';
const PRODUCT_ID = 'jarvisjr';
export async function jarvisAgentRoutes(app: FastifyInstance) {
// List agents
app.get('/jarvis/agents', async req => {
const auth = await extractAuth(req);
const parsed = ListAgentsQuerySchema.safeParse(req.query);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const { agents, total } = await repo.listByUser(auth.sub, parsed.data);
return { agents, total, limit: parsed.data.limit, offset: parsed.data.offset };
});
// Get agent
app.get('/jarvis/agents/:id', async req => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const agent = await repo.getById(id, auth.sub);
if (!agent) throw new NotFoundError('Agent not found');
return agent;
});
// Create agent
app.post('/jarvis/agents', async (req, reply) => {
const auth = await extractAuth(req);
const parsed = CreateAgentSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const input = parsed.data;
const now = new Date().toISOString();
const doc: JarvisAgentDoc = {
id: `agent_${crypto.randomUUID()}`,
userId: auth.sub,
productId: PRODUCT_ID,
name: input.name,
role: input.role,
systemPrompt: input.systemPrompt,
voiceId: input.voiceId,
coachingFramework: input.coachingFramework,
accentColor: input.accentColor,
welcomeMessage: input.welcomeMessage,
sessionLength: input.sessionLength,
difficultyLevel: input.difficultyLevel,
language: input.language,
privacyLevel: input.privacyLevel,
checkInSchedule: input.checkInSchedule,
isTemplate: input.isTemplate,
templateSource: input.templateSource,
totalSessions: 0,
lastSessionAt: null,
createdAt: now,
updatedAt: now,
};
const created = await repo.create(doc);
reply.code(201);
return created;
});
// Update agent
app.put('/jarvis/agents/:id', async req => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const parsed = UpdateAgentSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const updated = await repo.update(id, auth.sub, parsed.data);
if (!updated) throw new NotFoundError('Agent not found');
return updated;
});
// Delete agent
app.delete('/jarvis/agents/:id', async (req, reply) => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const deleted = await repo.remove(id, auth.sub);
if (!deleted) throw new NotFoundError('Agent not found');
reply.code(204);
});
// Duplicate agent
app.post('/jarvis/agents/:id/duplicate', async (req, reply) => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const source = await repo.getById(id, auth.sub);
if (!source) throw new NotFoundError('Agent not found');
const now = new Date().toISOString();
const doc: JarvisAgentDoc = {
...source,
id: `agent_${crypto.randomUUID()}`,
name: `${source.name} (Copy)`,
templateSource: source.id,
totalSessions: 0,
lastSessionAt: null,
createdAt: now,
updatedAt: now,
};
const created = await repo.create(doc);
reply.code(201);
return created;
});
}

View File

@ -0,0 +1,88 @@
/**
* JarvisJr agent configuration types.
* Each agent is a persistent coaching persona with voice, personality, and memory.
* Partition key: /userId
*/
import { z } from 'zod';
export const DIFFICULTY_LEVELS = ['beginner', 'intermediate', 'advanced', 'adaptive'] as const;
export const PRIVACY_LEVELS = ['standard', 'local_only'] as const;
export const COACHING_FRAMEWORKS = [
'socratic',
'star',
'scamper',
'immersive',
'cognitive_reframing',
'structured_feedback',
'freeform',
] as const;
export type DifficultyLevel = (typeof DIFFICULTY_LEVELS)[number];
export type PrivacyLevel = (typeof PRIVACY_LEVELS)[number];
export type CoachingFramework = (typeof COACHING_FRAMEWORKS)[number];
export interface JarvisAgentDoc {
id: string;
userId: string;
productId: string;
name: string;
role: string;
systemPrompt: string;
voiceId: string;
coachingFramework: CoachingFramework;
accentColor: string;
welcomeMessage: string;
sessionLength: number;
difficultyLevel: DifficultyLevel;
language: string;
privacyLevel: PrivacyLevel;
checkInSchedule: string | null;
isTemplate: boolean;
templateSource: string | null;
totalSessions: number;
lastSessionAt: string | null;
createdAt: string;
updatedAt: string;
}
export const CreateAgentSchema = z.object({
name: z.string().min(1).max(100),
role: z.string().min(1).max(200),
systemPrompt: z.string().min(1).max(10000),
voiceId: z.string().min(1).max(100),
coachingFramework: z.enum(COACHING_FRAMEWORKS).default('freeform'),
accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).default('#7C6BFF'),
welcomeMessage: z.string().max(500).default(''),
sessionLength: z.number().min(1).max(120).default(15),
difficultyLevel: z.enum(DIFFICULTY_LEVELS).default('adaptive'),
language: z.string().min(2).max(10).default('en'),
privacyLevel: z.enum(PRIVACY_LEVELS).default('standard'),
checkInSchedule: z.string().nullable().default(null),
isTemplate: z.boolean().default(false),
templateSource: z.string().nullable().default(null),
});
export const UpdateAgentSchema = z.object({
name: z.string().min(1).max(100).optional(),
role: z.string().min(1).max(200).optional(),
systemPrompt: z.string().min(1).max(10000).optional(),
voiceId: z.string().min(1).max(100).optional(),
coachingFramework: z.enum(COACHING_FRAMEWORKS).optional(),
accentColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(),
welcomeMessage: z.string().max(500).optional(),
sessionLength: z.number().min(1).max(120).optional(),
difficultyLevel: z.enum(DIFFICULTY_LEVELS).optional(),
language: z.string().min(2).max(10).optional(),
privacyLevel: z.enum(PRIVACY_LEVELS).optional(),
checkInSchedule: z.string().nullable().optional(),
});
export const ListAgentsQuerySchema = z.object({
limit: z.coerce.number().min(1).max(100).default(50),
offset: z.coerce.number().min(0).default(0),
});
export type CreateAgentInput = z.infer<typeof CreateAgentSchema>;
export type UpdateAgentInput = z.infer<typeof UpdateAgentSchema>;
export type ListAgentsQuery = z.infer<typeof ListAgentsQuerySchema>;

View File

@ -0,0 +1,207 @@
/**
* JarvisJr agent memory module unit tests validates schema parsing and constants.
*/
import { describe, it, expect } from 'vitest';
import {
CreateMemorySchema,
UpdateMemorySchema,
ListMemoryQuerySchema,
MEMORY_TYPES,
} from './types.js';
// ── Constants ──
describe('JarvisJr memory constants', () => {
it('has expected memory types', () => {
expect(MEMORY_TYPES).toEqual(['skill_note', 'preference', 'goal', 'context', 'exercise']);
});
});
// ── CreateMemorySchema ──
describe('CreateMemorySchema', () => {
const validMinimal = {
agentId: 'agent_abc123',
sessionId: 'sess_xyz789',
type: 'skill_note',
content: 'User tends to use filler words like "um" and "uh" frequently.',
};
it('accepts minimal valid input with defaults', () => {
const result = CreateMemorySchema.safeParse(validMinimal);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.agentId).toBe('agent_abc123');
expect(result.data.sessionId).toBe('sess_xyz789');
expect(result.data.type).toBe('skill_note');
expect(result.data.importance).toBe(0.5);
expect(result.data.tags).toEqual([]);
expect(result.data.expiresAt).toBeNull();
}
});
it('accepts full input with all fields', () => {
const result = CreateMemorySchema.safeParse({
...validMinimal,
importance: 0.9,
tags: ['communication', 'filler_words'],
expiresAt: '2027-01-01T00:00:00Z',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.importance).toBe(0.9);
expect(result.data.tags).toHaveLength(2);
expect(result.data.expiresAt).toBe('2027-01-01T00:00:00Z');
}
});
it('accepts all memory types', () => {
for (const type of MEMORY_TYPES) {
const result = CreateMemorySchema.safeParse({ ...validMinimal, type });
expect(result.success).toBe(true);
}
});
it('rejects missing agentId', () => {
const result = CreateMemorySchema.safeParse({
sessionId: validMinimal.sessionId,
type: validMinimal.type,
content: validMinimal.content,
});
expect(result.success).toBe(false);
});
it('rejects missing sessionId', () => {
const result = CreateMemorySchema.safeParse({
agentId: validMinimal.agentId,
type: validMinimal.type,
content: validMinimal.content,
});
expect(result.success).toBe(false);
});
it('rejects missing content', () => {
const result = CreateMemorySchema.safeParse({
agentId: validMinimal.agentId,
sessionId: validMinimal.sessionId,
type: validMinimal.type,
});
expect(result.success).toBe(false);
});
it('rejects empty content', () => {
const result = CreateMemorySchema.safeParse({ ...validMinimal, content: '' });
expect(result.success).toBe(false);
});
it('rejects invalid memory type', () => {
const result = CreateMemorySchema.safeParse({ ...validMinimal, type: 'thought' });
expect(result.success).toBe(false);
});
it('rejects importance out of range', () => {
expect(
CreateMemorySchema.safeParse({ ...validMinimal, importance: -0.1 }).success,
).toBe(false);
expect(
CreateMemorySchema.safeParse({ ...validMinimal, importance: 1.1 }).success,
).toBe(false);
});
it('rejects content exceeding max length', () => {
const result = CreateMemorySchema.safeParse({
...validMinimal,
content: 'A'.repeat(5001),
});
expect(result.success).toBe(false);
});
});
// ── UpdateMemorySchema ──
describe('UpdateMemorySchema', () => {
it('accepts partial update with single field', () => {
const result = UpdateMemorySchema.safeParse({ importance: 0.8 });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.importance).toBe(0.8);
expect(result.data.content).toBeUndefined();
}
});
it('accepts empty update', () => {
const result = UpdateMemorySchema.safeParse({});
expect(result.success).toBe(true);
});
it('accepts tags update', () => {
const result = UpdateMemorySchema.safeParse({ tags: ['updated', 'tag'] });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.tags).toEqual(['updated', 'tag']);
}
});
it('accepts nullable expiresAt', () => {
const result = UpdateMemorySchema.safeParse({ expiresAt: null });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.expiresAt).toBeNull();
}
});
it('rejects empty content string', () => {
const result = UpdateMemorySchema.safeParse({ content: '' });
expect(result.success).toBe(false);
});
});
// ── ListMemoryQuerySchema ──
describe('ListMemoryQuerySchema', () => {
it('applies defaults for empty query', () => {
const result = ListMemoryQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(50);
expect(result.data.offset).toBe(0);
expect(result.data.type).toBeUndefined();
expect(result.data.minImportance).toBeUndefined();
}
});
it('accepts type filter', () => {
const result = ListMemoryQuerySchema.safeParse({ type: 'goal' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('goal');
}
});
it('accepts minImportance filter', () => {
const result = ListMemoryQuerySchema.safeParse({ minImportance: '0.7' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.minImportance).toBe(0.7);
}
});
it('accepts higher limit for memory', () => {
const result = ListMemoryQuerySchema.safeParse({ limit: 200 });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(200);
}
});
it('rejects limit exceeding max', () => {
const result = ListMemoryQuerySchema.safeParse({ limit: 201 });
expect(result.success).toBe(false);
});
it('rejects invalid type', () => {
const result = ListMemoryQuerySchema.safeParse({ type: 'thought' });
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,131 @@
/**
* JarvisJr agent memory repository Cosmos DB CRUD.
* Container: jarvis_memory, partition key: /agentId
*/
import { getContainer } from '../../lib/cosmos.js';
import type { JarvisMemoryDoc, ListMemoryQuery } from './types.js';
function container() {
return getContainer('jarvis_memory');
}
export async function listByAgent(
agentId: string,
query: ListMemoryQuery,
): Promise<{ memories: JarvisMemoryDoc[]; total: number }> {
const conditions = ['c.agentId = @agentId'];
const params: { name: string; value: string | number }[] = [
{ name: '@agentId', value: agentId },
];
if (query.type) {
conditions.push('c.type = @type');
params.push({ name: '@type', value: query.type });
}
if (query.minImportance !== undefined) {
conditions.push('c.importance >= @minImportance');
params.push({ name: '@minImportance', value: query.minImportance });
}
const where = `WHERE ${conditions.join(' AND ')}`;
const countResult = await container()
.items.query<number>({
query: `SELECT VALUE COUNT(1) FROM c ${where}`,
parameters: params,
})
.fetchAll();
const total = countResult.resources[0] ?? 0;
const { resources } = await container()
.items.query<JarvisMemoryDoc>({
query: `SELECT * FROM c ${where} ORDER BY c.importance DESC, c.createdAt DESC OFFSET @offset LIMIT @limit`,
parameters: [
...params,
{ name: '@offset', value: query.offset },
{ name: '@limit', value: query.limit },
],
})
.fetchAll();
return { memories: resources, total };
}
export async function getById(id: string, agentId: string): Promise<JarvisMemoryDoc | null> {
try {
const { resource } = await container().item(id, agentId).read<JarvisMemoryDoc>();
return resource ?? null;
} catch {
return null;
}
}
export async function create(doc: JarvisMemoryDoc): Promise<JarvisMemoryDoc> {
const { resource } = await container().items.create(doc);
return resource as JarvisMemoryDoc;
}
export async function update(
id: string,
agentId: string,
updates: Partial<JarvisMemoryDoc>,
): Promise<JarvisMemoryDoc | null> {
try {
const { resource: existing } = await container().item(id, agentId).read<JarvisMemoryDoc>();
if (!existing) return null;
const merged = { ...existing, ...updates };
const { resource } = await container().item(id, agentId).replace(merged);
return resource as JarvisMemoryDoc;
} catch {
return null;
}
}
export async function remove(id: string, agentId: string): Promise<boolean> {
try {
await container().item(id, agentId).delete();
return true;
} catch {
return false;
}
}
export async function pruneExpired(agentId: string): Promise<number> {
const now = new Date().toISOString();
const { resources } = await container()
.items.query<JarvisMemoryDoc>({
query:
'SELECT * FROM c WHERE c.agentId = @agentId AND c.expiresAt != null AND c.expiresAt < @now',
parameters: [
{ name: '@agentId', value: agentId },
{ name: '@now', value: now },
],
})
.fetchAll();
let count = 0;
for (const mem of resources) {
const deleted = await remove(mem.id, agentId);
if (deleted) count++;
}
return count;
}
export async function getContextForSession(
agentId: string,
limit: number = 20,
): Promise<JarvisMemoryDoc[]> {
const { resources } = await container()
.items.query<JarvisMemoryDoc>({
query:
'SELECT * FROM c WHERE c.agentId = @agentId AND (c.expiresAt = null OR c.expiresAt > @now) ORDER BY c.importance DESC OFFSET 0 LIMIT @limit',
parameters: [
{ name: '@agentId', value: agentId },
{ name: '@now', value: new Date().toISOString() },
{ name: '@limit', value: limit },
],
})
.fetchAll();
return resources;
}

View File

@ -0,0 +1,119 @@
/**
* JarvisJr agent memory REST endpoints.
*
* GET /jarvis/agents/:agentId/memory list memories for agent
* POST /jarvis/agents/:agentId/memory create memory
* GET /jarvis/agents/:agentId/memory/context get context for session
* GET /jarvis/agents/:agentId/memory/:id get single memory
* PUT /jarvis/agents/:agentId/memory/:id update memory
* DELETE /jarvis/agents/:agentId/memory/:id delete memory
* POST /jarvis/agents/:agentId/memory/prune prune expired memories
*/
import type { FastifyInstance } from 'fastify';
import crypto from 'node:crypto';
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
import { extractAuth } from '../../lib/auth.js';
import * as repo from './repository.js';
import {
CreateMemorySchema,
UpdateMemorySchema,
ListMemoryQuerySchema,
type JarvisMemoryDoc,
} from './types.js';
const PRODUCT_ID = 'jarvisjr';
export async function jarvisMemoryRoutes(app: FastifyInstance) {
// Context retrieval — top memories for a session prompt
app.get('/jarvis/agents/:agentId/memory/context', async req => {
await extractAuth(req);
const { agentId } = req.params as { agentId: string };
const limit = (req.query as { limit?: string }).limit
? parseInt((req.query as { limit?: string }).limit!, 10)
: 20;
const memories = await repo.getContextForSession(agentId, limit);
return { memories, count: memories.length };
});
// List memories
app.get('/jarvis/agents/:agentId/memory', async req => {
await extractAuth(req);
const { agentId } = req.params as { agentId: string };
const parsed = ListMemoryQuerySchema.safeParse(req.query);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const { memories, total } = await repo.listByAgent(agentId, parsed.data);
return { memories, total, limit: parsed.data.limit, offset: parsed.data.offset };
});
// Get memory
app.get('/jarvis/agents/:agentId/memory/:id', async req => {
await extractAuth(req);
const { agentId, id } = req.params as { agentId: string; id: string };
const memory = await repo.getById(id, agentId);
if (!memory) throw new NotFoundError('Memory not found');
return memory;
});
// Create memory
app.post('/jarvis/agents/:agentId/memory', async (req, reply) => {
const auth = await extractAuth(req);
const { agentId } = req.params as { agentId: string };
const parsed = CreateMemorySchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const input = parsed.data;
const now = new Date().toISOString();
const doc: JarvisMemoryDoc = {
id: `mem_${crypto.randomUUID()}`,
agentId,
userId: auth.sub,
productId: PRODUCT_ID,
sessionId: input.sessionId,
type: input.type,
content: input.content,
importance: input.importance,
tags: input.tags,
createdAt: now,
expiresAt: input.expiresAt,
};
const created = await repo.create(doc);
reply.code(201);
return created;
});
// Update memory
app.put('/jarvis/agents/:agentId/memory/:id', async req => {
await extractAuth(req);
const { agentId, id } = req.params as { agentId: string; id: string };
const parsed = UpdateMemorySchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const updated = await repo.update(id, agentId, parsed.data);
if (!updated) throw new NotFoundError('Memory not found');
return updated;
});
// Delete memory
app.delete('/jarvis/agents/:agentId/memory/:id', async (req, reply) => {
await extractAuth(req);
const { agentId, id } = req.params as { agentId: string; id: string };
const deleted = await repo.remove(id, agentId);
if (!deleted) throw new NotFoundError('Memory not found');
reply.code(204);
});
// Prune expired memories
app.post('/jarvis/agents/:agentId/memory/prune', async req => {
await extractAuth(req);
const { agentId } = req.params as { agentId: string };
const count = await repo.pruneExpired(agentId);
return { pruned: count };
});
}

View File

@ -0,0 +1,53 @@
/**
* JarvisJr agent memory types.
* Per-agent persistent memory for context retrieval during sessions.
* Partition key: /agentId
*/
import { z } from 'zod';
export const MEMORY_TYPES = ['skill_note', 'preference', 'goal', 'context', 'exercise'] as const;
export type MemoryType = (typeof MEMORY_TYPES)[number];
export interface JarvisMemoryDoc {
id: string;
agentId: string;
userId: string;
productId: string;
sessionId: string;
type: MemoryType;
content: string;
importance: number;
tags: string[];
createdAt: string;
expiresAt: string | null;
}
export const CreateMemorySchema = z.object({
agentId: z.string().min(1),
sessionId: z.string().min(1),
type: z.enum(MEMORY_TYPES),
content: z.string().min(1).max(5000),
importance: z.number().min(0).max(1).default(0.5),
tags: z.array(z.string()).default([]),
expiresAt: z.string().nullable().default(null),
});
export const UpdateMemorySchema = z.object({
content: z.string().min(1).max(5000).optional(),
importance: z.number().min(0).max(1).optional(),
tags: z.array(z.string()).optional(),
expiresAt: z.string().nullable().optional(),
});
export const ListMemoryQuerySchema = z.object({
type: z.enum(MEMORY_TYPES).optional(),
minImportance: z.coerce.number().min(0).max(1).optional(),
limit: z.coerce.number().min(1).max(200).default(50),
offset: z.coerce.number().min(0).default(0),
});
export type CreateMemoryInput = z.infer<typeof CreateMemorySchema>;
export type UpdateMemoryInput = z.infer<typeof UpdateMemorySchema>;
export type ListMemoryQuery = z.infer<typeof ListMemoryQuerySchema>;

View File

@ -0,0 +1,193 @@
/**
* JarvisJr sessions module unit tests validates schema parsing and constants.
*/
import { describe, it, expect } from 'vitest';
import {
CreateSessionSchema,
CompleteSessionSchema,
ListSessionsQuerySchema,
SESSION_MODES,
SESSION_STATUSES,
} from './types.js';
// ── Constants ──
describe('JarvisJr session constants', () => {
it('has expected session modes', () => {
expect(SESSION_MODES).toEqual(['text', 'voice']);
});
it('has expected session statuses', () => {
expect(SESSION_STATUSES).toEqual(['active', 'completed', 'abandoned']);
});
});
// ── CreateSessionSchema ──
describe('CreateSessionSchema', () => {
const validMinimal = {
agentId: 'agent_abc123',
mode: 'text',
};
it('accepts minimal valid input with defaults', () => {
const result = CreateSessionSchema.safeParse(validMinimal);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.agentId).toBe('agent_abc123');
expect(result.data.mode).toBe('text');
expect(result.data.transcript).toEqual([]);
expect(result.data.summary).toBe('');
expect(result.data.coachingNotes).toEqual([]);
expect(result.data.exercises).toEqual([]);
expect(result.data.skillMetrics).toEqual([]);
expect(result.data.duration).toBe(0);
}
});
it('accepts voice mode', () => {
const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'voice' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.mode).toBe('voice');
}
});
it('accepts full input with transcript and metrics', () => {
const result = CreateSessionSchema.safeParse({
...validMinimal,
transcript: [
{ role: 'agent', content: 'Hello!', ts: '2026-03-01T00:00:00Z' },
{ role: 'user', content: 'Hi there.', ts: '2026-03-01T00:00:01Z' },
],
summary: 'Good session on articulation.',
coachingNotes: ['Practice filler word awareness'],
exercises: ['Record 2-minute speech'],
skillMetrics: [{ name: 'articulation', score: 72, delta: 5 }],
duration: 900,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.transcript).toHaveLength(2);
expect(result.data.skillMetrics[0].score).toBe(72);
}
});
it('rejects missing agentId', () => {
const result = CreateSessionSchema.safeParse({ mode: 'text' });
expect(result.success).toBe(false);
});
it('rejects missing mode', () => {
const result = CreateSessionSchema.safeParse({ agentId: 'agent_abc123' });
expect(result.success).toBe(false);
});
it('rejects invalid mode', () => {
const result = CreateSessionSchema.safeParse({ ...validMinimal, mode: 'video' });
expect(result.success).toBe(false);
});
it('rejects invalid transcript role', () => {
const result = CreateSessionSchema.safeParse({
...validMinimal,
transcript: [{ role: 'system', content: 'test', ts: '2026-03-01T00:00:00Z' }],
});
expect(result.success).toBe(false);
});
it('rejects negative duration', () => {
const result = CreateSessionSchema.safeParse({ ...validMinimal, duration: -1 });
expect(result.success).toBe(false);
});
it('rejects skill metric score out of range', () => {
const result = CreateSessionSchema.safeParse({
...validMinimal,
skillMetrics: [{ name: 'test', score: 101 }],
});
expect(result.success).toBe(false);
});
});
// ── CompleteSessionSchema ──
describe('CompleteSessionSchema', () => {
const validComplete = {
transcript: [
{ role: 'agent' as const, content: 'Hello!', ts: '2026-03-01T00:00:00Z' },
{ role: 'user' as const, content: 'Hi.', ts: '2026-03-01T00:00:01Z' },
],
summary: 'Practiced public speaking introduction.',
duration: 600,
};
it('accepts valid complete input', () => {
const result = CompleteSessionSchema.safeParse(validComplete);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.transcript).toHaveLength(2);
expect(result.data.summary).toBe('Practiced public speaking introduction.');
expect(result.data.duration).toBe(600);
expect(result.data.coachingNotes).toEqual([]);
}
});
it('rejects missing summary', () => {
const result = CompleteSessionSchema.safeParse({
transcript: validComplete.transcript,
duration: 600,
});
expect(result.success).toBe(false);
});
it('rejects empty summary', () => {
const result = CompleteSessionSchema.safeParse({
...validComplete,
summary: '',
});
expect(result.success).toBe(false);
});
it('rejects missing transcript', () => {
const result = CompleteSessionSchema.safeParse({
summary: 'Test',
duration: 600,
});
expect(result.success).toBe(false);
});
});
// ── ListSessionsQuerySchema ──
describe('ListSessionsQuerySchema', () => {
it('applies defaults for empty query', () => {
const result = ListSessionsQuerySchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(50);
expect(result.data.offset).toBe(0);
expect(result.data.agentId).toBeUndefined();
expect(result.data.status).toBeUndefined();
}
});
it('accepts agentId filter', () => {
const result = ListSessionsQuerySchema.safeParse({ agentId: 'agent_xyz' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.agentId).toBe('agent_xyz');
}
});
it('accepts status filter', () => {
const result = ListSessionsQuerySchema.safeParse({ status: 'completed' });
expect(result.success).toBe(true);
});
it('rejects invalid status', () => {
const result = ListSessionsQuerySchema.safeParse({ status: 'unknown' });
expect(result.success).toBe(false);
});
});

View File

@ -0,0 +1,148 @@
/**
* JarvisJr sessions repository Cosmos DB CRUD.
* Container: jarvis_sessions, partition key: /userId
*/
import { getContainer } from '../../lib/cosmos.js';
import type { JarvisSessionDoc, ListSessionsQuery } from './types.js';
function container() {
return getContainer('jarvis_sessions');
}
export async function listByUser(
userId: string,
query: ListSessionsQuery,
): Promise<{ sessions: JarvisSessionDoc[]; total: number }> {
const conditions = ['c.userId = @userId'];
const params: { name: string; value: string | number }[] = [
{ name: '@userId', value: userId },
];
if (query.agentId) {
conditions.push('c.agentId = @agentId');
params.push({ name: '@agentId', value: query.agentId });
}
if (query.status) {
conditions.push('c.status = @status');
params.push({ name: '@status', value: query.status });
}
const where = `WHERE ${conditions.join(' AND ')}`;
const countResult = await container()
.items.query<number>({
query: `SELECT VALUE COUNT(1) FROM c ${where}`,
parameters: params,
})
.fetchAll();
const total = countResult.resources[0] ?? 0;
const { resources } = await container()
.items.query<JarvisSessionDoc>({
query: `SELECT * FROM c ${where} ORDER BY c.createdAt DESC OFFSET @offset LIMIT @limit`,
parameters: [
...params,
{ name: '@offset', value: query.offset },
{ name: '@limit', value: query.limit },
],
})
.fetchAll();
return { sessions: resources, total };
}
export async function getById(id: string, userId: string): Promise<JarvisSessionDoc | null> {
try {
const { resource } = await container().item(id, userId).read<JarvisSessionDoc>();
return resource ?? null;
} catch {
return null;
}
}
export async function create(doc: JarvisSessionDoc): Promise<JarvisSessionDoc> {
const { resource } = await container().items.create(doc);
return resource as JarvisSessionDoc;
}
export async function update(
id: string,
userId: string,
updates: Partial<JarvisSessionDoc>,
): Promise<JarvisSessionDoc | null> {
try {
const { resource: existing } = await container().item(id, userId).read<JarvisSessionDoc>();
if (!existing) return null;
const merged = { ...existing, ...updates };
const { resource } = await container().item(id, userId).replace(merged);
return resource as JarvisSessionDoc;
} catch {
return null;
}
}
export async function getStats(userId: string): Promise<{
totalSessions: number;
currentStreak: number;
longestStreak: number;
perAgent: Record<string, number>;
}> {
const { resources } = await container()
.items.query<JarvisSessionDoc>({
query:
"SELECT c.agentId, c.createdAt FROM c WHERE c.userId = @userId AND c.status = 'completed' ORDER BY c.createdAt DESC",
parameters: [{ name: '@userId', value: userId }],
})
.fetchAll();
const perAgent: Record<string, number> = {};
for (const s of resources) {
perAgent[s.agentId] = (perAgent[s.agentId] || 0) + 1;
}
// Calculate streaks based on calendar days
let currentStreak = 0;
let longestStreak = 0;
let streak = 0;
let lastDate = '';
for (const s of resources) {
const date = s.createdAt.slice(0, 10); // YYYY-MM-DD
if (date === lastDate) continue; // same day
if (!lastDate) {
streak = 1;
} else {
const prev = new Date(lastDate);
const curr = new Date(date);
const diffMs = prev.getTime() - curr.getTime(); // DESC order
const diffDays = Math.round(diffMs / 86400000);
if (diffDays === 1) {
streak += 1;
} else {
if (streak > longestStreak) longestStreak = streak;
streak = 1;
}
}
lastDate = date;
}
if (streak > longestStreak) longestStreak = streak;
// Current streak: only if the most recent session is today or yesterday
if (resources.length > 0) {
const mostRecent = resources[0].createdAt.slice(0, 10);
const today = new Date().toISOString().slice(0, 10);
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
if (mostRecent === today || mostRecent === yesterday) {
currentStreak = streak;
}
}
return {
totalSessions: resources.length,
currentStreak,
longestStreak,
perAgent,
};
}

View File

@ -0,0 +1,120 @@
/**
* JarvisJr session REST endpoints.
*
* GET /jarvis/sessions list user's sessions
* POST /jarvis/sessions create/start session
* GET /jarvis/sessions/stats streak + per-agent stats
* GET /jarvis/sessions/:id get single session
* PUT /jarvis/sessions/:id/complete complete a session with summary
*/
import type { FastifyInstance } from 'fastify';
import crypto from 'node:crypto';
import { BadRequestError, NotFoundError } from '../../lib/errors.js';
import { extractAuth } from '../../lib/auth.js';
import * as repo from './repository.js';
import * as agentRepo from '../jarvis-agents/repository.js';
import {
CreateSessionSchema,
CompleteSessionSchema,
ListSessionsQuerySchema,
type JarvisSessionDoc,
} from './types.js';
const PRODUCT_ID = 'jarvisjr';
export async function jarvisSessionRoutes(app: FastifyInstance) {
// Stats — must be before :id route
app.get('/jarvis/sessions/stats', async req => {
const auth = await extractAuth(req);
return repo.getStats(auth.sub);
});
// List sessions
app.get('/jarvis/sessions', async req => {
const auth = await extractAuth(req);
const parsed = ListSessionsQuerySchema.safeParse(req.query);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const { sessions, total } = await repo.listByUser(auth.sub, parsed.data);
return { sessions, total, limit: parsed.data.limit, offset: parsed.data.offset };
});
// Get session
app.get('/jarvis/sessions/:id', async req => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const session = await repo.getById(id, auth.sub);
if (!session) throw new NotFoundError('Session not found');
return session;
});
// Create / start session
app.post('/jarvis/sessions', async (req, reply) => {
const auth = await extractAuth(req);
const parsed = CreateSessionSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const input = parsed.data;
const now = new Date().toISOString();
const doc: JarvisSessionDoc = {
id: `sess_${crypto.randomUUID()}`,
userId: auth.sub,
productId: PRODUCT_ID,
agentId: input.agentId,
mode: input.mode,
status: 'active',
transcript: input.transcript,
summary: input.summary,
coachingNotes: input.coachingNotes,
exercises: input.exercises,
skillMetrics: input.skillMetrics,
duration: input.duration,
messageCount: input.transcript.length,
createdAt: now,
completedAt: null,
};
const created = await repo.create(doc);
reply.code(201);
return created;
});
// Complete session
app.put('/jarvis/sessions/:id/complete', async req => {
const auth = await extractAuth(req);
const { id } = req.params as { id: string };
const parsed = CompleteSessionSchema.safeParse(req.body);
if (!parsed.success) {
throw new BadRequestError(parsed.error.issues.map(i => i.message).join('; '));
}
const existing = await repo.getById(id, auth.sub);
if (!existing) throw new NotFoundError('Session not found');
const input = parsed.data;
const now = new Date().toISOString();
const updated = await repo.update(id, auth.sub, {
status: 'completed',
transcript: input.transcript,
summary: input.summary,
coachingNotes: input.coachingNotes,
exercises: input.exercises,
skillMetrics: input.skillMetrics,
duration: input.duration,
messageCount: input.transcript.length,
completedAt: now,
});
if (!updated) throw new NotFoundError('Session not found');
// Increment agent session count
await agentRepo.incrementSessionCount(existing.agentId, auth.sub);
return updated;
});
}

View File

@ -0,0 +1,74 @@
/**
* JarvisJr session types.
* Each session is a coaching conversation between a user and an agent.
* Partition key: /userId
*/
import { z } from 'zod';
export const SESSION_MODES = ['text', 'voice'] as const;
export const SESSION_STATUSES = ['active', 'completed', 'abandoned'] as const;
export type SessionMode = (typeof SESSION_MODES)[number];
export type SessionStatus = (typeof SESSION_STATUSES)[number];
export const TranscriptEntrySchema = z.object({
role: z.enum(['user', 'agent']),
content: z.string(),
ts: z.string(),
});
export const SkillMetricSchema = z.object({
name: z.string(),
score: z.number().min(0).max(100),
delta: z.number().optional(),
});
export interface JarvisSessionDoc {
id: string;
userId: string;
productId: string;
agentId: string;
mode: SessionMode;
status: SessionStatus;
transcript: Array<{ role: 'user' | 'agent'; content: string; ts: string }>;
summary: string;
coachingNotes: string[];
exercises: string[];
skillMetrics: Array<{ name: string; score: number; delta?: number }>;
duration: number;
messageCount: number;
createdAt: string;
completedAt: string | null;
}
export const CreateSessionSchema = z.object({
agentId: z.string().min(1),
mode: z.enum(SESSION_MODES),
transcript: z.array(TranscriptEntrySchema).default([]),
summary: z.string().default(''),
coachingNotes: z.array(z.string()).default([]),
exercises: z.array(z.string()).default([]),
skillMetrics: z.array(SkillMetricSchema).default([]),
duration: z.number().min(0).default(0),
});
export const CompleteSessionSchema = z.object({
transcript: z.array(TranscriptEntrySchema),
summary: z.string().min(1),
coachingNotes: z.array(z.string()).default([]),
exercises: z.array(z.string()).default([]),
skillMetrics: z.array(SkillMetricSchema).default([]),
duration: z.number().min(0),
});
export const ListSessionsQuerySchema = z.object({
agentId: z.string().optional(),
status: z.enum(SESSION_STATUSES).optional(),
limit: z.coerce.number().min(1).max(100).default(50),
offset: z.coerce.number().min(0).default(0),
});
export type CreateSessionInput = z.infer<typeof CreateSessionSchema>;
export type CompleteSessionInput = z.infer<typeof CompleteSessionSchema>;
export type ListSessionsQuery = z.infer<typeof ListSessionsQuerySchema>;

View File

@ -59,6 +59,9 @@ vi.mock('./modules/memory/routes.js', () => ({ memoryRoutes: vi.fn() }));
vi.mock('./modules/public/routes.js', () => ({ publicRoutes: vi.fn() }));
vi.mock('./modules/tokens/routes.js', () => ({ tokenRoutes: vi.fn() }));
vi.mock('./modules/themes/routes.js', () => ({ themeRoutes: vi.fn() }));
vi.mock('./modules/jarvis-agents/routes.js', () => ({ jarvisAgentRoutes: vi.fn() }));
vi.mock('./modules/jarvis-sessions/routes.js', () => ({ jarvisSessionRoutes: vi.fn() }));
vi.mock('./modules/jarvis-memory/routes.js', () => ({ jarvisMemoryRoutes: vi.fn() }));
vi.mock('./lib/cosmos-init.js', () => ({ initCosmosIfNeeded: initCosmosIfNeededMock }));
vi.mock('./lib/config.js', () => ({ config: { CORS_ORIGIN: '*', PORT: 4003, HOST: '0.0.0.0' } }));
vi.mock('./modules/auth/jwt.js', () => ({ verifyToken: verifyTokenMock }));

View File

@ -74,6 +74,10 @@ import { feedbackRoutes } from './modules/feedback/routes.js';
import { impersonationRoutes } from './modules/impersonation/routes.js';
import { changelogRoutes } from './modules/changelog/routes.js';
import { pushTriggerRoutes } from './modules/push-triggers/routes.js';
// JarvisJr modules
import { jarvisAgentRoutes } from './modules/jarvis-agents/routes.js';
import { jarvisSessionRoutes } from './modules/jarvis-sessions/routes.js';
import { jarvisMemoryRoutes } from './modules/jarvis-memory/routes.js';
import { initCosmosIfNeeded } from './lib/cosmos-init.js';
import { config } from './lib/config.js';
import { seedDefaultFlags } from './modules/flags/seed.js';
@ -191,5 +195,9 @@ await app.register(impersonationRoutes, { prefix: '/api' });
await app.register(changelogRoutes, { prefix: '/api' });
// Push notification triggers (NomGap)
await app.register(pushTriggerRoutes, { prefix: '/api' });
// JarvisJr modules (agents, sessions, memory)
await app.register(jarvisAgentRoutes, { prefix: '/api' });
await app.register(jarvisSessionRoutes, { prefix: '/api' });
await app.register(jarvisMemoryRoutes, { prefix: '/api' });
await startService(app, { port: config.PORT, host: config.HOST });