learning_ai_common_plat/docs/AGENT_PROMPTS_SYNC_AND_COSMOS.md

25 KiB

Agent Prompts: Client-Side Sync Integration + MindLyst Cosmos Persistence

Two self-contained prompts for AI coding agents. Each prompt includes full context, exact file paths, what exists, what's missing, and verification commands. No guesswork required.


Prompt 1: Client-Side Sync Integration (ChronoMind + NomGap)

Context

Platform-service (Fastify 5, port 4003) in learning_ai_common_plat/services/platform-service/ has fully built and tested REST APIs:

ChronoMind modules (130 tests):

  • timers — 7 endpoints: GET /timers, GET /timers/sync, GET /timers/:id, POST /timers, PUT /timers/:id, DELETE /timers/:id, POST /timers/batch
  • routines — 7 endpoints: same pattern as timers at /routines/*
  • households — 9 endpoints at /households/*
  • shared-timers — 6 endpoints at /households/:householdId/timers/*

NomGap modules (52 tests):

  • fasting-sessions — 6 endpoints: POST/GET/GET/:id/PUT /fasting/sessions, GET /fasting/sessions/stats, GET /fasting/sessions/stats/weekly
  • fasting-protocols — 5 endpoints: GET/GET/:id/POST/PUT/:id/DELETE/:id /fasting/protocols
  • body-stages — 2 endpoints: GET /fasting/stages, POST /fasting/autophagy-confidence

All endpoints use JWT auth via Authorization: Bearer <token>, x-product-id header, x-request-id header, and productId field in Cosmos documents. Sync protocol uses syncVersion (monotonic integer) for optimistic concurrency, delta sync via ?since=<ISO>, and batch upsert returning { synced, conflicts, errors }.

What Already Exists (DO NOT REBUILD)

ChronoMind Web (learning_ai_clock/web/src/lib/)

  • platform-sync.ts (447 lines) — Full sync client with DTOs (SyncTimerDTO, SyncRoutineDTO, OfflineQueueItem), API functions (pullDelta, pushTimer, updateRemoteTimer, deleteRemoteTimer, batchUpsert, pullRoutineDelta, pushRoutine, updateRemoteRoutine, deleteRemoteRoutine, batchUpsertRoutines), offline queue (enqueueChange, enqueueDeleteChange, loadOfflineQueue, saveOfflineQueue), fullSync(), and DTO converters (timerToDTO, dtoToTimerPatch, routineToDTO, dtoToRoutinePatch).
  • use-sync.ts (198 lines) — React hook (useSync) with syncNow, setSyncEnabled, login, logout, syncedAddTimer, syncedUpdateTimer, syncedRemoveTimer. Auto-syncs on 60s interval. Merges pulled timers into Zustand store.
  • auth-api.ts (32 lines) — @bytelyst/auth-client wrapper with getAuthClient(), PRODUCT_ID = 'chronomind'.
  • auth-context.tsx — React auth context with login/register/logout/forgotPassword/changePassword/deleteAccount.
  • store.ts — Zustand store with addTimer, removeTimer, updateTimer, pause, resume, etc. Uses localStorage persistence.
  • routine-store.ts — Zustand store for routines.

Dashboard.tsx already imports useSync and destructures { isSyncing, syncEnabled, pendingChanges, lastError } for status display. But it does NOT call syncedAddTimer/syncedUpdateTimer/syncedRemoveTimer — timer mutations go directly to the Zustand store without enqueueing sync changes.

ChronoMind iOS (learning_ai_clock/ios/ChronoMind/Shared/Cloud/)

  • PlatformSyncManager.swift (450 lines) — Full @MainActor ObservableObject singleton with pullDelta(), pushTimer(), updateTimer(), deleteTimer(), pushOfflineQueue(), enqueueChange(), enqueueDelete(), periodic sync (60s), timerToDTO(), dtoToTimerPatch(). DTOs match server schema.
  • BUT: PlatformSyncManager is NOT called from any SwiftUI view or TimerStore. The TimerStore uses UserDefaults directly. sync(localTimers:) method exists but no view triggers it.

ChronoMind Android (learning_ai_clock/android/app/src/main/java/com/chronomind/app/sync/)

  • PlatformApiClient.kt (182 lines) — HTTP client with pullDelta(), createTimer(), updateTimer(), deleteTimer(), batchUpsert(), pullRoutinesDelta(). Uses HttpURLConnection.
  • SyncRepository.kt (246 lines) — Hilt @Singleton with sync(), enqueueCreate(), enqueueUpdate(), enqueueDelete(), offline queue in SharedPreferences. Merges pulled timers into Room via timerDao.upsert().
  • BUT: SyncRepository is NOT injected into TimerViewModel. The TimerViewModel calls TimerDao directly without sync.

NomGap (learning_ai_fastgap/src/)

  • api/client.ts (116 lines) — Fetch wrapper with auth token via @bytelyst/auth-client, x-product-id: nomgap, x-request-id, timeout, 401 retry.
  • api/auth-api.ts — Auth client with getAuthClient(), PRODUCT_ID = 'nomgap'.
  • api/fasting-api.ts (90 lines) — createSession(), getSession(), listSessions(), updateSession(), getUserStats(), getWeeklyStats() calling platform-service /fasting/sessions/*.
  • store/fasting-store.ts — Has a fire-and-forget syncSessionToBackend() that calls createSession() on session start, but .catch(() => {}) silently swallows errors. No session-end sync, no session-update sync, no history pull from server.
  • store/user-store.ts — Has loginWithAuth, registerWithAuth, hydrateFromToken, logout wired.
  • NO login/register UI screens exist yet (only store actions).

What Needs To Be Built

A. ChronoMind Web — Wire sync into timer/routine CRUD

Files to modify:

  1. web/src/components/Dashboard.tsx — Currently destructures useSync() but only uses status fields. Need to also destructure syncedAddTimer, syncedUpdateTimer, syncedRemoveTimer and call them alongside the Zustand store mutations.
  2. web/src/components/CreateTimerModal.tsx — After addTimer(newTimer), also call syncedAddTimer(newTimer).
  3. web/src/components/TimerCard.tsx — After dismiss/complete/snooze actions that call store mutations, also call syncedUpdateTimer.
  4. web/src/components/AlarmOverlay.tsx — After dismiss/snooze, call syncedUpdateTimer.
  5. web/src/components/PomodoroView.tsx — After round complete/session complete, call syncedUpdateTimer.
  6. web/src/components/RoutineEditor.tsx + web/src/components/RoutineRunner.tsx — Need routine sync. The useSync hook currently only syncs timers. Extend fullSync() in platform-sync.ts to also pull/push routines in the same sync cycle.

Pattern to follow:

// In Dashboard.tsx, pass sync functions down or lift via context
const { syncedAddTimer, syncedUpdateTimer, syncedRemoveTimer } = useSync();

// In every component that mutates timers:
// AFTER the Zustand store call (which updates UI immediately), ALSO enqueue sync
addTimer(newTimer);
syncedAddTimer(newTimer); // non-blocking — just enqueues in localStorage

Critical rules:

  • Zustand store is the source of truth for UI. Sync is fire-and-enqueue, never blocking.
  • syncedAddTimer etc. check isSyncEnabled() internally — safe to call always.
  • Don't wrap in try/catch — the enqueue is synchronous localStorage write.
  • Pass sync functions through props or React context — do NOT import useSync in every child component (breaks hook rules if used in non-component functions).

Bug to fix in fullSync() (platform-sync.ts line 294-339):

  • fullSync() currently only syncs timers. It needs to ALSO call pullRoutineDelta() and batchUpsertRoutines() for routines in the offline queue. The routine DTO conversion functions already exist (routineToDTO, dtoToRoutinePatch). Extend SyncResult to include pulledRoutines and merge them into the routine store.

B. ChronoMind iOS — Wire PlatformSyncManager into TimerStore

Files to modify:

  1. ios/ChronoMind/Shared/Store/TimerStore.swift — Add a reference to PlatformSyncManager.shared. After every timer add/update/delete in the store, call PlatformSyncManager.shared.enqueueChange(timer, .create/.update) or .enqueueDelete(timerId:).
  2. ios/ChronoMind/App/ContentView.swift or root view — On app launch, call PlatformSyncManager.shared.restoreAuthToken() equivalent (token is already restored in init). On appear, trigger initial sync.
  3. ios/ChronoMind/Views/Components/ — No changes needed; store is the single source of truth.

Critical rules:

  • PlatformSyncManager is @MainActor — safe to call from SwiftUI views.
  • The sync(localTimers:) method pulls delta AND pushes offline queue. Call it on app foreground (scenePhase == .active).
  • enqueueChange is synchronous (UserDefaults write) — call it inline after store mutations.
  • Do NOT block UI on sync. The async sync runs in background.

C. ChronoMind Android — Wire SyncRepository into TimerViewModel

Files to modify:

  1. android/app/src/main/java/com/chronomind/app/viewmodel/TimerViewModel.kt — Inject SyncRepository via Hilt constructor. After every timerDao.insert/update/delete, also call syncRepository.enqueueCreate/enqueueUpdate/enqueueDelete.
  2. android/app/src/main/java/com/chronomind/app/di/AppModule.kt — Ensure SyncRepository is provided (it's already @Singleton with @Inject constructor — Hilt should auto-provide).
  3. App startup (e.g., MainActivity.kt) — Call syncRepository.restoreAuthToken() and trigger initial sync() in a coroutine scope.

Critical rules:

  • sync() is a suspend fun — call from viewModelScope.launch(Dispatchers.IO).
  • enqueueCreate/enqueueUpdate/enqueueDelete are regular (non-suspend) functions — safe to call inline.
  • Trigger sync on app resume (Activity onResume).

D. NomGap — Complete session sync + add login UI

Files to modify:

  1. src/store/fasting-store.ts — The syncSessionToBackend() function is fire-and-forget on session START only. Add:

    • On endFast(): call updateSession(session.id, { status, endedAt, waterIntake, notes }).
    • On pauseFast()/resumeFast(): call updateSession(session.id, { status, pausedAt }).
    • On addMoodCheckin(): call updateSession(session.id, { ... }) with updated checkins.
    • On tick() stage transitions: optionally batch these (don't call API on every tick — debounce to every 5 minutes or on stage change).
    • Add a loadSessionHistory() action that calls listSessions() and populates sessionHistory from the server (for cross-device history).
  2. src/api/fasting-api.ts — Already complete. No changes needed.

  3. New file: src/screens/auth/AuthScreen.tsx — Login/register screen. Use useUserStore().loginWithAuth and registerWithAuth which already exist. Include:

    • Email + password fields
    • Login / Register toggle
    • Error display
    • "Continue without account" option (sets local-only mode)
    • Navigate to main app on success
  4. src/app/(tabs)/_layout.tsx or navigation — Add auth gate: if user not authenticated and preference !== 'local_only', show AuthScreen.

  5. src/store/fasting-store.ts startFast() — Currently generates userId: 'local'. When authenticated, use useUserStore.getState().profile?.id instead.

Critical rules:

  • NomGap is React Native (Expo) — NO web-specific APIs (localStorage, window). Use MMKV for persistence.
  • @bytelyst/auth-client is already configured in src/api/auth-api.ts.
  • Session sync should be resilient to offline — the existing .catch(() => {}) pattern is correct for fire-and-forget, but add an offline queue similar to ChronoMind's pattern for session updates that fail.
  • Do NOT sync on every tick() — debounce stage transitions to avoid API spam.

Verification Commands

# ChronoMind web
cd learning_ai_clock/web && npm test && npm run typecheck

# ChronoMind iOS
# Open ChronoMind.xcodeproj, Cmd+B

# ChronoMind Android
cd learning_ai_clock/android && ./gradlew :app:compileDebugKotlin

# NomGap
cd learning_ai_fastgap && npm test && npm run typecheck

# Platform-service (should still pass — no server changes)
cd learning_ai_common_plat && pnpm --filter @lysnrai/platform-service test

Commit Convention

  • feat(web): wire timer sync into Dashboard CRUD operations
  • feat(ios): connect PlatformSyncManager to TimerStore
  • feat(android): inject SyncRepository into TimerViewModel
  • feat(app): add session sync on end/pause/resume + auth screen

Prompt 2: MindLyst Cosmos DB Persistence — Remove In-Memory Fallbacks

Context

MindLyst web (learning_multimodal_memory_agents/mindlyst-native/web/) is a Next.js 16 App Router application with 33 API route files in src/app/api/. The data layer has a dual-mode pattern:

const container = isCosmosConfigured() ? getCosmosContainer('brains') : null;
if (container) {
  // Cosmos path
} else {
  // In-memory fallback path
}

11 routes already have working Cosmos paths (they use isCosmosConfigured() + getCosmosContainer()):

  1. brains/route.ts — container: brains
  2. memory/route.ts — container: memory_items
  3. streak/route.ts — container: streaks
  4. notifications/route.ts — container: notification_log
  5. brief/route.ts — container: daily_briefs
  6. reflection/route.ts — container: reflections
  7. share-card/route.ts — container: share_cards
  8. brain-growth/route.ts — container: brain_insights
  9. analytics/route.ts — container: analytics_events
  10. insights/route.ts — container: brain_insights
  11. seed/route.ts — calls ensureContainers()

~22 routes are PURELY in-memory — they use module-level const items: T[] = [] or let state = {...} or new Map(). These lose all data on server restart. They need to be wired to Cosmos using the same dual-mode pattern.

What Already Exists (DO NOT REBUILD)

  • src/lib/cosmos.ts (74 lines) — isCosmosConfigured(), getCosmosContainer(containerId), MINDLYST_CONTAINERS array (9 containers), ensureContainers().
  • src/lib/user.tsresolveUserId(headers) for extracting userId from request.
  • src/lib/abuse.tscheckRateLimit() for rate limiting.
  • .env.example — Shows COSMOS_ENDPOINT, COSMOS_KEY, COSMOS_DATABASE env vars.
  • Pattern in existing routes (e.g., brains/route.ts) — Shows exactly how to do the dual-mode: check isCosmosConfigured(), get container, use parameterized queries with partitionKey: userId.

What Needs To Be Built

Phase 1: Add new containers to cosmos.ts

Add these containers to MINDLYST_CONTAINERS in src/lib/cosmos.ts:

// Add to MINDLYST_CONTAINERS array:
{ id: "triage_results", partitionKey: "/userId" },
{ id: "brain_packs", partitionKey: "/userId" },
{ id: "referrals", partitionKey: "/userId" },
{ id: "ab_tests", partitionKey: "/userId" },
{ id: "waitlist", partitionKey: "/id" },        // no userId for public waitlist
{ id: "email_captures", partitionKey: "/userId" },
{ id: "engagement_data", partitionKey: "/userId" },
{ id: "context_triggers", partitionKey: "/userId" },
{ id: "brain_chats", partitionKey: "/userId" },

Phase 2: Wire each purely-in-memory route to Cosmos

For each route below, follow this exact pattern:

  1. Import getCosmosContainer, isCosmosConfigured from @/lib/cosmos
  2. At the top of the handler, get the container: const container = isCosmosConfigured() ? getCosmosContainer("<name>") : null;
  3. For every READ operation: if container, query Cosmos with parameterized SQL; else use the existing in-memory array/map.
  4. For every WRITE operation: if container, use container.items.create() / container.item(id, partitionKey).replace() / container.item(id, partitionKey).delete(); else use the existing in-memory mutation.
  5. Keep the in-memory fallback intact — don't remove it. It's needed for local dev without Cosmos.

Routes to wire (sorted by data importance):

# Route File Container Partition Key In-Memory Pattern
1 triage/route.ts triage_results /userId Module-level arrays for triage results and retry queue
2 brain-chat/route.ts brain_chats /userId new Map() for conversation history + embedding cache
3 brain-packs/route.ts brain_packs /userId Arrays for packs, submissions, moderation queue
4 referral/route.ts referrals /userId Arrays for referral links and activations
5 ab-test/route.ts ab_tests /userId Arrays for experiments and user assignments
6 waitlist/route.ts waitlist /id Array for waitlist entries (public, no userId)
7 email-capture/route.ts email_captures /userId Array for captured emails
8 engagement/route.ts engagement_data /userId Arrays for segments and campaigns
9 context-triggers/route.ts context_triggers /userId Arrays for location/calendar triggers
10 nudge/route.ts memory_items /userId Queries existing memory_items container (reads memory items > 48h not acted on) — may just need container reference, not new container
11 monitoring/route.ts analytics_events /userId Reads from analytics — may just need container reference
12 share-templates/route.ts Static data (12 templates). DO NOT persist to Cosmos — these are constants, not user data.
13 store-listing/route.ts Static data. DO NOT persist.
14 launch/route.ts Static launch plan data. DO NOT persist.
15 onboarding-email/route.ts Static email templates. DO NOT persist.
16 prompts/route.ts Computed prompts. DO NOT persist.
17 capture-config/route.ts Static config. DO NOT persist.
18 accessibility-config/route.ts Static config. DO NOT persist.
19 push-content/route.ts Static content definitions. DO NOT persist.
20 scheduler/route.ts Config/definitions for offline queue + sync. DO NOT persist.
21 thumbnails/route.ts Metadata cache. DO NOT persist (cache is ephemeral by design).
22 extract/route.ts Proxy to extraction-service. No data to persist.

Only routes 1-11 need Cosmos wiring. Routes 12-22 are static data, computed responses, or proxies — they don't store user data and should NOT be persisted.

Phase 3: Document type consistency

Every Cosmos document MUST include:

  • id: string — unique document ID
  • userId: string — partition key value
  • productId: 'mindlyst'CRITICAL: missing from current routes. Add productId: 'mindlyst' to every create() call.
  • createdAt: string — ISO timestamp
  • updatedAt: string | null — ISO timestamp on updates

Check existing routes (brains, memory, streak, etc.) and add productId: 'mindlyst' where missing.

Critical Rules

  1. Never remove the in-memory fallback. The else branch must always work for local dev without Cosmos.
  2. Always use parameterized queries — never string-interpolate user input into SQL. Pattern: { query: "SELECT * FROM c WHERE c.userId = @userId", parameters: [{ name: "@userId", value: userId }] }.
  3. Always pass { partitionKey: userId } to queries and point reads. Cosmos requires this for cross-partition query avoidance.
  4. Use container.item(id, partitionKey).read/replace/delete() for single-document operations — NOT queries.
  5. Add productId: 'mindlyst' to every document creation.
  6. Do NOT add new npm dependencies. @azure/cosmos is already in package.json.
  7. Do NOT modify the cosmos.ts client pattern. The singleton + getCosmosContainer() approach is correct.
  8. Handle Cosmos errors gracefully — wrap in try/catch, return 500 with generic error message (never expose Cosmos error details to client).
  9. Brain chat embedding cache (brain-chat/route.ts) uses a Map with 5-min TTL for performance. Keep the in-memory cache even when Cosmos is configured — persist conversation history to Cosmos but keep the embedding cache in memory.

Example Transformation

Before (pure in-memory):

const referrals: Referral[] = [];

export async function POST(request: NextRequest) {
  const body = await request.json();
  const referral = { id: `ref_${Date.now()}`, ...body };
  referrals.push(referral);
  return NextResponse.json(referral, { status: 201 });
}

After (dual-mode):

import { getCosmosContainer, isCosmosConfigured } from '@/lib/cosmos';
import { resolveUserId } from '@/lib/user';

const referrals: Referral[] = []; // in-memory fallback

export async function POST(request: NextRequest) {
  const container = isCosmosConfigured() ? getCosmosContainer('referrals') : null;
  const userId = resolveUserId(request.headers);
  const body = await request.json();
  const referral = {
    id: `ref_${Date.now()}_${crypto.randomUUID()}`,
    userId,
    productId: 'mindlyst',
    ...body,
    createdAt: new Date().toISOString(),
  };

  if (container) {
    await container.items.create(referral);
  } else {
    referrals.push(referral);
  }

  return NextResponse.json(referral, { status: 201 });
}

Verification Commands

# Type-check (catches import errors, type mismatches)
cd learning_multimodal_memory_agents/mindlyst-native/web && npx tsc --noEmit

# Build (catches runtime import issues)
cd learning_multimodal_memory_agents/mindlyst-native/web && npx next build --webpack

# Manual smoke test: start dev server, hit API routes
cd learning_multimodal_memory_agents/mindlyst-native/web && npm run dev -- -p 3050
# Then: curl http://localhost:3050/api/brains (should return default brains in-memory mode)
# Then: curl http://localhost:3050/api/streak (should return streak)

Verification Checklist (agent must confirm each)

  • npx tsc --noEmit passes with zero errors
  • npx next build --webpack succeeds
  • All 11 routes that need Cosmos wiring now import isCosmosConfigured + getCosmosContainer
  • Every container.items.create() call includes productId: 'mindlyst'
  • In-memory fallback still works (test with no COSMOS_* env vars set)
  • No new npm dependencies added
  • MINDLYST_CONTAINERS in cosmos.ts updated with new container definitions
  • seed/route.ts ensureContainers() will auto-create all new containers

Commit Convention

  • feat(web): add Cosmos containers for triage, brain-chat, brain-packs, referrals, ab-tests, waitlist, email-capture, engagement, context-triggers
  • feat(web): wire triage + brain-chat + brain-packs routes to Cosmos DB
  • feat(web): wire referral + ab-test + waitlist + email-capture routes to Cosmos DB
  • feat(web): wire engagement + context-triggers + nudge + monitoring routes to Cosmos DB
  • fix(web): add productId: 'mindlyst' to all Cosmos document creation calls