merged adiuvAI and api repo

This commit is contained in:
Roberto
2026-06-12 18:01:41 +02:00
parent b9148c67a0
commit 53962a05a4
16 changed files with 112773 additions and 127 deletions

View File

@@ -1,174 +1,297 @@
# CLAUDE.md
## Commands
Guide Claude Code when work in repo.
## Keeping This File Up to Date
Update when lesson learned. Update when:
- Non-obvious arch decision made or found
- Gotcha, footgun, surprising behavior hit (+ fix/workaround)
- New command, workflow, tool added
- Convention set that not obvious from code
- Integration detail clarified (IPC protocol behavior, agent tool call edge cases)
Do **not** add derivable-from-code things, generic best practices, or ephemeral task notes — durable knowledge only.
> graphify rules live in the root `CLAUDE.md` (single source).
## Repository Layout
**Single merged monorepo.** Electron app and FastAPI backend were previously separate submodules; they now live as plain subdirectories in this repo.
| Directory | What |
|-----------|------|
| **`electron/`** | Electron desktop app (TypeScript/React) |
| **`api/`** | FastAPI backend (Python) |
| **`docs/`** | Planning docs & working memory |
| **`graphify-out/`** | Knowledge graph (see root `CLAUDE.md`) |
---
## Electron App (`electron/`)
### Commands
```bash
source ~/.nvm/nvm.sh && npm start # Dev with hot-reload
source ~/.nvm/nvm.sh && npm run make # Build distributable packages
source ~/.nvm/nvm.sh && npm run package # Package without installers
source ~/.nvm/nvm.sh && npm run lint # ESLint (.ts/.tsx)
source ~/.nvm/nvm.sh && npx drizzle-kit generate # Generate migration from schema
source ~/.nvm/nvm.sh && npx drizzle-kit push # Push schema directly (dev only)
cd electron
npm run start # Dev (Electron + Vite)
npm run lint # ESLint
npm run knip # Dead code analysis
npm run make # Build installers (Win/Linux/macOS)
npm run package # Package without installers
npm run dev:web # Standalone web SPA dev
npm run build:web # Build standalone SPA → dist-web/
npm run preview:web # Preview built web SPA
npx drizzle-kit generate # Generate migration from schema
npx drizzle-kit push # Push schema directly (dev only)
```
No test suite currently.
## Architecture
AdiuvAI is a local-first Electron desktop app. The three processes communicate via a custom tRPC v11 ↔ IPC bridge (the public `electron-trpc` package is incompatible with tRPC v11).
### Architecture
```
Renderer (React 19) ──ipcLink──► Preload (contextBridge) ──IPC──► Main (tRPC router + SQLite)
Renderer (React 19 + TanStack Router)
↓ custom ipcLink (NOT electron-trpc — incompatible with tRPC v11)
Preload (contextBridge: window.electronTRPC + window.electronAI)
↓ IPC channels
Main Process (Node.js)
├── tRPC router (CRUD + AI proxy procedures)
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
└── Backend delegation layer (orchestrator.ts forwards to FastAPI WS)
```
### Main Process (`src/main/`)
**Local-first storage, cloud AI.** All user data (clients, projects, tasks, notes, timelines) in local SQLite. AI lives entirely on the FastAPI backend — Electron orchestrator is a thin delegation shell that forwards to `/api/v1/device` WS and dispatches v3 typed stream frames + tool-call ↔ DrizzleExecutor round-trips back to renderer.
Owns the database and all business logic.
**IPC channels**:
- `'trpc'` — bidirectional tRPC request/response (all CRUD + auth + scout + memory proxy)
- `'ai:stream'` — one-way v3 stream frames main → renderer
- `'ai:action'` — AI side-effects (e.g. agent auto-creates task)
| File | Purpose |
|---|---|
| `index.ts` | Window creation, app lifecycle |
| `ipc.ts` | Bridges `ipcMain` to tRPC procedures |
| `router/index.ts` | All tRPC sub-routers merged into `appRouter` |
| `db/index.ts` | Drizzle + better-sqlite3, WAL mode, singleton `getDb()` |
| `db/schema.ts` | Table definitions: clients, projects, tasks, checkpoints, notes, noteEdits, taskComments |
| `db/notes-backfill.ts` | Startup backfill: generates aiSummary for notes with null summary |
| `store.ts` | electron-store for persistent UI settings |
**Main process layout (`src/main/`)**:
- `index.ts` — Window creation, app lifecycle, protocol handler
- `ipc.ts` — Custom tRPC↔IPC bridge
- `store.ts` — electron-store for `FormatPrefs` + `uiLanguage`; exports `getUiLanguage()`
- `router/index.ts` All tRPC sub-routers (~1627 LOC)
- `db/schema.ts` — 10 tables: clients, projects, tasks, timelineEvents, timelineEventDependencies, notes, noteEdits, taskComments, scoutRuns, scoutRunActions
- `db/index.ts` — Drizzle + better-sqlite3 (WAL), singleton `getDb()`, `initDb()` migrations
- `db/notes-backfill.ts` Startup backfill: generates `aiSummary` for notes with null summary
- `ai/orchestrator.ts` — Thin backend-delegation layer (~304 LOC). Connectivity/auth guard → `BackendClient.sendHomeRequest()` / `sendFloatingRequest()` → forwards v3 stream frames to renderer. Also schedules daily-brief regeneration.
- `ai/token.ts` — Two-tier token storage (safeStorage + electron-store fallback)
- `scouts/scout-scheduler.ts` — Local scout scheduling (filesystem scouts)
- `api/backend-client.ts` — WS client to FastAPI: handles tool-call round-trips, v3 stream frame dispatch, journey + scout proxies
- `api/drizzle-executor.ts` — Executes backend-issued tool calls against local SQLite. Wraps results through `formatRow()`/`formatRows()` using user FormatPrefs
- `auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
- `auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
- `auth/locale-defaults.ts` — Detects timezone, date/time format, language from OS locale
### Preload (`src/preload/trpc.ts`)
**tRPC routers** (in `appRouter`): `health`, `settings`, `clients`, `projects`, `tasks`, `timelineEvents`, `timelineEventDependencies`, `notes`, `noteEdits`, `taskComments`, `ai`, `auth`, `scout` (with `local` / `cloud` / `journey` sub-routers), `memory`.
Exposes `window.electronTRPC` with `sendMessage()` / `onMessage()`.
**Renderer** (`src/renderer/`): file-based routing via TanStack Router (`routeTree.gen.ts` auto-generated). shadcn/ui new-york theme, neutral colors. Path alias `@/*``src/renderer/*`. Notes editor: Milkdown (`@milkdown/crepe`).
### Renderer (`src/renderer/`)
**Non-obvious details**:
- `electron-trpc` NOT used — custom IPC bridge (`ipc.ts` + `lib/ipcLink.ts`) because electron-trpc bundles tRPC v10 internals
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
- `forge.config.ts` has cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3)
- DB has no foreign key constraints — cascade deletes in tRPC procedures
- Timestamps are milliseconds (`Date.getTime()`), not ISO strings
- Notes use `aiSummary` (≤250 char, backend `gpt-4o-mini` via `POST /api/v1/scouts/notes/summarize`) for AI navigation — LanceDB fully removed
- AI note edits go through `noteEdits` HITL table (`type: append|insert|replace`, `status: pending|approved|rejected`); backend tool `propose_note_edit` → drizzle-executor inserts row; user approves/rejects in UI; auto-reject on missing anchor
- `checkpoints` table replaced by `timelineEvents` + `timelineEventDependencies` (events are typed `milestone|checkpoint|activity`, with optional dep edges)
- `scoutRuns` + `scoutRunActions` populated by backend-client on tool_call/run_complete frames; UI reads via `scout.runs` / `scout.runActions`
React 19 — never accesses Node APIs directly. All data through `trpc.*.useQuery()` / `trpc.*.useMutation()`.
**Settings Page (shared Electron + Web)**:
- Settings page runs in **both** Electron and standalone web SPA. Same React components — no duplication.
- **Platform Adapter**: `PlatformProvider` context (`src/renderer/lib/platform.tsx`) exposes `isElectron`/`isWeb`/`hasLocalAgents`/`hasFileDialog`. Components use `usePlatform()` to gate Electron-only features.
- **Web build**: `vite.web.config.mts``dist-web/`. Entry: `web.html``src/renderer/web-main.tsx` (uses `httpBatchLink` via `lib/httpLink.ts` instead of `ipcLink`).
- **Electron-only gating**: Device ID card and local scout filesystem gated behind `platform.isElectron`. On web: visible but disabled, not hidden.
- **Gotcha**: Do NOT add Electron-specific settings (server URL, native file pickers) without wrapping in `platform.isElectron`. Same component tree renders on web.
| File | Purpose |
|---|---|
| `lib/ipcLink.ts` | Custom TRPCLink routing through `window.electronTRPC` |
| `lib/trpc.ts` | `createTRPCReact<AppRouter>()` typed client |
| `index.tsx` | QueryClient + tRPC + Router providers |
**Onboarding Wizard**:
- First-run wizard collects 5 fields: `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`. Plus `user_name` from `name`+`surname`.
- All fields stored as encrypted core memory (backend `MemoryMiddleware`), not local electron-store.
- `onboarding_completed_at` on `users` table (nullable TIMESTAMPTZ) gates flow — `null` = show wizard, non-null = skip.
- `AppShell.tsx` gates: if `profile.onboardingCompletedAt == null` → render `<OnboardingFlow>` instead of app.
- `auth.status` tRPC procedure auto-seeds `language` and `user_name` into MemoryCore if missing (fire-and-forget `.catch(() => {})`).
- Format prefs (timezone, dateFormat, timeFormat) stored in electron-store (`FormatPrefs`), not core memory — device-specific.
- `drizzle-executor.ts` wraps all query results through `formatRow()`/`formatRows()` using user FormatPrefs.
- Settings > Profile allows post-onboarding edit of all fields + format prefs.
- **Gotcha — shadcn Button `outline` variant in dark mode**: variant defines `dark:bg-input/30 dark:border-input dark:hover:bg-input/50` — overrides custom `className` background. Fix: switch between `variant="default"` and `variant="outline"` instead of className overrides.
- **Gotcha — locale codes vs human names**: `app.getLocale()` and `navigator.language` return codes like `en-US`. Use `Intl.DisplayNames(undefined, { type: 'language' })` to convert to "English". Must do in both main process (`locale-defaults.ts`) and renderer (`OnboardingFlow.tsx`).
### Routing
**i18n (Internationalization)**:
- `i18next` + `react-i18next` with bundled JSON translations (no lazy loading).
- Config in `src/renderer/i18n.ts`. 5 languages: EN, IT, ES, FR, DE. `SUPPORTED_LANGUAGES` exported for UI selectors.
- Translation files: `src/renderer/locales/{en,it,es,fr,de}/translation.json`. Namespaces: `nav`, `auth`, `tasks`, `settings`, `common`, `errors`, `home`, `timeline`, `projects`, `scouts`.
- **`common.*` namespace** holds shared labels (`save`, `cancel`, `delete`, `edit`, `add`, `rename`, `saving`, `deleting`, `creating`, `renameDescription`, `deleteTitle`). Check `common.*` before adding new key.
- Pluralization uses i18next `_one`/`_other` suffixes.
- `LanguageSync` component in `src/renderer/index.tsx` reads persisted `uiLanguage` from electron-store via tRPC on startup, syncs to i18next.
- Language selector in `GeneralSection.tsx` (Settings > General). On change: (1) calls `i18n.changeLanguage()`, (2) persists to electron-store via `setUiLanguage` mutation, (3) writes to backend core memory so AI responds in same language.
- `getUiLanguage()` exported from `src/main/store.ts`.
- Static data arrays needing translation use `labelKey` pattern: store translation key, call `t(labelKey)` at render. Used in `NAV_ITEMS`, `COLUMNS`, `SECTIONS`, `SUGGESTION_CHIPS`.
- When adding new translated text: add key to **all 5** JSON files. Keep `common.*` consistent across all languages.
File-based via TanStack Router (`tsr.config.json` at root). Route tree auto-generated at `routeTree.gen.ts`.
**Google OAuth (adiuvAI side)**:
- `adiuvai://` NOT accepted by Google as redirect URI — Google only accepts `http://localhost` or `https://`. API backend exposes `GET /auth/oauth/google/web-callback` which receives Google redirect and bounces to `adiuvai://oauth/callback?...`. Redirect URI in Google Cloud Console points to backend, not Electron app.
- `app.requestSingleInstanceLock()` required for `second-instance` event on Windows/Linux. If returns `false`, call `app.quit()` immediately.
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as third arg so OS protocol registration includes entry script.
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) — authorize endpoint is public, `get()` throws when not authenticated.
- Backup key in `backup-key.ts` stored in `encryptedTokens` under key `backup_key`, reusing `getToken/setToken` from `token.ts`. Device-bound, never password-derived — social-login users can use backup features.
Routes: `__root.tsx` (AppShell layout), `index`, `tasks`, `timeline`, `projects`, `notes.$noteId`
---
### tRPC Routers
## api (FastAPI Backend)
`health`, `settings`, `clients`, `projects`, `tasks`, `checkpoints`, `notes`, `noteEdits`, `taskComments`, `ai`
### Commands
### Database
```bash
cd api
Schema in `src/main/db/schema.ts`, migrations in `src/main/db/migrations/`. DB created in Electron's `userData` as `adiuvai.db`. On startup, `initDb()` runs non-destructive migrations.
# Development
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
To add a table/column: edit `schema.ts``drizzle-kit generate``drizzle-kit push` (dev) or commit the migration.
# Production
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120
### Adding a Feature (end-to-end)
# Database migrations
alembic upgrade head
1. **Schema**`src/main/db/schema.ts`
2. **Router** — Add sub-router in `src/main/router/index.ts`, merge into `appRouter`
3. **Types** — Flow automatically via `AppRouter` export
4. **UI** — Components in `src/renderer/components/<feature>/`, data via `trpc.*.useQuery()`
# Testing
pytest # all tests
pytest -v # verbose
pytest tests/test_deep_agent.py # single file
pytest tests/test_deep_agent.py -k test_name # single test
## AI Subsystem (`src/main/ai/`)
# Linting/formatting
ruff check .
ruff format .
LangGraph-based agentic system with pluggable LLM providers.
# Docker (full stack)
docker compose up --build
```
### Orchestrator (`orchestrator.ts`)
### Architecture
Classifies user intent → routes to a specialist agent:
```
FastAPI app (app/main.py)
├── Lifespan: APScheduler crons (memory hourly + audit weekly) when SCHEDULER_ENABLED
├── Middleware: TierRateLimit → Sanitizer → CORS
├── HTTP Routes (app/api/routes/) — all under /api/v1
│ ├── auth.py — register, login, refresh, profile, OAuth, onboarding, password
│ ├── chat.py — POST /chat, /chat/brief, /chat/embed
│ ├── scouts.py — catalog, can-create, trigger, notes/summarize
│ ├── scout_setup.py — guided scout setup (journey)
│ ├── billing.py — Stripe checkout, webhook, subscription, invoices
│ ├── device_ws.py — WS /device (unified streaming endpoint: home, floating, brief, journey)
│ └── memory.py — core / relational / forget-all
├── Agent System (app/agents/)
│ ├── task_agent.py
│ ├── project_agent.py
│ ├── note_agent.py
│ ├── timeline_agent.py
│ └── filesystem_agent.py
├── Core (app/core/)
│ ├── deep_agent.py — main agent runner (run_home / run_floating / run_brief / run_journey)
│ ├── brief_agent.py — daily brief generation
│ ├── agent_runner.py — local + cloud agent run executor
│ ├── agent_session_buffer.py — per-session conversation buffer
│ ├── agent_registry.py — decorator-based agent registry
│ ├── llm.py — LiteLLM factory (multi-provider)
│ ├── memory_middleware.py — encrypted core memory read/write
│ ├── memory_extraction.py — LLM extraction from conversation tail
│ ├── memory_maintenance.py — drain queue, contradiction audit, proactive mining
│ ├── note_summarizer.py — gpt-4o-mini summary for notes
│ ├── output_formatter.py — render agent output to user-facing markdown
│ ├── embeddings.py
│ ├── device_manager.py — device registration / WS session tracking
│ ├── ws_context.py — per-WS user context plumbing
│ ├── langfuse_client.py — Langfuse prompt + tracing client
│ └── preprocessors/ — input preprocessors (e.g. email_html.py)
├── Auth (app/auth/oauth_providers.py) — GoogleOAuthProvider (httpx + manual PKCE)
├── Billing (app/billing/) — tier_manager + stripe_service
├── Integrations (app/integrations/) — gmail.py, ms_graph.py
└── Models (app/models.py) — SQLAlchemy 2.0 ORM
```
| Agent | Scope | Tools |
|---|---|---|
| Project | Project-scoped Q&A | `read_project_notes`, `add_task`, `get_summary`, `suggest_checkpoints`, `suggest_tasks` |
| Knowledge | Cross-project search | `list_notes` + `get_note` (aiSummary-based navigation) |
| General | Workspace-wide | `add_task` |
**HTTP route prefix**: every router included with `prefix="/api/v1"`. So `/api/v1/auth/...`, `/api/v1/chat`, `/api/v1/scouts/...`, `/api/v1/memory/...`, `/api/v1/device` (WS).
All providers use LangChain `bindTools()` + ToolMessage loop (max 5 iterations).
**ORM models** (`app/models.py`): `User`, `RefreshToken`, `OAuthAccount`, `Subscription`, `LocalScoutConfig`, `CloudScoutConfig`, `ScoutRunLog`, `MemoryCore`, `MemoryAssociative`, `MemoryEpisodic`, `MemoryProactive`, `ExtractionQueue`, `MemoryRelation`, `Plugin`. PostgreSQL (asyncpg + SQLAlchemy 2.0 async). Alembic migrations in `alembic/versions/`.
Also exports `dailyBrief()` for AI-generated daily summaries (`ai.dailyBrief` tRPC mutation).
**Lifespan crons** (only if `settings.SCHEDULER_ENABLED`):
- `_memory_cron_tick` — hourly: drains Free-tier extraction queue + mines proactive patterns for Power+ users
- `_memory_audit_cron_tick` — weekly: contradiction scan + label canonicalization for all users (Phase 7)
### Streaming
**LLM routing**: backend agents own all intelligence. Tool calls describe client-side ops (JSON) → Electron `drizzle-executor` runs them against local SQLite → result returned to backend over WS. Tool loop cap inside agent runner prevents runaway iteration.
`sendStreamChunk(sender, token, done)` over IPC `'ai:stream'`. Renderer subscribes via `window.electronAI.onStreamChunk()` in `AIChatPanel.tsx`. `<tool_call>` blocks are filtered before display.
**Zero-trust data model**: backend never stores raw user content. PostgreSQL holds auth, billing, plugin metadata, encrypted memory (Core/Associative/Episodic/Proactive/Relational), scout configs, run logs.
### Providers (`llm.ts`)
**Config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev.
| Provider | Model | Notes |
|---|---|---|
| OpenAI | `gpt-4o-mini` | Via LangChain |
| Anthropic | `claude-sonnet-4-20250514` | Via LangChain |
| Copilot | `ChatCopilot` wrapper | `copilot.ts` / `chat-copilot.ts` |
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py`. Active suites: agent runner, auth, brief/deep agents, device WS, integrations, journey, memory (audit/extraction/middleware/models/proactive/relations), middleware, output formatter, preprocessors, schemas, ws_unified.
All use `temperature: 0.3`, streaming enabled. Provider management in `provider.ts`.
### Non-obvious details
**Token storage** (`token.ts`) — two-tier fallback:
1. electron-store + `safeStorage` — encrypted at rest (preferred)
2. Plain electron-store — last resort (e.g. WSL with no keyring)
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` — tier changes take effect immediately, no re-login needed
- **Refresh tokens hashed**: plaintext returned to client, stored as SHA-256 in DB — server can never retrieve plaintext (intentional)
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
- **Unified device WS**: `/api/v1/device` is the single bidirectional channel. Handles home requests, floating requests, daily briefs, journeys, heartbeats. Tool calls round-trip through the same socket
- **Prompt IP protection**: prompts kept server-side via Langfuse (`langfuse_client`). `SanitizerMiddleware` strips leaked fragments from responses
- **Agents don't execute operations**: tools return JSON describing client-side ops — Electron client executes against local SQLite
- **Alembic async/sync split**: app uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2``env.py` handles URL conversion
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
- **Run-disconnect tracking**: `_mark_runs_disconnected` flips active runs when WS drops so client can resume cleanly
### Notes AI Navigation (aiSummary index)
**Onboarding (API side)**:
- `PUT /auth/me/memory` — updates core memory k/v pairs, optionally marks onboarding complete (`mark_onboarded: true` sets `users.onboarding_completed_at`).
- `POST /auth/me/onboarding/reset` — nullifies `onboarding_completed_at` so wizard re-runs.
- `POST /auth/onboarding/normalize` — LLM-normalizes free-text onboarding inputs via `gpt-4o-mini`; returns inputs unchanged on error.
- `get_current_user()` in `auth.py` middleware decrypts core memory blocks, includes in `UserProfile.memory` dict.
- `users.onboarding_completed_at` — nullable TIMESTAMPTZ, returned as epoch ms (int) in UserProfile schema.
Notes have `aiSummary` (≤250 char, nullable) and `aiSummaryUpdatedAt` columns. Generated by backend `POST /api/v1/scouts/notes/summarize` (gpt-4o-mini, Langfuse `note_summary` prompt).
**i18n (API side)**:
- `_language_instruction()` in `app/core/deep_agent.py` reads user's `language` from `MemoryCore`, appends system prompt directive ("Always respond in {language}") to all `run_*` functions.
- Electron client writes chosen language to backend core memory on change — API picks up on next agent call.
- `list_notes` tool output includes the summary per note so AI can navigate without reading full content.
- `notes-backfill.ts` generates missing summaries on startup (throttled 1 req/s, skipped when offline).
- Summary is regenerated fire-and-forget on note create/update and on HITL approve.
**Google OAuth (api side)**:
- OAuth routes in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
- Provider abstraction in `app/auth/oauth_providers.py``GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 implemented manually via `generate_pkce_pair()`.
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev, doesn't survive restarts, doesn't scale to multiple workers. Replace with Redis in production.
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` required before creating linked `OAuthAccount` to populate `new_user.id` before commit.
- `OAUTH_REDIRECT_URI` must point to **API backend** (e.g. `https://api.adiuvai.com/...`).
- **Unverified email + existing account = 409**: if `email_verified=False` and email already registered, callback returns 409. Without this guard, branch 3 would INSERT duplicate email and crash with DB constraint violation (500).
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restart.
### Notes HITL (`noteEdits` table)
### Tier System
AI-proposed note edits go to `noteEdits` instead of directly modifying `notes.content`:
- `type: append | insert | replace` — append adds at end; insert after `anchorBefore` text; replace replaces `anchorText`.
- `status: pending | approved | rejected` — pending shows in UI with dashed border + Approve/Reject.
- On approve: content merged into `notes.content`; summary regenerated. If anchor not found (note edited since proposal), auto-rejects.
- `propose_note_edit` backend tool → drizzle-executor `propose_note_edit` case → inserts `noteEdits` row.
- `noteEditsRouter` in `router/index.ts`: `list`, `listPending`, `approve`, `reject`.
Source of truth: `app/billing/tier_manager.py` (`FEATURES` + `RATE_LIMITS` dicts).
### AI Approval Pattern
| Feature | Free | Pro | Power | Team |
|---------------------|--------|-----------|-----------|-----------|
| Rate limit | 20/min | 60/min | 120/min | 200/min |
| Providers | 1 | unlimited | unlimited | unlimited |
| Relational memory | no | yes | yes | yes |
| Proactive mining | no | no | yes | yes |
Tasks and checkpoints have `isAiSuggested` + `isApproved` columns. AI suggestions appear pending user approval (dashed borders in UI).
`tier_manager.get_tier()` falls back to `'power'` in dev (`settings.ENV == 'dev'`) when no subscription found, else `'free'`. Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `tier_manager.check_feature()` calls scattered through agent + memory paths.
## Config Notes
---
- **BackendClient case conversion (footgun)**: `backend-client.ts` `proxyGet`/`proxyPost`/`proxyPut` **camelCase the response** and **snake_case the request body**. The FastAPI BE speaks snake_case; the TS side speaks camelCase. So a BE response `{authorize_url}` arrives as `{authorizeUrl}` — reading `data.authorize_url` returns `undefined`. Always type the proxy generic with camelCase keys and read camelCase. (Real bug: `shell.openExternal(data.authorize_url)``undefined` → Electron throws `Error processing argument at index 0, conversion failure from undefined`.)
- Vite configs use `.mts` (not `.ts`) — avoids ESM/CJS conflicts with electron-forge
- `@/*` path alias → `src/renderer/*` (TypeScript + Vite + shadcn/ui)
- **shadcn/ui**: new-york style, neutral base color
- **Icons**: lucide-react only — do not introduce other icon libraries
- **Tailwind 4** — CSS variable theming in `globals.css`, no `tailwind.config.js`
- **Notes editor**: Milkdown (`@milkdown/crepe`) at `src/renderer/components/notes/MilkdownEditor.tsx`
## Cross-Project Integration
## Design Context
Electron app and FastAPI backend communicate via **WebSocket** (`/api/v1/device`):
### Users
Freelancers and solo professionals managing client work (projects, tasks, notes, timelines). Single workspace, no enterprise overhead. AI as force multiplier. They open the app mid-workday — often stressed — so the interface must feel immediately grounding and in control.
1. Electron connects with `?token=<jwt>` query param
2. Client sends typed request frames (home / floating / brief / journey_start / journey_message)
3. Server streams v3 typed frames (text deltas, tool_call, run_complete, error)
4. Tool call frames → Electron `drizzle-executor` runs against local SQLite → returns `tool_result` over same socket
5. Heartbeat loop keeps connection alive; backend marks runs disconnected on drop
### Brand Personality
**Calm. Intelligent. Warm.** A thoughtful companion, not a flashy tool. Confident and understated — never loud, gamified, or corporate. Fully original aesthetic (no external design system references; this look is intentional and owned).
There is no fully-local AI fallback — the Electron orchestrator is a thin delegation shell that requires connectivity + auth. If offline or logged out, `checkConnectivity()` short-circuits with a user-facing error.
### Emotional Goal
When a user opens AdiuvAI, the first impression should communicate **"everything is under control"** — calm clarity over urgency. The design should lower cognitive load, not raise it.
---
### Aesthetic Direction
- Light mode: pinkish-white canvas `#f4edf3`, golden yellow primary `#fbc881`, slate blue-gray secondary `#8a8ea9`, dusty lavender borders `#c8c3cd`
- Dark mode: near-black `#0c0c0c`, pure white primary, dark gray `#323232` surfaces
- Geist sans-serif, weights 400/500/600. Tight tracking (`-1px`) on headings. Body `text-sm`, metadata `text-xs`
- 10px border-radius (`rounded-lg`), `rounded-2xl` for chat/AI elements
- Glassmorphism on AI inputs (`backdrop-blur-xl`, transparency, gradient border via padding-box/border-box technique)
- Spring animations (stiffness 400, damping 30), scale-and-fade transitions
- No gamification (badges, streaks, confetti). Mature and professional
- Dashed borders + Sparkles icon = AI-pending state marker
## MCP Servers
### Accessibility
Best-effort — not formally audited. Maintain reasonable contrast and keyboard operability without targeting a specific WCAG level.
### Current Design Focus
**Polish and refinement.** The overall direction is solid; the priority is elevating specific areas that feel rough or inconsistent — tighter spacing, more intentional hierarchy, better empty/loading states, and smoother motion.
### Design Principles
1. **Clarity over cleverness** — Clear hierarchy, generous whitespace, comfortable density. Never sacrifice legibility for style.
2. **AI as quiet partner** — Deeply integrated but never intrusive. Dashed borders for pending AI items, Sparkles icon as the sole AI marker. Surface AI capabilities without making them the hero.
3. **Warmth in restraint** — The warm palette feels approachable without being playful. Dark mode trades warmth for focus. Neither mode should feel cold or aggressive.
4. **Motion with purpose** — Spring animations reinforce spatial relationships and acknowledge state changes. Never purely decorative. Respect reduced-motion preferences where possible.
5. **Polish over features** — Every surface should feel considered. Prefer refining what exists over introducing new complexity. The right amount of visual weight is the minimum needed.
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — workspace-level, prompt management docs
- **shadcn** (`npx shadcn@latest mcp`) — configured in `electron/` for UI component generation

269
.claude/CLAUDE.original.md Normal file
View File

@@ -0,0 +1,269 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Keeping This File Up to Date
Update this file whenever a lesson is learned during development. Specifically, update CLAUDE.md when:
- A non-obvious architectural decision is made or discovered
- A gotcha, footgun, or surprising behavior is encountered (and the fix/workaround)
- A new command, workflow, or tool is added to the project
- A convention is established that isn't obvious from reading the code
- An integration detail is clarified (e.g., how the IPC protocol actually behaves, edge cases in the agent tool call cycle)
Do **not** add things already derivable from reading the code, generic best practices, or ephemeral task notes — only durable, reusable knowledge.
## Repository Layout
This is a **monorepo with git submodules**. Each submodule is an independent repo with its own `.claude/CLAUDE.md` for detailed guidance.
| Directory | What | Submodule |
|-----------|------|-----------|
| **`adiuvAI/`** | Electron desktop app (TypeScript/React) | `git.muticolturano.com/adiuvAI/adiuvAI` |
| **`api/`** | FastAPI backend (Python) | `git.muticolturano.com/adiuvAI/api` |
| **`website/`** | Landing page (single `index.html`) | `git.muticolturano.com/adiuvAI/website` |
| **`docs/`** | Planning docs & working memory (not a submodule) | -- |
After cloning, run `git submodule update --init --recursive` to populate submodule contents.
---
## adiuvAI (Electron App)
> **Detailed docs**: `adiuvAI/.claude/CLAUDE.md` covers commands, architecture, AI subsystem, design context, and conventions in depth.
### Commands
```bash
cd adiuvAI
npm run start # Start dev server (Electron + Vite)
npm run lint # ESLint
npm run knip # Dead code analysis
npm run make # Build installers (Windows/Linux/macOS)
npm run package # Package without creating installers
npx drizzle-kit generate # Generate migration from schema changes
npx drizzle-kit push # Push schema directly (dev only)
```
No test suite currently.
### Architecture
```
Renderer (React 19 + TanStack Router)
↓ custom ipcLink (NOT electron-trpc — incompatible with tRPC v11)
Preload (contextBridge: window.electronTRPC + window.electronAI)
↓ IPC channels
Main Process (Node.js)
├── tRPC router (all CRUD + AI procedures)
├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
├── LanceDB (vector embeddings, 1536-dim text-embedding-3-small)
└── LangGraph orchestrator (3 specialist agents, pluggable LLM providers)
```
**This is a local-first app.** All user data (tasks, notes, projects) lives in local SQLite. The AI system (LangGraph + LangChain) runs entirely in the Electron main process with pluggable providers (OpenAI, Anthropic, GitHub Copilot).
**IPC channels**:
- `'trpc'` — bidirectional tRPC request/response (all CRUD)
- `'ai:stream'` — one-way token streaming from main → renderer
- `'ai:action'` — AI side-effects (e.g., task auto-created by agent)
**Key source paths**:
- `src/main/ipc.ts` — Custom tRPC↔IPC bridge
- `src/main/router/index.ts` — All tRPC routers (~600 LOC)
- `src/main/ai/orchestrator.ts` — LangGraph intent routing + 3 agents (~991 LOC)
- `src/main/db/schema.ts` — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
- `src/renderer/routes/` — File-based routing (TanStack Router auto-generates `routeTree.gen.ts`)
- `src/renderer/components/ui/` — shadcn/ui primitives (new-york theme, neutral colors)
- `src/main/auth/auth-manager.ts` — Login, register, logout, OAuth flow (singleton)
- `src/main/auth/backup-key.ts` — Device-specific AES-256 backup key (safeStorage, not password-derived)
- `src/main/ai/token.ts` — Two-tier token storage: safeStorage + electron-store fallback
- `src/main/auth/locale-defaults.ts` — Detects timezone, date/time format, language from OS locale
- `src/main/api/format-row.ts` — Formats timestamp columns in query results using user's FormatPrefs
**Non-obvious details**:
- `electron-trpc` is NOT used — custom IPC bridge in `ipc.ts` + `ipcLink.ts` because electron-trpc bundles tRPC v10 internals
- Vite configs use `.mts` extension to avoid ESM/CJS conflicts with electron-forge
- `forge.config.ts` has complex cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3 and LanceDB)
- DB has no foreign key constraints — cascade deletes are implemented in tRPC procedures
- Timestamps are milliseconds (JavaScript `Date.getTime()`), not ISO strings
- Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)
**Settings Page (shared between Electron and Web)**:
- The Settings page is designed to run in **both** the Electron app and a standalone web SPA (future landing-page user portal). The same React components are used — no duplication.
- **Platform Adapter pattern**: `PlatformProvider` context (`src/renderer/lib/platform.tsx`) exposes `isElectron`/`isWeb`/`hasLocalAgents`/`hasFileDialog` flags. Components use `usePlatform()` to conditionally render Electron-only features (device ID, local agent filesystem) or disable them on web.
- **6 sections**: Profile, AI Preferences, Account, Billing, Appearance, Agents. Sidebar nav with icons in `types.ts` (`SECTIONS` array).
- **Web build**: `vite.web.config.mts` builds a standalone SPA to `dist-web/`. Entry: `web.html``src/renderer/web-main.tsx` (uses `httpBatchLink` via `src/renderer/lib/httpLink.ts` instead of `ipcLink`). Scripts: `npm run dev:web`, `npm run build:web`, `npm run preview:web`.
- **Electron-only gating**: Device ID card and local agent filesystem features are gated behind `platform.isElectron`. On web, local agents are visible but disabled (not hidden).
- **Gotcha**: Do NOT add Electron-specific settings (e.g. server URL, native file pickers) without wrapping in `platform.isElectron`. The same component tree renders on web.
**Onboarding Wizard**:
- First-run wizard collects 5 fields: `job_role`, `industry`, `primary_use_case`, `tone_preference`, `language`. Plus `user_name` derived from profile `name`+`surname`.
- All fields stored as encrypted core memory (backend `MemoryMiddleware`), not local electron-store.
- `onboarding_completed_at` on the `users` table (nullable TIMESTAMPTZ) gates the flow — `null` = show wizard, non-null = skip.
- `AppShell.tsx` gates: if `profile.onboardingCompletedAt == null` → render `<OnboardingFlow>` instead of the app.
- `auth.status` tRPC procedure auto-seeds `language` and `user_name` into MemoryCore if missing (fire-and-forget `.catch(() => {})`).
- Format prefs (timezone, dateFormat, timeFormat) are stored in electron-store (`FormatPrefs`), not core memory — they're device-specific.
- `drizzle-executor.ts` wraps all query results through `formatRow()`/`formatRows()` using the user's FormatPrefs.
- Settings > Profile section allows post-onboarding editing of all fields + format prefs.
- **Gotcha — shadcn Button `outline` variant in dark mode**: The variant defines `dark:bg-input/30 dark:border-input dark:hover:bg-input/50` which overrides any custom `className` background. Fix: switch between `variant="default"` and `variant="outline"` instead of adding className overrides.
- **Gotcha — locale codes vs human names**: `app.getLocale()` and `navigator.language` return codes like `en-US`. Use `Intl.DisplayNames(undefined, { type: 'language' })` to convert to "English". This must be done in both the main process (`locale-defaults.ts`) and renderer (`OnboardingFlow.tsx`).
**i18n (Internationalization)**:
- Uses `i18next` + `react-i18next` with bundled JSON translations (no lazy loading).
- Config in `src/renderer/i18n.ts`. 5 languages: EN, IT, ES, FR, DE. `SUPPORTED_LANGUAGES` array exported for UI selectors.
- Translation files: `src/renderer/locales/{en,it,es,fr,de}/translation.json`. Namespaces: `nav`, `auth`, `tasks`, `settings`, `common`, `errors`, `home`, `timeline`, `projects`, `agents`.
- **`common.*` namespace** holds shared labels (`save`, `cancel`, `delete`, `edit`, `add`, `rename`, `saving`, `deleting`, `creating`, `renameDescription`, `deleteTitle`). Before adding a new key, check if `common.*` already has it.
- Pluralization uses i18next `_one`/`_other` suffixes (e.g. `tasksDueToday_one`, `tasksDueToday_other`).
- `LanguageSync` component in `src/renderer/index.tsx` reads persisted `uiLanguage` from electron-store via tRPC on startup and syncs to i18next.
- Language selector lives in `GeneralSection.tsx` (Settings > General). On change it: (1) calls `i18n.changeLanguage()`, (2) persists to electron-store via `setUiLanguage` mutation, (3) writes to backend core memory so AI responds in the same language.
- `getUiLanguage()` exported from `src/main/store.ts` — used by `orchestrator.ts` to append language hint to daily brief prompt.
- Static data arrays that need translation use `labelKey` pattern (not `label`): store a translation key, call `t(labelKey)` at render time. Used in `NAV_ITEMS`, `COLUMNS`, `SECTIONS`, `SUGGESTION_CHIPS`.
- When adding new translated text: add the key to **all 5** JSON files. Keep `common.*` keys consistent across all languages.
**Google OAuth (adiuvAI side)**:
- `adiuvai://` is NOT accepted by Google as a redirect URI — Google only accepts `http://localhost` or `https://`. The API backend exposes `GET /auth/oauth/google/web-callback` which receives the Google redirect and immediately bounces to `adiuvai://oauth/callback?...`. The redirect URI registered in Google Cloud Console points to the backend, not the Electron app.
- `app.requestSingleInstanceLock()` is required for the `second-instance` event to fire on Windows/Linux. If it returns `false`, call `app.quit()` immediately (another instance is already running).
- In dev (`process.defaultApp === true`), `setAsDefaultProtocolClient('adiuvai')` must include `[path.resolve(process.argv[1])]` as the third argument so the OS protocol registration includes the entry script.
- `loginWithOAuth` uses `fetch()` directly (not `this.get()`) because the authorize endpoint is public — `get()` throws when not authenticated.
- The backup key in `backup-key.ts` is stored in `encryptedTokens` under the key `backup_key`, reusing `getToken/setToken` from `token.ts`. It is device-bound and never password-derived, so social-login users can use backup features without issue.
---
## api (FastAPI Backend)
### Commands
```bash
cd api
# Development
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
# Production
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120
# Database migrations
alembic upgrade head
# Testing
pytest # all tests
pytest -v # verbose
pytest tests/test_agents.py # single file
pytest tests/test_agents.py -k test_name # single test
# Linting/formatting
ruff check .
ruff format .
# Docker (full stack: app + postgres + minio + qdrant)
docker compose up --build
```
### Architecture
```
FastAPI app (app/main.py)
├── Middleware: TierRateLimiter → Sanitizer → CORS
├── HTTP Routes (app/api/routes/)
│ ├── auth.py — register, login, token refresh (bcrypt + HS256 JWT)
│ ├── chat.py — POST /chat, WS /chat/stream
│ ├── plans.py — execution plan playbooks
│ ├── storage.py — E2E-encrypted cloud storage (S3)
│ ├── backup.py — encrypted backup upload/download
│ ├── vectors.py — encrypted vector upsert/search (Pinecone/Qdrant)
│ ├── plugins.py — plugin marketplace (Power+ tier)
│ └── billing.py — Stripe subscriptions
├── Agent System (app/agents/)
│ ├── task_agent.py — 8 tools
│ ├── project_agent.py — 6 tools
│ ├── checkpoint_agent.py — 4 tools
│ └── note_agent.py — 5 tools
├── Orchestration (app/core/)
│ ├── orchestrator.py — intent classification + agent routing
│ ├── agent_registry.py — decorator-based agent registry
│ ├── execution_plan.py — server-side prompt templates + plan builder
│ ├── llm.py — LiteLLM factory (100+ providers)
│ └── memory_middleware.py
├── Billing (app/billing/)
│ ├── tier_manager.py — feature matrix (Free/Pro/Power/Team)
│ └── stripe_service.py — Stripe checkout + webhooks
├── Storage (app/storage/) — S3 blob store, vector store, encryption
└── Marketplace (app/marketplace/) — plugin catalog, review, revenue sharing
```
**LLM routing**: `gpt-4o-mini` classifies intent → routes to domain agent → agent uses `gpt-4o` with tools → tool calls describe client-side operations (JSON) → Electron executes locally and returns results.
**Zero-trust data model**: The backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, and storage record pointers. All user data is E2E-encrypted before leaving the Electron client.
**Key config**: `app/config/settings.py` — all env vars via Pydantic Settings. Copy `.env.example` to `.env` for local dev. Stripe and S3 gracefully stub when keys aren't configured.
**Database**: PostgreSQL with async SQLAlchemy 2.0 + asyncpg. 9 ORM models in `app/models.py`. Alembic migrations in `alembic/versions/`.
**Testing**: pytest + pytest-asyncio. Fixtures in `tests/conftest.py` create in-memory SQLite + moto-mocked S3. Test users seeded per tier (free/pro/power/team).
### Non-obvious details
- **Tier from DB, not JWT**: `get_current_user` decodes JWT but fetches authoritative tier from `subscriptions` table — tier changes take effect immediately without re-login
- **Refresh tokens hashed**: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve the plaintext (intentional)
- **WebSocket auth via query param**: `?token=<jwt>` instead of Bearer header (WebSocket handshake limitation)
- **Prompt IP protection**: `PromptTemplateRegistry` keeps prompts server-side; clients receive opaque `template_id`. `SanitizerMiddleware` strips leaked fragments from responses
- **Agents don't execute operations**: Tools return JSON describing client-side ops — the Electron client executes against local SQLite
- **Alembic async/sync split**: App uses `postgresql+asyncpg`, Alembic CLI needs `postgresql+psycopg2``env.py` handles the URL conversion
- **Tool loop cap**: Agent `_tool_loop` stops after 5 iterations to prevent infinite loops
- **Route order matters**: `/backup/history` must be declared before `/backup/{backup_id}` to avoid path param shadowing
- **CORS includes `app://`**: Electron uses custom `app://` protocol, not http/https
- **Vector search on encrypted data is not semantic**: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — a known trade-off
**Onboarding (API side)**:
- `PUT /auth/me/memory` — updates core memory k/v pairs and optionally marks onboarding complete (`mark_onboarded: true` sets `users.onboarding_completed_at`).
- `POST /auth/me/onboarding/reset` — nullifies `onboarding_completed_at` so the wizard re-runs.
- `POST /auth/onboarding/normalize` — LLM-normalizes free-text onboarding inputs via `gpt-4o-mini`; returns inputs unchanged on error.
- `get_current_user()` in `auth.py` middleware now decrypts core memory blocks and includes them in `UserProfile.memory` dict.
- `users.onboarding_completed_at` is a nullable TIMESTAMPTZ column — returned as epoch ms (int) in UserProfile schema.
**i18n (API side)**:
- `_language_instruction()` in `app/core/deep_agent.py` reads the user's `language` from `MemoryCore` and appends a system prompt directive ("Always respond in {language}") to all 4 `run_*` functions.
- The Electron client writes the user's chosen language to backend core memory on language change, so the API picks it up on the next agent call.
**Google OAuth (api side)**:
- OAuth routes live in `app/api/routes/auth.py`: `GET /auth/oauth/{provider}/authorize`, `POST /auth/oauth/{provider}/callback`, `GET /auth/oauth/{provider}/web-callback` (bounces to deep link, excluded from OpenAPI schema).
- Provider abstraction in `app/auth/oauth_providers.py``GoogleOAuthProvider` uses `httpx` directly (no `authlib`). PKCE S256 is implemented manually via `generate_pkce_pair()`.
- `_pending_states` dict in `routes/auth.py` is **in-memory** — works for single-process dev but does not survive restarts and does not scale to multiple workers. Replace with Redis in production.
- `users.password_hash` is **nullable** — social-only users have `password_hash=None`. `await db.flush()` is required before creating a linked `OAuthAccount` to populate `new_user.id` before commit.
- `OAUTH_REDIRECT_URI` must point to the **API backend** (e.g. `https://api.adiuvai.com/...`), not the website domain. `adiuvai.com` is a static site with no server-side routing.
- **Unverified email + existing account = 409**: if `email_verified=False` and the email is already registered, the callback returns 409. Without this guard, branch 3 would attempt to INSERT a duplicate email and crash with a DB constraint violation (500).
- **Testing OAuth routes**: mock `GoogleOAuthProvider.exchange_code` and `get_userinfo` with `patch.object(..., new=AsyncMock(...))` — works because FastAPI instantiates a new provider per request. Use `monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...)` to simulate configured credentials without restarting the app.
### Tier System
| Feature | Free | Pro | Power | Team |
|---------|------|-----|-------|------|
| Rate limit | 20/min | 60/min | 120/min | 200/min |
| Agents | 3 | unlimited | unlimited | unlimited |
| Cloud storage | 0 GB | 5 GB | 25 GB | unlimited |
| Plugin marketplace | no | no | yes | yes |
Enforced in `app/api/middleware/rate_limit.py` (sliding window) and `app/billing/tier_manager.py` (feature checks + quota enforcement).
---
## Cross-Project Integration
The Electron app and FastAPI backend communicate via **WebSocket** (`/chat/stream`):
1. Electron connects with `?token=<jwt>` query param
2. Client sends `ChatRequest` JSON frame
3. Server streams text chunks, then a final frame: `{"done": true, "response": "...", "actions": []}`
4. Server sends `tool_call` frames → Electron executes against local SQLite → returns `tool_result`
5. Server pings every 30 seconds to keep connection alive
The Electron app also has a **fully local AI path** (LangGraph orchestrator in main process) that doesn't require the backend — this is the primary path for desktop use.
---
## MCP Servers
- **Langfuse Docs** (`https://langfuse.com/api/mcp`) — configured at workspace level for prompt management documentation
- **shadcn** (`npx shadcn@latest mcp`) — configured in `adiuvAI/` for UI component generation

View File

@@ -1,14 +1,21 @@
{
"permissions": {
"allow": [
"Bash(git add AI_REFACTOR_PLAN.md)",
"Bash(git commit:*)",
"Read(//home/rmusso/adiuvai-api/**)",
"mcp__shadcn__get_item_examples_from_registries",
"mcp__shadcn__view_items_in_registries",
"Bash(npm run lint)",
"Bash(npx eslint --ext .ts,.tsx src/renderer/components/ai/blocks/)",
"WebFetch(domain:ui.shadcn.com)"
"allow": []
},
"enabledPlugins": {
"caveman@caveman": true
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac"
}
]
}
]
}
}
}

16
.gitignore vendored Normal file
View File

@@ -0,0 +1,16 @@
skills/
unused_skills/
.vscode/mcp.json
.claude/skills/brand-guidelines/*
.claude/skills/frontend-design/*
.claude/skills/remotion-best-practices/*
.mcp.json
docs/node_modules
docs/package.json
docs/package-lock.json
tmp/
.superpowers/
graphify-out/cache/
graphify-out/manifest.json
graphify-out/cost.json
.claude/settings.local.json

9
CLAUDE.md Normal file
View File

@@ -0,0 +1,9 @@
## graphify
This project has a graphify knowledge graph at graphify-out/.
Rules:
- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
- For cross-module "how does X relate to Y" questions, prefer `graphify query "<question>"`, `graphify path "<A>" "<B>"`, or `graphify explain "<concept>"` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)

862
docs/REFACTOR_PLAN.md Normal file
View File

@@ -0,0 +1,862 @@
# Refactoring & Security Plan — adiuvAI Workspace
**Generated:** 2026-06-12 · **Tree state:** `main` @ `315c5d0` (clean). Line numbers reference this state — execute file-split refactors (QUAL-*) **last**, they invalidate line references.
**Scope:** `adiuvAI/` (Electron), `api/` (FastAPI), `waitlist/`, `website/`.
**Method:** 8 parallel read-only audit agents (security ×2, dead-code/deps ×2, perf/correctness ×2, quality/types, waitlist/website), tool-assisted (`knip`, `npm audit`, `ruff`, `tsc --noEmit`, import-graph). Every finding verified against actual code.
**Legend:** `⚠️ REVIEW` = needs human or strong-model validation before execution. Do NOT hand these to a low-capability executor unattended.
---
## Summary Table
| ID | Category | Severity | File | Description |
|----|----------|----------|------|-------------|
| SEC-01 | Security | **Critical** | adiuvAI/src/main/api/drizzle-executor.ts | Backend can read ANY file on user's disk via generic FS tool handlers |
| SEC-02 | Security | High | api/app/api/middleware/rate_limit.py | Zero rate limiting on login/register/refresh (unauthenticated bypass) |
| SEC-03 | Security | High | api/app/models.py | Per-user Fernet key stored plaintext beside the ciphertext it protects |
| SEC-04 | Security | High | api/app/core/deep_agent.py +3 | Raw user content/PII sent to Langfuse (GDPR / L.132/2025) |
| SEC-05 | Security | High | adiuvAI/src/main/api/drizzle-executor.ts | Mass assignment: backend sets arbitrary columns on insert/update |
| SEC-06 | Security | High | adiuvAI/src/main/index.ts | No will-navigate / setWindowOpenHandler guards on main window |
| SEC-07 | Security | High | adiuvAI (app-wide) | No Content-Security-Policy |
| SEC-08 | Security | High | waitlist/app/rate_limit.py | Rate limit keyed on spoofable CF-Connecting-IP / X-Forwarded-For |
| SEC-09 | Security | High | waitlist/app/config.py | CONFIRM_SECRET defaults to per-process random → broken across workers |
| SEC-10 | Security | Medium | api/app/api/routes/auth.py | Login 500-crashes for social-only accounts (account-type oracle) |
| SEC-11 | Security | Medium | api/app/api/routes/auth.py | No password strength validation on register |
| SEC-12 | Security | Medium | api/app/api/routes/auth.py | No logout endpoint / refresh-token revocation |
| SEC-13 | Security | Medium | api + adiuvAI (WS handshake) | JWT in WebSocket URL query string (both sides) |
| SEC-14 | Security | Medium | api/app/config/settings.py | Insecure defaults (JWT_SECRET) with no prod startup guard |
| SEC-15 | Security | Medium | api (device_ws, scout_runner, deep_agent) | User message content in INFO logs (GDPR) |
| SEC-16 | Security | Medium | api/app/models.py | Relational-memory entity labels stored plaintext (third-party PII) |
| SEC-17 | Security | Medium | api/app/api/middleware/rate_limit.py | In-memory limiter state → ×4 bypass under gunicorn -w 4 |
| SEC-18 | Security | Medium | api (auth.py, scouts.py) | In-memory OAuth state stores break multi-worker; strand Gmail tokens |
| SEC-19 | Security | Medium | api/app/api/routes/scout_webhooks.py | Gmail Pub/Sub webhook fail-open when audience unset |
| SEC-20 | Security | Medium | adiuvAI/src/main/ipc.ts | IPC bridge doesn't validate sender; all procedures public |
| SEC-21 | Security | Medium | adiuvAI/src/main/ai/token.ts | Plaintext token + backup-key fallback in electron-store |
| SEC-22 | Security | Medium | adiuvAI/src/renderer/lib/httpLink.ts | Web SPA keeps JWT in localStorage |
| SEC-23 | Security | Medium | waitlist/app/security.py | Origin validation bypass via startswith |
| SEC-24 | Security | Medium | waitlist/app/routes.py | GDPR erasure + confirm triggered by bare GET (mail scanners) |
| SEC-25 | Security | Medium | waitlist/app/routes.py | Unauthenticated email re-send = mail-bombing vector |
| SEC-26 | Security | Medium | website/index.html | Third-party scripts without SRI; lucide@latest from unpkg |
| SEC-27 | Security | Medium | api/app/api/routes/device_ws.py | _index_sessions: no ownership check + never purged on disconnect |
| SEC-28 | Security | Medium | adiuvAI (db, attachments) | Local SQLite + attachments unencrypted at rest (product decision) |
| SEC-29 | Security | Low | api/app/api/routes/auth.py | Email enumeration via register 409 |
| SEC-30 | Security | Low | api/app/api/routes/device_ws.py | Internal exception strings returned to WS clients |
| SEC-31 | Security | Low | adiuvAI/src/main/router/index.ts | attachments.create accepts arbitrary sourcePath |
| SEC-32 | Security | Low | adiuvAI/src/main/index.ts | Scout Gmail deep link forwarded without local state check |
| SEC-33 | Security | Low | waitlist/app/routes.py | Partial email fragments in logs |
| SEC-34 | Security | Low | website (all pages) | No CSP / security headers on static site |
| SEC-35 | Security | Low | website/i18n.js | innerHTML i18n sink (latent XSS) |
| SEC-36 | Security | Low | adiuvAI/src/main/index.ts | sandbox not explicitly enabled in webPreferences |
| CORR-01 | Correctness | **Critical** | api/app/api/routes/device_ws.py | Chat tool calls: no timeout + send-before-register race → agent hangs forever |
| CORR-02 | Correctness | High | api/app/core/device_manager.py | WS unregister clobbers a freshly reconnected connection |
| CORR-03 | Correctness | High | api/app/core/memory_maintenance.py | Free-tier extraction drains with empty content — feature is a paid no-op |
| CORR-04 | Correctness | High | adiuvAI (renderer chat contexts) | Backend errors returned as success payloads ignored → chat wedged in "streaming" + listener leaks |
| CORR-05 | Correctness | High | adiuvAI/src/main/router/index.ts + drizzle-executor.ts | Cascade-delete gaps, zero transactions; AI-issued deletes orphan rows + disk files |
| CORR-06 | Correctness | High | adiuvAI (backend-client.ts + files/indexer.ts) | WS drop mid-index permanently wedges project in 'scanning' |
| CORR-07 | Correctness | Medium | adiuvAI/src/main/api/backend-client.ts | WS close handler clobbers newly opened connection (logout→login race) |
| CORR-08 | Correctness | Medium | api/app/core/memory_maintenance.py | Confidence decay applied per invocation, not per period |
| CORR-09 | Correctness | Medium | api (models + memory_middleware) | Check-then-insert races: memory_core / memory_relations lack unique constraints |
| CORR-10 | Correctness | Medium | api/app/api/routes/device_ws.py | Fire-and-forget create_task: no refs, no per-user concurrency cap |
| CORR-11 | Correctness | Medium | adiuvAI/src/main/router/index.ts | tasks.update unconditionally destroys briefing + chat history |
| CORR-12 | Correctness | Medium | adiuvAI/src/main/ai/orchestrator.ts | Stream errors emitted as plain stream_end → blank persisted messages |
| CORR-13 | Correctness | Medium | adiuvAI/src/main/api/backend-client.ts | withRetry retries non-idempotent POSTs (duplicate scout runs) |
| CORR-14 | Correctness | Medium | api/app/billing/stripe_service.py | Sync Stripe SDK calls block the event loop |
| CORR-15 | Correctness | Medium | api/app/api/routes/device_ws.py | run_home_stream swallows exceptions; client never gets error stream_end |
| CORR-16 | Correctness | Medium | api/app/api/routes/auth.py | delete_account: Stripe commit then DB deletes, broad except pass |
| CORR-17 | Correctness | Medium | adiuvAI/src/main/files + projectFolders.ts | Index-session TOCTOU + stale _active entry |
| CORR-18 | Correctness | Low | api/app/api/routes/scouts.py | Scout trigger TOCTOU → duplicate concurrent runs |
| CORR-19 | Correctness | Low | adiuvAI (router, drizzle-executor, auth-manager) | Swallowed-error triage: silent [] returns, bare catches, unguarded JSON.parse |
| CORR-20 | Correctness | Low | api/app/api/routes/auth.py | Naive/aware datetime kludge on refresh expiry |
| CORR-21 | Correctness | Low | adiuvAI (misc) | LanguageSync inside App body; fake AbortSignal; startsWith containment |
| PERF-01 | Performance | High | api/app/api/routes/auth.py | bcrypt runs on the event loop (rate-limit-exempt login path) |
| PERF-02 | Performance | High | api/app/core/deep_agent.py | Home channel buffers ALL tokens — streaming endpoint doesn't stream |
| PERF-03 | Performance | High | api/app/core/memory_maintenance.py | Hourly mining duplicates proactive rows; _load_proactive has no LIMIT |
| PERF-04 | Performance | Medium | adiuvAI/src/main/db/schema.ts | No SQLite indexes on any user-data table (sync full scans block IPC) |
| PERF-05 | Performance | Medium | api/app/models.py | Missing PG composite/pgvector indexes; refresh_tokens never purged |
| PERF-06 | Performance | Medium | adiuvAI (orchestrator + ChatSurface) | Per-token IPC message + full markdown re-parse per chunk |
| PERF-07 | Performance | Medium | api/app/core/deep_agent.py | Tool calls executed serially per LLM step (each a WS round-trip) |
| PERF-08 | Performance | Medium | api (deep_agent, scout_runner, langfuse_client) | Blocking lf.flush() per request; sync get_prompt on cache miss |
| PERF-09 | Performance | Medium | api/app/api/middleware/auth.py | get_current_user: 3 queries + full Fernet decrypt on EVERY request |
| PERF-10 | Performance | Medium | api/app/core/memory_middleware.py | Same User row re-fetched up to 4×/message; update_core commits per key |
| PERF-11 | Performance | Medium | adiuvAI/src/main/ai/orchestrator.ts | Health-check HTTP round-trip before every chat message |
| PERF-12 | Performance | Medium | adiuvAI/src/main/router/index.ts | Paid LLM brief regeneration fired by every week-relevant mutation |
| PERF-13 | Performance | Medium | adiuvAI (vite configs, ChatChartBlock, notes route) | No route code-splitting; recharts + Milkdown eager in initial bundle |
| PERF-14 | Performance | Medium | adiuvAI/src/renderer/index.tsx | Default QueryClient → refetch storms into sync SQLite on window focus |
| PERF-15 | Performance | Medium | adiuvAI (projectFolders.ts + indexer.ts) | Double full-folder walk; no WS backpressure (memory spike) |
| PERF-16 | Performance | Medium | api (scouts/engine.py, device_ws.py) | DB sessions held open across LLM calls (pool starvation) |
| PERF-17 | Performance | Medium | api/app/core/scout_runner.py | Per-file get_file_metadata WS round-trip during scans |
| PERF-18 | Performance | Low | api (embeddings.py, llm.py) | Fresh AsyncOpenAI client per call, never closed |
| PERF-19 | Performance | Low | adiuvAI/src/renderer/components/tasks/TaskListView.tsx | tasks.list returns all rows; pagination client-side |
| PERF-20 | Performance | Low | api/app/api/middleware/rate_limit.py | Limiter dict never sheds keys; tier read from JWT not DB |
| PERF-21 | Performance | Low | adiuvAI/src/main/api/drizzle-executor.ts | Unbounded page-details tables; uncapped pdf/docx base64 reads |
| PERF-22 | Performance | Low | adiuvAI/src/renderer/context | HeaderContext unmemoized value; ContextualChat re-renders route per token |
| DEAD-01 | Dead code | High | api/requirements.txt | 5 dead deps: pinecone, qdrant-client, boto3, moto[s3], google-auth-oauthlib |
| DEAD-02 | Dead code | High | api/app/core/scout_registry.py | Fully orphaned module (BaseAgent, zero importers) |
| DEAD-03 | Dead code | Medium | api/app/api/middleware/rate_limit.py | slowapi Limiter exported, zero decorated routes — dep removable |
| DEAD-04 | Dead code | Medium | api/app/api/routes/chat.py | HTTP /chat routes unused by Electron client (WS is the channel) |
| DEAD-05 | Dead code | Medium | adiuvAI/package.json | 4 unused deps: next-themes, mammoth, pdf-parse, @hello-pangea/dnd |
| DEAD-06 | Dead code | Medium | adiuvAI/src (5 files) | Dead modules: batch-types, useChatStream, useTaskBriefCache, ScoutRunLog, blocks barrel |
| DEAD-07 | Dead code | Medium | adiuvAI/src/main/auth/backup-key.ts | Documented architecture but zero importers — wire or remove |
| DEAD-08 | Dead code | Medium | adiuvAI/knip.json | Missing web-SPA entries → knip false-flags live files |
| DEAD-09 | Dead code | Medium | api/requirements.txt | langchain meta-package → langchain-core; redundant websockets pin |
| DEAD-10 | Dead code | Low | api (13 files) | 13 ruff F401 unused imports (auto-fixable) |
| DEAD-11 | Dead code | Low | adiuvAI/src (7 exports) | Dead utility exports (getRawSqlite, parseDateRange, formatTime, …) |
| DEAD-12 | Dead code | Low | api/requirements.txt + backend-client.ts | Dev deps in runtime requirements; stale endpoint doc comments |
| DEPS-01 | Dependencies | High | adiuvAI/package.json | ws 8.19.0 vulnerable (GHSA-58qx-3vcg-4xpx) — only runtime-reachable vuln |
| DEPS-02 | Dependencies | High | api/requirements.txt | python-jose floor 3.3.0 permits CVE-2024-33663/33664 versions |
| DEPS-03 | Dependencies | Medium | api/requirements.txt | cryptography floor 42.0.0 permits CVE-2024-26130 versions |
| DEPS-04 | Dependencies | Medium | api/ | No lock file — `>=` floors make installs unauditable |
| DEPS-05 | Dependencies | Medium | adiuvAI/package.json | eslint 8 + @typescript-eslint 5 both EOL |
| DEPS-06 | Dependencies | Medium | adiuvAI/package.json | Electron 40 → 42 (two majors of Chromium security patches behind) |
| DEPS-07 | Dependencies | Low | adiuvAI (transitive) | tmp/tar/esbuild advisories in forge/drizzle-kit toolchain — no fix, monitor |
| DEPS-08 | Dependencies | Low | adiuvAI/package.json | @types/ws in dependencies instead of devDependencies |
| QUAL-01 | Quality | High | adiuvAI/src/main/router/index.ts | God file: 1967 LOC, 15 sub-routers — split per domain |
| QUAL-02 | Quality | High | api/app/core/deep_agent.py | _run_single_agent vs _run_single_agent_stream ~90% duplicated |
| QUAL-03 | Quality | High | api (4 files) | Four hand-rolled LLM tool loops — extract one |
| QUAL-04 | Quality | High | adiuvAI/src/main/router/index.ts | CRUD repetition ×8 tables — extract factory |
| QUAL-05 | Quality | High | api/app/core/deep_agent.py | God file: 1329 LOC — split prompt-context / tools / runner |
| QUAL-06 | Quality | Medium | adiuvAI/src/main/api/backend-client.ts | God file 1191 LOC; 230-line openDeviceWebSocket; send* boilerplate ×6 |
| QUAL-07 | Quality | Medium | api/app/core/scout_runner.py | 1051 LOC; run_local_agent/run_cloud_agent 210-line twins |
| QUAL-08 | Quality | Medium | api/app/api/routes/device_ws.py | Protocol + business logic mixed — extract ws_handlers package |
| QUAL-09 | Quality | Medium | adiuvAI/src/renderer/components/projects/ProjectSidebar.tsx | 1292 LOC, 25+ useState — extract dialogs |
| QUAL-10 | Quality | Medium | api/app/api/routes/scouts.py | Ownership-check + 404 repeated 6× — FastAPI dependency |
| QUAL-11 | Quality | Medium | adiuvAI/src/main/router/index.ts | Three competing error-return styles, zero TRPCError |
| QUAL-12 | Quality | Medium | api/app/api/routes/device_ws.py | Raw-dict frames bypass pydantic schemas (also kills 7 type:ignores) |
| QUAL-13 | Quality | Medium | api + adiuvAI (WS boundary) | camelCase/snake_case contract untrusted (dual-read hedges) |
| QUAL-14 | Quality | Medium | api (auth.py, scouts.py) | OAuth route blocks → dedicated files; shared TTLStateStore |
| QUAL-15 | Quality | Low | api/app/agents | Copy-pasted _is_uuid + row formatting across agents |
| TYPE-01 | Type safety | High | adiuvAI/src/renderer/components/ai/blocks (3 files) | Broken relative imports — block types silently `any` |
| TYPE-02 | Type safety | High | api/ | No Python type checker configured at all |
| TYPE-03 | Type safety | Medium | adiuvAI/ | tsc --noEmit does not pass; no CI typecheck |
| TYPE-04 | Type safety | Medium | api/app/core/deep_agent.py | context: dict[str, Any] threaded everywhere — RequestContext model |
| TYPE-05 | Type safety | Medium | adiuvAI/src/main/api/drizzle-executor.ts | Cast cluster: table-as-Record ×5, bogus Boolean predicate casts |
| TYPE-06 | Type safety | Low | adiuvAI/src/renderer | (window as any).electronAI ×4 — global declaration file |
| TYPE-07 | Type safety | Low | api + adiuvAI | Misc: ToolResult TypedDict, 11 missing py annotations, vite/client types |
---
## Execution Order
Line numbers reference the current tree — **do mechanical fixes before file splits**.
**Phase 0 — Mechanical quick wins (safe for low-capability executor, no review needed):**
TYPE-01 → DEPS-01 → DEAD-10 → DEAD-01 → DEAD-02 → DEAD-05 → DEAD-08 → DEPS-08 → DEAD-11 → DEAD-12 → PERF-14 → PERF-18 → CORR-20
**Phase 1 — Critical & High security (ALL ⚠️ REVIEW, human validates each diff):**
SEC-01 → SEC-05 → SEC-06 → SEC-07 → SEC-20 (these four+one harden the Electron trust boundary together) → SEC-02 → SEC-14 → SEC-19 (share a startup-guard mechanism: do SEC-14 first) → SEC-09 → SEC-08 → SEC-04 → SEC-15 → SEC-03 (needs migration design)
**Phase 2 — Critical & High correctness:**
CORR-01 → CORR-02 (same files, same review session) → CORR-04 → CORR-12 (CORR-04 depends on CORR-12's error-frame contract — implement together) → CORR-06 → CORR-05 ⚠️ → CORR-03
**Phase 3 — Remaining Medium security + correctness:**
SEC-10 → SEC-11 → SEC-12 → SEC-13 (coordinated api+electron change) → SEC-17/SEC-18 (one Redis introduction covers both) → SEC-27 → SEC-21 → remaining SEC Mediums → CORR-07..CORR-17
**Phase 4 — Performance:**
PERF-01 → PERF-02 ⚠️ → PERF-03 → PERF-04 → PERF-05 (migrations) → PERF-09/PERF-10 (same subsystem) → PERF-06 → rest
**Phase 5 — Dependencies & tooling:**
DEPS-02 → DEPS-03 → DEPS-04 → TYPE-02 → TYPE-03 → DEPS-05 ⚠️ → DEPS-06 ⚠️
**Phase 6 — Structural refactors (after all above; invalidates line numbers):**
QUAL-02 ⚠️ → QUAL-03 ⚠️ → QUAL-01 → QUAL-04 ⚠️ → QUAL-05 → QUAL-06..QUAL-15 → TYPE-04..TYPE-07 → DEAD-04 ⚠️ / DEAD-07 ⚠️ (owner decisions)
---
# 1. Security
> Per instruction: **every SEC item is ⚠️ REVIEW** unless explicitly noted "mechanical". A botched auth check or sanitizer is worse than the original bug.
### SEC-01 — Backend can read ANY file on the user's disk ⚠️ REVIEW
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts``handleListDirectory` L375-398, `handleReadFileContent` L400-436, `handleGetFileMetadata` L438-461
- **Severity:** Critical · OWASP A01:2021 Broken Access Control
- **Problem:** These handlers take a backend-supplied `path`, resolve it (`fs.promises.realpath(path.resolve(dirPath))`) and read it with **no allowlist or root containment**. `read_file_content` returns up to 500 KB of any file (`~/.ssh/id_rsa`, browser cookie DBs). The backend is semi-trusted by design; if compromised, blast radius = the entire user filesystem. The backend adds no guard either (`api/app/agents/filesystem_agent.py:24-33` passes absolute paths straight through). Contrast: `handleReadProjectFolderFile` (L511-529) does containment correctly.
- **Fix:** After realpath, assert the resolved path is inside an allowed root: the set of configured `projects.folderPath` values (query the projects table) plus nothing else. Use `const rel = path.relative(root, resolved); if (rel.startsWith('..') || path.isAbsolute(rel)) throw new ExecutorError('Access denied')`. Apply identically in all three handlers. Also fix the existing `startsWith` containment in `handleReadProjectFolderFile` L527 to use the same `path.relative` check (defeats `C:\proj\foo-evil` vs `C:\proj\foo`).
- **Risk:** Local filesystem scouts or agent flows that legitimately read outside project folders will break — verify which directories local scout configs reference and add those roots to the allowlist before locking down. Test: agent file-read inside project folder still works; read of `C:\Windows\win.ini` rejected.
### SEC-02 — No rate limiting on login/register/refresh ⚠️ REVIEW
- **File:** `api/app/api/middleware/rate_limit.py:41-48` (`_EXEMPT_PATHS`), `:88-91` (no-token pass-through)
- **Severity:** High · OWASP A07:2021 Identification & Authentication Failures
- **Problem:** Limiter only acts on requests with a valid Bearer JWT; `if not token: return await call_next(request)`. Login/register/refresh are unauthenticated → **never rate-limited**. Unlimited password guessing and refresh-token guessing.
- **Fix:** Remove `/api/v1/auth/login` and `/api/v1/auth/register` from `_EXEMPT_PATHS`. In `dispatch()`, when no token is present and `request.url.path.startswith("/api/v1/auth/")`, apply an IP-keyed sliding window (reuse the existing `_window` mechanism keyed on client IP, fixed limit e.g. 5/min for login/register/refresh, 429 on excess). Note this remains per-process until SEC-17 lands Redis.
- **Risk:** Shared-NAT users may hit limits; size window accordingly. Test: 6th login attempt in a minute → 429; authenticated traffic unaffected.
### SEC-03 — Fernet key stored in plaintext beside its ciphertext ⚠️ REVIEW
- **File:** `api/app/models.py:83` (`User.encryption_key`), read at `api/app/core/memory_middleware.py:559-566`
- **Severity:** High · OWASP A02:2021 Cryptographic Failures
- **Problem:** The per-user key that encrypts all memory tiers sits as a plaintext column in the same PostgreSQL DB. Any DB-level compromise or backup leak decrypts everything — the "encrypted at rest" zero-trust claim collapses.
- **Fix:** Introduce a KEK from env/KMS (model: the existing `OAUTH_ENCRYPTION_KEY` pattern in `api/app/integrations/__init__.py:90-102`). Store `Fernet(KEK).encrypt(user_key)` in the column; unwrap in `_get_fernet`. One-time Alembic data migration re-wrapping existing keys; keep a dual-read window (try unwrap, fall back to raw) during rollout, then remove fallback.
- **Risk:** Migration must be reversible and tested against a DB snapshot; losing the KEK = losing all memory data. Requires key-rotation runbook. This is design work — human required.
### SEC-04 — Raw user content & PII sent to Langfuse ⚠️ REVIEW
- **Files:** `api/app/core/deep_agent.py:912,926,933,975` · `api/app/scouts/engine.py:260-268` (email subject/sender/2000-char body) · `api/app/core/scout_runner.py:266-278`
- **Severity:** High · OWASP A09:2021 / GDPR Art. 5, 28, 44 / Italian Law 132/2025
- **Problem:** Full conversation history, tool I/O, and triaged **email content** are transmitted to Langfuse when configured. `hash_user_id()` only pseudonymizes the identifier; bodies go in clear. Requires Langfuse as contracted sub-processor; possible international transfer depending on `LANGFUSE_BASE_URL`.
- **Fix:** Add `LANGFUSE_CAPTURE_CONTENT: bool = False` to settings. Wrap every `input=`/`output=` content assignment in the three files: when flag is false, pass `{"redacted": True, "chars": len(...)}` instead. Keep metadata/token counts. Document Langfuse as sub-processor in the privacy policy regardless.
- **Risk:** Reduced debugging richness. Verify Langfuse prompt management (`get_prompt`) is unaffected — it's a separate path.
### SEC-05 — Mass assignment in drizzle-executor insert/update ⚠️ REVIEW
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts``handleInsert` L268-290, `handleUpdate` L292-327
- **Severity:** High · OWASP A08:2021
- **Problem:** Table name is allowlisted, columns are not: `{ id: crypto.randomUUID(), ...data, createdAt: now }` spreads backend keys verbatim — any real column writable. Also: `...data` is spread **after** `id`, so a backend-supplied `id` overrides the executor's generated one (contradicting the code's own comment).
- **Fix:** Define a per-table writable-column allowlist (object map table→string[]). Before building `values`/`withTimestamp`, pick only allowlisted keys; always strip `id` and `createdAt` from incoming `data`/`updates`. Reorder insert to `{ ...picked, id: crypto.randomUUID(), createdAt: now, updatedAt: now }`.
- **Risk:** Additive validation. Test every agent CRUD flow (create task/note/event, update, propose_note_edit) still works — the allowlist must cover all fields agents legitimately set.
### SEC-06 — No navigation / window-open guards ⚠️ REVIEW
- **File:** `adiuvAI/src/main/index.ts``createWindow` L85-117 (handlers absent repo-wide)
- **Severity:** High · OWASP A05:2021
- **Problem:** No `setWindowOpenHandler`, no `will-navigate` guard. Any navigation to remote content lands in the privileged renderer (which has `window.electronTRPC`/`electronAI`).
- **Fix:** In `createWindow` add:
```ts
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https://')) shell.openExternal(url);
return { action: 'deny' };
});
mainWindow.webContents.on('will-navigate', (e, url) => {
const ok = (MAIN_WINDOW_VITE_DEV_SERVER_URL && url.startsWith(MAIN_WINDOW_VITE_DEV_SERVER_URL)) || url.startsWith('file://');
if (!ok) e.preventDefault();
});
```
- **Risk:** Minimal — app uses internal TanStack routing. Test external links in chat still open in OS browser.
### SEC-07 — No Content-Security-Policy ⚠️ REVIEW
- **File:** `adiuvAI/index.html` (no meta CSP); `src/main/index.ts` (no onHeadersReceived)
- **Severity:** High · OWASP A05:2021
- **Problem:** Zero CSP → no defense-in-depth for any future XSS sink; `connect-src` unrestricted (compromised renderer exfiltrates anywhere).
- **Fix:** In production only (`!IS_DEV`), register `session.defaultSession.webRequest.onHeadersReceived` injecting: `default-src 'self'; script-src 'self'; connect-src 'self' https://<backend-host> wss://<backend-host>; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; object-src 'none'; frame-ancestors 'none'`. Backend host from existing config.
- **Risk:** Tailwind/shadcn need `style-src 'unsafe-inline'`; Vite dev needs exemption (gate on IS_DEV). Test packaged build fully: chat, charts, Milkdown editor, images.
### SEC-08 — Waitlist rate limit keyed on spoofable headers ⚠️ REVIEW
- **File:** `waitlist/app/rate_limit.py:19-32`
- **Severity:** High · OWASP A04:2021 / CWE-348
- **Problem:** `_get_client_ip` trusts `cf-connecting-ip` then `x-forwarded-for` unconditionally. Random header per request = fresh 5/min bucket → unlimited signup spam, Brevo quota burn, poisoned stored `ip_address`.
- **Fix:** Add `TRUSTED_PROXY_IPS: str = ""` setting. Trust `cf-connecting-ip` only when `request.client.host` is in that list; otherwise use `request.client.host`. Also firewall origin to Cloudflare ranges at infra level.
- **Risk:** Misconfigured trusted-proxy list rate-limits Cloudflare itself (all users share CF egress IPs) — must be deployed together with correct env value.
### SEC-09 — Waitlist CONFIRM_SECRET random default ⚠️ REVIEW
- **File:** `waitlist/app/config.py:19`; `waitlist/Dockerfile:21` (`-w 2`)
- **Severity:** High · OWASP A05:2021
- **Problem:** `CONFIRM_SECRET: str = secrets.token_hex(32)` — with 2 gunicorn workers and no env var, each worker has a different HMAC secret: ~50% of confirm/unsubscribe links fail silently; all tokens die on restart. Breaks double opt-in AND the GDPR erasure path.
- **Fix:** Remove default: `CONFIRM_SECRET: str` (required field — pydantic fails startup if unset). Update `.env.example` comment.
- **Risk:** Deploys without the env var now fail loudly at boot — that's the point. Coordinate with deploy.
### SEC-10 — Login 500 for social-only accounts ⚠️ REVIEW
- **File:** `api/app/api/routes/auth.py:169` (and `_verify_password` L73-74)
- **Severity:** Medium · OWASP A07:2021
- **Problem:** OAuth-only users have `password_hash=None`; `hashed.encode()` → uncaught `AttributeError` → 500. Differing responses (500 vs 401) = account-type oracle + crash.
- **Fix:** Replace the check with: `if user is None or user.password_hash is None or not _verify_password(body.password, user.password_hash): raise HTTPException(401, "Invalid credentials")`.
- **Risk:** None. Test: password login against a Google-only account → 401.
### SEC-11 — No password strength on register ⚠️ REVIEW (mechanical)
- **File:** `api/app/api/routes/auth.py:100-104`
- **Severity:** Medium · OWASP A07:2021
- **Problem:** `password: str` unconstrained (1-char/empty accepted); `_ChangePasswordRequest` already requires `min_length=8`. `email` is plain `str`, not `EmailStr`.
- **Fix:** Line 102: `password: str = Field(min_length=8)`; change `email: str` to `email: EmailStr` (import from pydantic). Mirror what change-password already does.
- **Risk:** None for new accounts. Existing weak-password accounts unaffected.
### SEC-12 — No logout / refresh-token revocation ⚠️ REVIEW
- **File:** `api/app/api/routes/auth.py` (endpoint absent; rotation only at L209)
- **Severity:** Medium · OWASP A07:2021
- **Problem:** No server-side logout; stolen refresh token valid 30 days; no reuse detection on rotated tokens.
- **Fix:** Add `POST /api/v1/auth/logout` (authenticated): body `{refresh_token: str}`, delete the matching `RefreshToken` row (SHA-256 the input, filter by hash + `user_id`). Optional `all: bool` to delete all the user's tokens. Wire Electron `auth-manager.ts` logout to call it before clearing local tokens.
- **Risk:** Electron + backend change together. Test logout then refresh → 401.
### SEC-13 — JWT in WebSocket URL query string ⚠️ REVIEW
- **Files:** `api/app/api/routes/device_ws.py:78` · `adiuvAI/src/main/api/backend-client.ts:895`
- **Severity:** Medium · OWASP A09:2021
- **Problem:** `?token=<jwt>` lands in reverse-proxy access logs, APM traces. Token grants full account access for 30 min.
- **Fix:** The Electron client uses the `ws` package which supports handshake headers: pass `{ headers: { Authorization: \`Bearer ${token}\` } }` as the second arg to `new WebSocket(url, opts)`. Backend: read `websocket.headers.get("authorization")` first, fall back to `?token=` for one release, then remove fallback. Interim mitigation: strip query strings for `/api/v1/ws/device` in proxy access logs.
- **Risk:** Coordinated two-repo change; old clients break when fallback removed. Web SPA (browser WebSocket) cannot set headers — if web ever uses the WS, it needs the ticket pattern instead.
### SEC-14 — Insecure config defaults, no prod guard ⚠️ REVIEW
- **File:** `api/app/config/settings.py:6-7`; `api/docker-compose.yml:22-23`
- **Severity:** Medium · OWASP A05:2021
- **Problem:** `JWT_SECRET = "change-me-in-production"` and `postgres:postgres` defaults; nothing refuses to start in prod with defaults → forgeable JWTs.
- **Fix:** Add a pydantic `model_validator` on `Settings`: if `ENV == "prod"` and (`JWT_SECRET == "change-me-in-production"` or `len(JWT_SECRET) < 32`), raise `ValueError` at import. Same guard hook used by SEC-19 (`GMAIL_PUBSUB_AUDIENCE` empty → fail in prod).
- **Risk:** Prod deploys with missing env now fail at boot (intended). Dev unaffected.
### SEC-15 — User message content in INFO logs ⚠️ REVIEW
- **Files:** `api/app/api/routes/device_ws.py:258-265,343` · `api/app/core/scout_runner.py:292-311` · `api/app/core/deep_agent.py:958-984` · `api/app/core/memory_middleware.py:528,554`
- **Severity:** Medium · OWASP A09:2021 / GDPR
- **Problem:** Chat messages (`message[:200]`), tool args/outputs (`[:800]`/`[:1200]`), memory queries logged at INFO — uncontrolled PII sink in centralized logs.
- **Fix:** Replace content interpolations with length+ID only (e.g. `logger.info("home_request user=%s chars=%d", user_id, len(message))`). Gate full content behind `logger.debug` + a `LOG_CONTENT=false` setting.
- **Risk:** Less debuggable ops. Each call site is a one-line change; review the full grep list before executing.
### SEC-16 — Relational-memory labels stored plaintext ⚠️ REVIEW
- **File:** `api/app/models.py:432-435`; predicates at `api/app/api/routes/memory.py:34-45`
- **Severity:** Medium · OWASP A02:2021 / GDPR
- **Problem:** `subject_label`/`object_label` hold names of user's contacts/employers (`works_at`, `reports_to`) in plaintext — third-party personal data. Only `notes_encrypted` protected.
- **Fix (decision):** Either encrypt labels with the user Fernet key (breaks label-match queries at `memory_middleware.py:421-428` — would need deterministic encryption or hashing for lookups) or formally document in the processing record and rely on SEC-03's KEK to mitigate DB-leak. **Recommend the latter** + SEC-03. Human decision required.
- **Risk:** Encrypting breaks `upsert_relation` matching — substantial rework. Don't hand to low-capability model.
### SEC-17 — In-memory rate limiter ×4 bypass ⚠️ REVIEW
- **File:** `api/app/api/middleware/rate_limit.py:67,81`
- **Severity:** Medium · OWASP A04:2021
- **Problem:** Per-process dicts under `gunicorn -w 4` → effective limit up to 4× tier limit, nondeterministic.
- **Fix:** Back the sliding window with Redis (`INCR` + `EXPIRE` or sorted-set window) keyed `rl:{user_id}:{minute}`. docker-compose already has a commented Redis service — enable it. Do together with SEC-18 (one Redis introduction).
- **Risk:** New infra dependency; need connection-failure fallback policy (fail-open vs fail-closed — recommend fail-open with warning log).
### SEC-18 — In-memory OAuth state stores ⚠️ REVIEW
- **Files:** `api/app/api/routes/auth.py:62` (`_pending_states`) · `api/app/api/routes/scouts.py:458,694-697` (`_pending_scout_oauth_states`, holds encrypted Gmail token between callback and finalize)
- **Severity:** Medium · OWASP A04:2021
- **Problem:** Multi-worker: authorize and callback can hit different workers → random 401s; scout flow strands the Gmail token; all state lost on redeploy.
- **Fix:** Move both to Redis with TTL (in-code comments already prescribe this). Implement one `TTLStateStore` (see QUAL-14) with `set(key, dict, ttl)` / `pop(key)`; Redis-backed in prod, dict-backed in tests.
- **Risk:** Prereq: Redis from SEC-17. Test full OAuth login + scout Gmail connect flows.
### SEC-19 — Gmail Pub/Sub webhook fail-open ⚠️ REVIEW
- **File:** `api/app/api/routes/scout_webhooks.py:48-52`; default at `api/app/config/settings.py:69`
- **Severity:** Medium · OWASP A05:2021/A07:2021
- **Problem:** Empty `GMAIL_PUBSUB_AUDIENCE` → `_verify_pubsub_jwt` returns True for ANY unauthenticated POST; attacker-supplied `emailAddress` triggers scout runs (LLM cost) for arbitrary users. Also unauthenticated → not rate-limited (SEC-02).
- **Fix:** Fail closed: `if not settings.GMAIL_PUBSUB_AUDIENCE: if settings.ENV == "prod": return False; logger.warning(...); return True`. Tie into SEC-14's startup guard.
- **Risk:** Prod deploys without the env var stop receiving Gmail push (intended — they were unverified anyway).
### SEC-20 — IPC bridge doesn't validate sender ⚠️ REVIEW
- **Files:** `adiuvAI/src/main/ipc.ts:44-87` · `adiuvAI/src/main/index.ts:122-131` (dialog + scope handlers)
- **Severity:** Medium · OWASP A01:2021
- **Problem:** `ipcMain.on('trpc', …)` dispatches without checking `event.senderFrame`; all router procedures are `publicProcedure`. Any frame that runs in the renderer gets full CRUD/auth/scout access. The cheap control that contains SEC-06/SEC-07 failures.
- **Fix:** At the top of each `ipcMain.on/handle` callback: validate `event.senderFrame.url` starts with the dev-server URL (dev) or the app's `file://` index (prod); silently drop otherwise. Extract a `isTrustedSender(event)` helper used by all four registration points (`trpc`, `ai:contextual-scope-update`, `dialog:showOpenDialog`, any others found at execution time).
- **Risk:** None today (no sub-frames). Test dev + packaged.
### SEC-21 — Plaintext token/backup-key fallback ⚠️ REVIEW
- **Files:** `adiuvAI/src/main/ai/token.ts:37-47` · `adiuvAI/src/main/auth/backup-key.ts:24-35`
- **Severity:** Medium · OWASP A02:2021
- **Problem:** When `safeStorage.isEncryptionAvailable()` is false (keyring-less Linux/WSL), JWTs and the AES backup key are written plaintext to electron-store JSON. `readFromStore` also silently returns raw string on decrypt failure (downgrade masking).
- **Fix:** (1) Tag values with `enc:`/`plain:` prefix on write; on read, never attempt plaintext interpretation of an `enc:` value. (2) When safeStorage unavailable: keep tokens in-memory only (re-login per launch) and emit a renderer-visible warning. Never persist the backup key plaintext — fail backup-key creation instead.
- **Risk:** Keyring-less Linux users lose session persistence. Migration: existing stored values have no prefix — treat unprefixed as legacy-plaintext, re-encrypt on first read.
### SEC-22 — Web SPA token in localStorage ⚠️ REVIEW
- **File:** `adiuvAI/src/renderer/lib/httpLink.ts:19`
- **Severity:** Medium (web build only) · OWASP A07:2021
- **Problem:** `localStorage.getItem('adiuvai-token')` — readable by any XSS.
- **Fix (decision):** httpOnly secure cookie set by backend (needs CSRF protection + CORS review) or in-memory + silent refresh. If deferring, at minimum pair with strict CSP on the web deployment. Product/infra decision — don't execute blindly.
### SEC-23 — Waitlist Origin startswith bypass ⚠️ REVIEW (mechanical)
- **File:** `waitlist/app/security.py:50-51`
- **Severity:** Medium · OWASP A01:2021 / CWE-346
- **Problem:** `origin.startswith(o)` → `https://adiuvai.com.evil.example` passes.
- **Fix:** `origin in allowed` exact match; for Referer, compare `urllib.parse.urlsplit(referer)._replace(path="", query="", fragment="")` scheme+host against allowed origins.
- **Risk:** None; existing tests cover the endpoint.
### SEC-24 — GDPR erasure + confirm on bare GET ⚠️ REVIEW
- **File:** `waitlist/app/routes.py:87` (confirm), `:125-154` (unsubscribe → `_anonymize_entry`)
- **Severity:** Medium · CWE-650
- **Problem:** Mail scanners (SafeLinks, Mimecast) follow GETs → can anonymize a subscriber or auto-confirm a bot-planted email before any human clicks.
- **Fix:** Both GET handlers render a minimal HTML page with `<form method="POST">` + the token in a hidden field; move the state change to new POST handlers (`POST /waitlist/confirm`, `POST /waitlist/unsubscribe`). Keep token validation identical.
- **Risk:** Email templates unchanged (links still GET the page). Update the 19-test suite: existing GET tests now assert the form page; add POST tests.
### SEC-25 — Unauthenticated re-send mail bombing ⚠️ REVIEW
- **File:** `waitlist/app/routes.py:52-62`
- **Severity:** Medium · OWASP API4:2023
- **Problem:** Re-POSTing a known email re-triggers `send_confirmation_email`. With SEC-08's spoof bypass: mail-bomb any subscribed address via your Brevo account.
- **Fix:** Add `last_email_sent_at` column (Alembic migration). In the existing-entry branch, skip the send if `last_email_sent_at` < 15 min ago (still return the generic success response — preserves anti-enumeration).
- **Risk:** Migration + test. Depends on SEC-08 for full mitigation.
### SEC-26 — Website scripts without SRI; lucide@latest ⚠️ REVIEW (mechanical)
- **File:** `website/index.html:44-48`
- **Severity:** Medium · OWASP A08:2021 / CWE-353
- **Problem:** GSAP + ScrollTrigger (cdnjs) and `lucide@latest` (unpkg) loaded with no `integrity`/`crossorigin`; `@latest` ships whatever unpkg serves — supply-chain risk on the page hosting the PII signup form.
- **Fix:** Pin lucide to an exact version; add `integrity="sha384-…" crossorigin="anonymous"` to all three script tags (compute hashes from the pinned files), or self-host under `website/assets/`.
- **Risk:** Hash must match exact file; test animations + icons after.
### SEC-27 — _index_sessions: no ownership check, never purged ⚠️ REVIEW
- **File:** `api/app/api/routes/device_ws.py:65` (registry), `:700` (cancel), `:732` (batch)
- **Severity:** Medium · OWASP A01:2021
- **Problem:** `_handle_index_file_batch` / `_handle_index_session_cancel` look up by `sessionId` without verifying `session["user_id"] == user_id` — a user who learns another's session UUID can cancel it or corrupt it. Sessions also leak forever on mid-index disconnect (no TTL, no cleanup in WS finally).
- **Fix:** (1) In both handlers, after lookup: `if session is None or session["user_id"] != user_id: send error frame, return`. (2) In the WS `finally` block, drop all `_index_sessions` entries whose `user_id == user_id` and `ws is websocket`.
- **Risk:** None — pure tightening. Test indexing E2E (test_ws_index_session suite exists).
### SEC-28 — Local data unencrypted at rest ⚠️ REVIEW (product decision)
- **Files:** `adiuvAI/src/main/db/index.ts:94` (plain SQLite), `adiuvAI/src/main/attachments/storage.ts:17-19`
- **Severity:** Medium (informational) · OWASP A02:2021
- **Problem:** All local user content (clients, projects, notes, attachments) plaintext on disk. For a data-sovereignty product, a deliberate gap.
- **Fix (decision):** Options: SQLCipher (better-sqlite3-multiple-ciphers fork), or document reliance on OS full-disk encryption, or encrypt attachments with the existing device backup key (DEAD-07). Roberto decides; do not execute without direction.
### SEC-29 — Register email enumeration · Low ⚠️ REVIEW
- **File:** `api/app/api/routes/auth.py:127` — 409 "Email already registered". Acceptable UX trade-off for most SaaS; mitigate via SEC-02 rate limiting. **Fix:** no code change now; revisit if email verification flow lands.
### SEC-30 — Internal exception strings to WS clients · Low ⚠️ REVIEW (mechanical)
- **File:** `api/app/api/routes/device_ws.py:467-468, 503-505, 539-541, 578-580, 625-631, 653-659`
- **Problem:** `error=str(exc)` / f-string exception text sent to renderer — can leak DB errors, paths.
- **Fix:** Replace each with a generic message (`"Internal error"` + the request_id); keep the existing server-side `logger.exception`. 6 call sites, mechanical.
### SEC-31 — attachments.create arbitrary sourcePath · Low ⚠️ REVIEW
- **File:** `adiuvAI/src/main/router/index.ts:1893-1915` → `attachments/storage.ts:25-37`
- **Problem:** Renderer-supplied absolute path copied without restriction; defense-in-depth issue (matters only post-SEC-06/20 compromise).
- **Fix:** Track paths returned by the `dialog:showOpenDialog` handler in a per-session `Set`; `attachments.create` rejects `sourcePath` not in the set. Cap file size (e.g. 100 MB).
- **Risk:** Drag-and-drop flows (if any) bypass the dialog — verify before enforcing.
### SEC-32 — Scout Gmail deep link no state check · Low ⚠️ REVIEW
- **File:** `adiuvAI/src/main/index.ts:46-54`
- **Problem:** `adiuvai://scout/oauth/gmail/callback` code+state forwarded to renderer with no pending-flow check (login OAuth path does this correctly in `auth-manager.ts:321-359`). Backend validates state server-side, limiting impact.
- **Fix:** Mirror `_pendingOAuth`: main process records pending Gmail-OAuth state when the flow starts; `handleDeepLink` drops callbacks whose state doesn't match.
### SEC-33 — Waitlist partial email in logs · Low
- **File:** `waitlist/app/routes.py:75`. `email[:3]` can reveal short local-parts. **Fix:** log entry `id` only. Mechanical.
### SEC-34 — Website missing CSP/security headers · Low ⚠️ REVIEW
- **Fix:** At the reverse proxy serving `website/`: add `Content-Security-Policy` (allowlist cdnjs/unpkg or self-hosted after SEC-26), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`. Infra change, not repo code.
### SEC-35 — Website innerHTML i18n sink · Low ⚠️ REVIEW (mechanical)
- **File:** `website/i18n.js:461-464`
- **Problem:** `el.innerHTML = t[key]` — not exploitable today (hardcoded dictionary) but latent; `?lang=` is user-controlled.
- **Fix:** Use `textContent` for all keys except an explicit whitelist of the 4 `data-i18n-html` keys that genuinely contain markup; never derive `t[key]` content from URL/user input.
### SEC-36 — sandbox not explicit · Low ⚠️ REVIEW
- **File:** `adiuvAI/src/main/index.ts:97-98` (webPreferences)
- **Problem:** `contextIsolation: true`, `nodeIntegration: false` are set (good), fuses are strong, but `sandbox` not enabled. Preload only uses `contextBridge`/`ipcRenderer`, so sandbox should be compatible.
- **Fix:** Add `sandbox: true` to webPreferences; full regression pass on packaged build (preload behavior changes under sandbox).
---
# 2. Correctness & Safety
### CORR-01 — Chat tool calls: no timeout + send-before-register race ⚠️ REVIEW
- **File:** `api/app/api/routes/device_ws.py:238-245` (`_executor`); contrast correct pattern `api/app/core/scout_runner.py:190-198`
- **Severity:** Critical
- **Problem:** (1) `await future` with no `asyncio.wait_for` — if Electron never replies (renderer crash, swallowed executor exception) the agent run hangs forever. (2) Frame is sent **before** `create_pending_call` registers the future; a fast `tool_result` arriving first no-ops on unknown id → permanent hang.
- **Fix:** Reorder: `future = device_manager.create_pending_call(user_id, payload["id"])` THEN `await websocket.send_text(...)`. Wrap: `return await asyncio.wait_for(future, timeout=30)` with `except asyncio.TimeoutError:` → pop the entry from `device_manager` `pending_calls` and return `{"error": "tool call timed out"}` so the LLM loop can continue. Also pop the dict entry on timeout in the scout path (`device_manager.py:145` leaks it).
- **Risk:** 30 s must exceed slowest legitimate executor op (large folder file reads) — confirm against `read_project_folder_file` worst case. Test: kill renderer mid-tool-call → agent run ends with error within 30 s.
### CORR-02 — WS unregister clobbers reconnected connection ⚠️ REVIEW
- **Files:** `api/app/api/routes/device_ws.py:133-145` · `api/app/core/device_manager.py:73-81`
- **Severity:** High
- **Problem:** Message loop ends silently on disconnect; heartbeat keeps the handler alive up to 30 s. If the client reconnects in that window, `register()` stores the new connection, then the OLD handler's `finally` calls `unregister(user_id)` — **popping the new healthy connection** and cancelling its pending futures. Also user looks online during the zombie window (scout runs dispatched into void).
- **Fix:** (1) `device_manager.unregister(self, user_id, ws)`: only pop when `self._connections.get(user_id)` and `self._connections[user_id].ws is ws`. Update both call sites. (2) End the heartbeat promptly: replace `asyncio.gather(loop, heartbeat)` with `done, pending = await asyncio.wait({...}, return_when=FIRST_COMPLETED)` then cancel pending.
- **Risk:** Core connection lifecycle — test reconnect-within-30s, normal disconnect, heartbeat failure (test_device_ws suite exists).
### CORR-03 — Free-tier extraction drains with empty content
- **File:** `api/app/core/memory_maintenance.py:120-154`
- **Severity:** High
- **Problem:** `run_extraction(..., last_user_msg="", last_assistant_msg="", ...)` — `row.episode_id` never used. Every hourly tick: one wasted gpt-4o-mini call per queue row over `"User: \nAssistant: "`, then row deleted. Free-tier memory extraction has never worked, and it costs money.
- **Fix:** For each row, load `MemoryEpisodic` by `row.episode_id` (+ `user_id`), decrypt `summary` with the user's Fernet (reuse `memory_middleware._get_fernet` or pass middleware instance), pass decrypted content as the extraction input. Group rows per user into a single call. Skip+delete rows whose episode is missing.
- **Risk:** Test asserting extracted candidates reference episode content (extend test_memory_extraction). Watch cost: now real extractions run.
### CORR-04 — Backend errors as success payloads ignored → chat wedged
- **Files:** `adiuvAI/src/main/router/index.ts:957-960` (returns `{response:'', error}`) · `adiuvAI/src/renderer/context/ContextualChatContext.tsx:160,210-240` · `adiuvAI/src/renderer/components/brief/TaskBriefChat.tsx:125,144` · `adiuvAI/src/renderer/components/ai/AIChatPanel.tsx:313-319`. Correct template: `useAIChat.ts:226-256`.
- **Severity:** High
- **Problem:** When offline/unauthenticated, no stream frames ever arrive; the three listed call sites never check `data.error` on mutation success → `isStreaming` stays true forever, `send()` no-ops, stream listeners leak (each leaked listener receives every future `ai:stream` frame).
- **Fix:** Preferred (cleaner, pre-1.0): make `aiRouter.chat`/brief/research procedures **throw TRPCError** instead of returning `{error}` (ipcLink already propagates errors, `ipcLink.ts:76`); then `onError` is the single error path — update `useAIChat` accordingly. Each renderer call site: in the error path, call the active unsubscribe, push an error bubble, reset `isStreaming`/`isResearching`. Add `useEffect` unmount cleanup for `TaskBriefChat`'s send listener (L144). Delete dead `hooks/useChatStream.ts` (DEAD-06).
- **Risk:** Touches every chat surface. Test: stop backend → send in contextual sidebar, task brief, home chat → error bubble shown, re-send works. Implement together with CORR-12 (shared error-frame contract).
### CORR-05 — Cascade-delete gaps; zero transactions ⚠️ REVIEW
- **Files:** `adiuvAI/src/main/router/index.ts` — `projects.delete` L211-220, `clients.deleteWithCascade` L119-151, `timelineEvents.delete` L711-723, `notes.delete` L835-840, `tasks.delete` L511-534 · `adiuvAI/src/main/api/drizzle-executor.ts` `handleDelete` L329-340
- **Severity:** High (data integrity)
- **Problem:** No FK constraints, hand-rolled deletes, no `db.transaction()` anywhere in src/main: project delete orphans timelineEvents/notes/projectFolderFiles/noteEdits; event delete leaves dangling dependency edges that **poison the cycle-guard BFS** (L756-772, falsely rejects new deps); AI-issued deletes (`handleDelete`) cascade nothing — orphan rows + attachment files on disk; `tasks.delete` interleaves file unlinks between row deletes (partial state on crash).
- **Fix:** Pre-1.0 clean option (preferred per Roberto's preference): add `references(..., { onDelete: 'cascade' })` to schema + enable `PRAGMA foreign_keys = ON` in `db/index.ts`, generate migration. Alternative: shared `cascadeDelete{Task,Project,Client,Note,TimelineEvent}` helpers in a new `src/main/db/cascade.ts`, used by BOTH tRPC routers and `handleDelete`, wrapped in `db.transaction()` (better-sqlite3 sync transactions), file unlinks after commit.
- **Risk:** FK route requires verifying every insert ordering satisfies constraints, and migration against existing user DBs with already-orphaned rows (clean orphans first). Test matrix: delete project with events+deps+notes+pending edits; agent-delete task with attachments → no orphan rows, files removed. Human review required — this deletes data.
### CORR-06 — WS drop wedges folder indexing in 'scanning'
- **Files:** `adiuvAI/src/main/api/backend-client.ts:1099-1100` (listeners cleared, `onDone` never called) · `adiuvAI/src/main/files/indexer.ts:106-117` (only `finalize` resets status) · consumers refusing rescan: `drizzle-executor.ts:486`, `files/daily-rescan.ts:22`, `router/projectFolders.ts:61`
- **Severity:** High
- **Problem:** WS close clears `indexListeners` without invoking callbacks → `folderLastScanStatus` stays `'scanning'` forever → all future scans refuse to run. Recovery currently requires manual DB surgery.
- **Fix:** (1) In the WS close handler, before `indexListeners.clear()`: iterate and call each `onDone('error')`. (2) Startup recovery in `initDb()` or app-ready: `UPDATE projects SET folder_last_scan_status='error' WHERE folder_last_scan_status='scanning'`.
- **Risk:** None meaningful. Test: kill backend mid-index → status becomes 'error', rescan works.
### CORR-07 — backend-client close handler clobbers new socket
- **File:** `adiuvAI/src/main/api/backend-client.ts:1079-1104`
- **Severity:** Medium-High
- **Problem:** `ws.on('close')` unconditionally nulls `persistentWs`, stops heartbeat, rejects+clears ALL listener maps. Logout→login: the old socket's deferred close event destroys the new connection's state and schedules a duplicate reconnect.
- **Fix:** Capture `ws` in closure; first line of the close and error handlers: `if (this.persistentWs !== ws) return;`. Same guard inside the heartbeat interval callback.
- **Risk:** Low. Test logout+immediate login → exactly one socket, in-flight chat on new socket survives.
### CORR-08 — Decay applied per invocation, not per period
- **File:** `api/app/core/memory_maintenance.py:280-309` (proactive, runs hourly), `:61-104` (relations)
- **Severity:** Medium
- **Problem:** `periods = days_elapsed // PERIOD; new_confidence = confidence * FACTOR**periods` recomputed from immutable `created_at` and re-applied every tick — a row ≥8 days old loses 10% **per hour** (intended: per 7 days).
- **Fix:** Compute confidence as a pure function of age each read, OR add `last_decayed_at` column (migration) and decay only `(now - last_decayed_at) // PERIOD` periods, updating the column. Pure-function approach avoids the migration — preferred.
- **Risk:** Recompute approach changes stored semantics — confirm nothing else mutates `confidence` (confirmation boosts do; then `last_decayed_at` is the correct route). ⚠️ REVIEW the choice.
### CORR-09 — Check-then-insert races, missing unique constraints ⚠️ REVIEW
- **Files:** `api/app/core/memory_middleware.py:232-247` (`update_core`), `:421-450` (`upsert_relation`); `api/app/api/routes/auth.py:125-127` (register IntegrityError→500)
- **Severity:** Medium
- **Problem:** No `UNIQUE(user_id, key)` on memory_core, none on memory_relations `(user_id, subject_label, predicate, object_label)`. Concurrent writers (documented fire-and-forget language seeding + onboarding PUT) create duplicates; `scalar_one_or_none()` then raises `MultipleResultsFound`. Register duplicate-email race returns 500 instead of 409.
- **Fix:** Alembic migration adding both unique constraints (dedupe existing rows first: keep newest per key). Convert both writers to PostgreSQL `INSERT ... ON CONFLICT DO UPDATE` (copy the savepoint pattern from `scouts/engine.py:123-135`). Register: catch `IntegrityError` → 409.
- **Risk:** Migration fails if production data has duplicates — dedupe step mandatory. Test concurrent update_core calls.
### CORR-10 — Fire-and-forget create_task: no refs, no concurrency cap
- **Files:** `api/app/api/routes/device_ws.py:120,130,171-215`; also `scouts.py:247-249`, `memory_middleware.py:201`, `note_agent.py:84,112`
- **Severity:** Medium
- **Problem:** Bare `asyncio.create_task` — loop holds weak refs (GC can collect mid-flight); exceptions surface only at GC; a frame-spamming client spawns unbounded concurrent agent runs (WS path has no rate limit).
- **Fix:** Per-connection `tasks: set[asyncio.Task]`; on spawn: `t = create_task(...); tasks.add(t); t.add_done_callback(tasks.discard)`. In WS `finally`: cancel all. Cap: if ≥3 in-flight stream-producing requests for the connection, reply immediately with `stream_end` error frame "busy".
- **Risk:** Cap value is judgment — confirm 3 is right for legitimate parallel use (home + contextual + brief). ⚠️ REVIEW the cap.
### CORR-11 — tasks.update wipes briefing + chat history on every update
- **File:** `adiuvAI/src/main/router/index.ts:504-506`; hash helper exists at `:55-64`; staleness check at `:976-989`
- **Severity:** Medium (silent user-data loss)
- **Problem:** Every update — including a kanban status drag — deletes `taskBriefings` + `taskBriefChats`.
- **Fix:** Load the task before update; compute `hashTaskForBriefing(prev)` vs hash of merged next state; run the two deletes only when hashes differ.
- **Risk:** Verify `hashTaskForBriefing` covers exactly the fields the briefing depends on. Test: status drag preserves brief chat; title edit invalidates it.
### CORR-12 — Stream errors emitted as plain stream_end
- **File:** `adiuvAI/src/main/ai/orchestrator.ts:105,111,141,147,192,198`; renderer persistence of empty message `ContextualChatContext.tsx:187-204`
- **Severity:** Medium
- **Problem:** `onError` sends a normal `stream_end` (no error flag); catch blocks send `stream_end` with `requestId: ''` (matches nothing). Mid-stream failure looks like success → blank assistant bubble **persisted to SQLite**.
- **Fix:** Extend the stream-end frame with optional `error?: string` in `src/shared` types (mirror backend's existing error-capable `WsStreamEnd`). orchestrator: pass real requestId + error message; delete the `requestId: ''` sends. Renderers: on `error`, show error bubble, skip `appendMessage` persistence when content empty. Backend twin fix: CORR-15.
- **Risk:** Frame contract change across main↔renderer — do together with CORR-04.
### CORR-13 — withRetry retries non-idempotent POSTs ⚠️ REVIEW
- **File:** `adiuvAI/src/main/api/backend-client.ts:700-723`
- **Severity:** Medium
- **Problem:** `proxyPost` retried up to 3× on timeout/5xx/429 — a timed-out-but-processed `/scouts/trigger` or scout create duplicates server work; immediate retry on 429 worsens it.
- **Fix:** Add `{ retry?: boolean }` option to `proxyPost` defaulting false; enable only for verified-idempotent endpoints (reads proxied via POST, if any). Exclude `RateLimitError` from retry entirely (or respect Retry-After). GET/DELETE-by-id keep retry.
- **Risk:** Requires classifying every proxyPost call site idempotent-or-not — review the list.
### CORR-14 — Sync Stripe calls block the event loop
- **File:** `api/app/billing/stripe_service.py:71-79, 197, 225-238` (auto_paging_iter paginates network synchronously)
- **Severity:** Medium
- **Fix:** Wrap each Stripe call in `await asyncio.to_thread(...)`; for `auto_paging_iter`, collect inside the thread: `await asyncio.to_thread(lambda: list(itertools.islice(it, 100)))`.
- **Risk:** None behavioral. Mechanical once pattern set.
### CORR-15 — run_home_stream swallows exceptions; no error stream_end
- **File:** `api/app/api/routes/device_ws.py:295-307`; correct pattern at `:498-505` (brief)
- **Severity:** Medium
- **Problem:** On exception: logged, partial chunks stored as an episode, client never receives `stream_end` with error → renderer hangs (pairs with CORR-04).
- **Fix:** In the except block: send `WsStreamEnd(request_id=..., error="Internal error")` (generic per SEC-30); skip `store_episode` when no response text was produced.
- **Risk:** None. Test: raise inside agent → client gets error frame.
### CORR-16 — delete_account: partial transaction + broad except ⚠️ REVIEW
- **File:** `api/app/api/routes/auth.py:759-795`
- **Severity:** Medium
- **Problem:** Stripe cancel commits its own transaction, then memory deletes, then user delete + final commit; failure mid-way = cancelled subscription with live account. `except Exception: pass` at L786-787 hides genuine failures. (DB-level `ondelete="CASCADE"` makes the manual memory deletes redundant.)
- **Fix:** Reorder: all DB deletes in one transaction first (drop the redundant manual memory-table deletes — FK cascade covers them; verify each memory table's FK has `ondelete="CASCADE"` in migrations before relying on it), commit, THEN Stripe cancel; on Stripe failure log + enqueue retry rather than failing the deletion. Narrow the except to expected types.
- **Risk:** Account deletion is irreversible — human review + test with seeded data.
### CORR-17 — Index-session TOCTOU + stale _active entry
- **Files:** `adiuvAI/src/main/files/indexer.ts:67` (`startIndexSession`) · `adiuvAI/src/main/router/projectFolders.ts:84-99` · trigger sites `drizzle-executor.ts:487-501`, `files/daily-rescan.ts:22`
- **Severity:** Medium
- **Problem:** Three concurrent triggers can all pass the `!== 'scanning'` check before any writes 'scanning'. `_active.set` runs after `await startIndexSession` returns — a zero-delta session completes before the entry exists, leaving stale `{status:'starting'}`.
- **Fix:** Module-level `const _inFlight = new Set<string>()` in indexer.ts; first synchronous statement of `startIndexSession`: `if (_inFlight.has(projectId)) throw new Error('Scan already in progress'); _inFlight.add(projectId)`; remove in `finalize`'s finally. In projectFolders.ts, register the `_active` entry before awaiting.
- **Risk:** Low. Also add `.catch(console.error)` to the `void startIndexSession(...)` fire-and-forget sites (daily-rescan.ts:25, drizzle-executor.ts:497).
### CORR-18 — Scout trigger TOCTOU · Low
- **File:** `api/app/api/routes/scouts.py:225-249` (+ `scout_runner.py:577`)
- **Fix:** Add `stable_agent_id` to `_running_agents` synchronously in the route before `db.commit()`/`create_task`; discard on spawn failure. Keep the in-task add idempotent.
### CORR-19 — Swallowed-error triage · Low
- **Files/fixes (mechanical batch):**
- `adiuvAI/src/main/router/index.ts:1156-1164, 1276-1294, 1374-1382, 1429-1434` — scout list/labels/catalog/runs return `[]` on any error → return `{ data, error }` envelope (or throw, per QUAL-11's chosen convention).
- `adiuvAI/src/main/api/drizzle-executor.ts:566-568` — include `error: message` in the `kind:'error'` result.
- `adiuvAI/src/main/auth/auth-manager.ts:594` — wrap `JSON.parse(text)` in try/catch, return `{}` on failure.
- Documented-intentional `.catch(() => {})` seeding (router/index.ts:1577,1585) — leave as is.
### CORR-20 — Naive/aware datetime kludge · Low
- **File:** `api/app/api/routes/auth.py:205` — unconditional `.replace(tzinfo=timezone.utc)` would mislabel a non-UTC value. **Fix:** `if rt.expires_at.tzinfo is None: rt_exp = rt.expires_at.replace(tzinfo=timezone.utc) else: rt_exp = rt.expires_at` (pattern from `scout_runner.py:166-167`).
### CORR-21 — Renderer/main misc · Low
- `adiuvAI/src/renderer/index.tsx:22-30` — move `LanguageSync` component definition to module scope (currently declared inside `App`, remount trap).
- `adiuvAI/src/main/ipc.ts:68` — `undefined as unknown as AbortSignal`: make the field optional in the local type (see TYPE-05 family).
---
# 3. Performance
### PERF-01 — bcrypt on the event loop
- **File:** `api/app/api/routes/auth.py:69-74`, called at `:134, :169, :656-659`
- **Severity:** High
- **Problem:** 100-300 ms CPU per call freezes ALL WS streams/tool round-trips; login is currently rate-limit-exempt (SEC-02) → credential-stuffing stalls the whole server.
- **Fix:** `return await asyncio.to_thread(bcrypt.hashpw, password.encode(), bcrypt.gensalt())` etc. — make `_hash_password`/`_verify_password` async, update 3 call sites.
- **Risk:** None. Verify tests still pass (auth suite exists).
### PERF-02 — Home channel buffers all tokens (fake streaming) ⚠️ REVIEW
- **File:** `api/app/core/deep_agent.py:1219-1227` (token withholding), `:1075` (`ainvoke` not `astream`)
- **Severity:** High (UX latency)
- **Problem:** `run_home_stream` accumulates every token and yields ONE blob after the whole run (including tool loops). User sees nothing until the end. Root cause: `_normalize_tagged_list_lines` needs the full text.
- **Fix:** Switch the LLM call to `llm_with_tools.astream()` accumulating tool-call deltas; make `_normalize_tagged_list_lines` line-buffered (process and flush complete lines as they arrive, hold only the current partial line). Measure time-to-first-frame before/after.
- **Risk:** Tool-call delta accumulation across LangChain/LiteLLM is fiddly; the comment near L1080-1100 documents a past double-LLM-call bug that must not regress. Strong-model territory.
### PERF-03 — Hourly mining duplicates + unbounded prompt injection
- **File:** `api/app/core/memory_maintenance.py:175-253`; `api/app/core/memory_middleware.py:703-718` (`_load_proactive`, no LIMIT); cron at `api/app/main.py:46-77`
- **Severity:** High (cost + correctness)
- **Problem:** Hourly re-mining of the same 30-day window, 3-5 new rows per tick, no dedup → ~100 near-duplicate rows/user/day, all decrypted into every chat prompt.
- **Fix:** (1) `_load_proactive`: add `.order_by(confidence.desc()).limit(20)`. (2) Mine only when episodes newer than the user's latest `MemoryProactive.created_at` exist. (3) Dedup new patterns against existing (reuse `decide_action`). (4) Consider daily cadence for the mining portion of the cron.
- **Risk:** (1) is mechanical and urgent; (2)-(4) need the memory test suites re-run.
### PERF-04 — No SQLite indexes (Electron)
- **File:** `adiuvAI/src/main/db/schema.ts` (only ai_chat tables indexed, migration 0006)
- **Severity:** Medium (High at scale — scans are synchronous, blocking all IPC)
- **Fix:** Add Drizzle `index()` on: `tasks.projectId`, `tasks.dueDate`, `taskComments.taskId`, `taskAttachments.taskId`, `notes.projectId`, `noteEdits.noteId`, `timelineEvents.projectId`, `timelineEventDependencies.fromEventId`+`.toEventId`, `scoutRunActions.runId`; `uniqueIndex` on `projectFolderFiles(projectId, relativePath)` (indexer does a per-file SELECT by that pair — `files/indexer.ts:128-136`, O(n²)). `npx drizzle-kit generate`.
- **Risk:** Migration must apply cleanly on existing user DBs (bootstrapMigrationsLedger path) — test against a populated dev DB.
### PERF-05 — Missing PG indexes; refresh_tokens never purged
- **File:** `api/app/models.py` (+ alembic migration needed)
- **Severity:** Medium
- **Fix:** One Alembic migration adding: `memory_core(user_id, key)` (unique — shared with CORR-09), `memory_episodic(user_id, created_at DESC)`, `memory_relations(user_id, subject_label, predicate, object_label)` (unique — CORR-09), `scout_run_logs(user_id, started_at)`, HNSW index on `memory_associative.embedding` (`USING hnsw (embedding vector_cosine_ops)`), `refresh_tokens(expires_at)`. Add a daily cleanup to the existing cron: `DELETE FROM refresh_tokens WHERE expires_at < now()`.
- **Risk:** HNSW build time on large tables; coordinate with CORR-09 dedupe.
### PERF-06 — Per-token IPC + full markdown re-parse
- **Files:** `adiuvAI/src/main/ai/orchestrator.ts:103,139,180` · `adiuvAI/src/renderer/components/ai/ChatSurface.tsx:197-199,352-358` · `useAIChat.ts:185-188`, `ContextualChatContext.tsx:183-185`, `TaskBriefChat.tsx:98-101`
- **Severity:** Medium
- **Problem:** token → IPC message → setState → full ReactMarkdown re-parse of the accumulated text + smooth scroll, per token. Quadratic on long answers.
- **Fix:** Coalesce in main: buffer chunks per requestId, flush on 40 ms timer (and on stream_end). Renderer: render the in-flight message as plain text (whitespace-pre-wrap), run ReactMarkdown only on `stream_end`; throttle autoscroll with requestAnimationFrame.
- **Risk:** Ensure stream_end flushes the tail buffer; visual check that markdown "pops in" acceptably at end.
### PERF-07 — Serial tool calls per LLM step
- **File:** `api/app/core/deep_agent.py:953-986, 1103-1137`; `api/app/core/scout_runner.py:289-312`
- **Severity:** Medium
- **Fix:** `results = await asyncio.gather(*(t.ainvoke(args) for ...))`, then append `ToolMessage`s preserving the original tool_call id order. Apply in all three loops (or once, after QUAL-03's shared loop).
- **Risk:** Concurrent tool calls hit the Electron executor concurrently — drizzle-executor handlers are async over a sync driver, fine, but verify pending-call map handles parallel ids (it's keyed by id — fine). Order preservation matters for the LLM.
### PERF-08 — Blocking Langfuse flush/get_prompt
- **Files:** `api/app/core/deep_agent.py:1009-1010,1165-1166` · `scout_runner.py:323-324` · `memory_extraction.py:319-322` · `langfuse_client.py:99`
- **Severity:** Medium
- **Fix:** Delete the per-request `lf.flush()` calls (SDK background-flushes; keep one flush in app shutdown lifespan). Wrap `lf.get_prompt(...)` in `asyncio.to_thread` at the call sites in langfuse_client, or pre-warm the prompt cache at startup.
- **Risk:** Traces may lag a few seconds in Langfuse UI — acceptable.
### PERF-09 — get_current_user does 3 queries + full memory decrypt per request
- **File:** `api/app/api/middleware/auth.py:62-91`
- **Severity:** Medium
- **Fix:** Split dependencies: `get_current_user` (user+tier in ONE query via outerjoin on subscriptions, no memory) and `get_current_user_with_memory` (adds core-block decrypt). Switch only the routes that actually read `.memory` (chat/profile) to the latter; scout CRUD, billing, etc. use the light one.
- **Risk:** Grep every `current_user.memory` access before switching routes. Test suite covers auth paths.
### PERF-10 — Redundant memory-middleware queries; per-key commits
- **Files:** `api/app/core/memory_middleware.py` (`_get_fernet`/`_get_user_debug` per method) · `api/app/api/routes/auth.py:560-575` (`update_memory` loop)
- **Severity:** Medium
- **Fix:** Cache `(user, fernet, tier)` on the middleware instance per request (it's constructed per call — add lazy `_ctx` loaded once). `update_memory`: add a `update_core_many(dict)` method doing one SELECT of existing keys, one bulk write, one commit.
- **Risk:** Instance lifetime check: confirm MemoryMiddleware isn't a long-lived singleton (it takes `db` per call — if constructed per request, caching is safe).
### PERF-11 — Health check before every chat
- **File:** `adiuvAI/src/main/ai/orchestrator.ts:76-87`
- **Fix:** In `checkConnectivity`, return early-success when `backendClient.persistentWs?.readyState === OPEN` (expose a `isWsConnected()` getter); keep the HTTP health check only as fallback when WS down.
- **Risk:** None — `sendHomeRequest` already rejects with OfflineError when WS is down.
### PERF-12 — Paid brief regen per mutation
- **File:** `adiuvAI/src/main/router/index.ts:447,502,532,668,706,721` → `orchestrator.ts:240-254` (1.5 s debounce)
- **Severity:** Medium (LLM cost)
- **Fix:** Replace eager regeneration with dirty-flag: `invalidateBriefCache()` only marks the cache stale; regeneration happens lazily on next `dailyBrief` read or the existing scheduler slot tick. Delete `scheduleBriefRegeneration`.
- **Risk:** First home-page visit after edits pays the generation latency — show the stale brief while regenerating (cache already stores it).
### PERF-13 — No code-splitting; heavy eager imports
- **Files:** `adiuvAI/vite.renderer.config.mts:13-16`, `vite.web.config.mts:22` · `ChatChartBlock.tsx:20` (recharts) · `routes/notes.$noteId.tsx:27` (Milkdown)
- **Fix:** Add `autoCodeSplitting: true` to TanStackRouterVite in both configs. `React.lazy` + Suspense for `ChatChartBlock` (renders only on `<chart>` tag) and `MilkdownEditor`.
- **Risk:** Test chart block + notes editor render after splitting (Suspense fallback flash).
### PERF-14 — Default QueryClient → focus refetch storms (mechanical)
- **File:** `adiuvAI/src/renderer/index.tsx:15` (contrast `web-main.tsx:24`)
- **Fix:** `new QueryClient({ defaultOptions: { queries: { staleTime: 30_000, refetchOnWindowFocus: false } } })` — local writes already invalidate explicitly.
- **Risk:** Multi-window staleness (single-window app — fine).
### PERF-15 — Double folder walk; no WS backpressure
- **Files:** `adiuvAI/src/main/router/projectFolders.ts:65` + `files/indexer.ts:67,191-215`
- **Fix:** Pass the pre-flight `ScanDelta` into `startIndexSession` (skip re-walk). In the batch loop, check `ws.bufferedAmount` and await drain (or gate on `onProgress` acks) before sending the next batch.
- **Risk:** Backpressure logic needs a stall timeout; test with a large folder.
### PERF-16 — DB sessions across LLM calls
- **Files:** `api/app/scouts/engine.py:48-70` · `api/app/api/routes/device_ws.py:736-829`; pool config `api/app/db.py:24-28`
- **Fix:** Restructure to short sessions: load needed rows → close → LLM work → new session for writes. Interim: raise `pool_size`/`max_overflow` and set explicit `pool_timeout`.
- **Risk:** Detached-instance errors after session close — copy needed attrs to plain objects first.
### PERF-17 — Per-file metadata WS round-trips
- **File:** `api/app/core/scout_runner.py:386-405`
- **Fix:** Extend the Electron `list_directory` executor result to include `mtimeMs`/`size` per entry (same `fs.stat` it already does for dirents — `drizzle-executor.ts:375-398`); backend reads mtimes from the listing, drops per-file `get_file_metadata` calls.
- **Risk:** Two-repo protocol change; version-skew tolerance (backend falls back to old path if field absent).
### PERF-18 — Per-call AsyncOpenAI clients (mechanical)
- **Files:** `api/app/core/embeddings.py:23` · `api/app/core/llm.py:154`
- **Fix:** Module-level `_client: AsyncOpenAI | None` lazy singleton; reuse across calls.
### PERF-19 — tasks.list client-side pagination · Low
- **Files:** `adiuvAI/src/main/router/index.ts:314-317` · `TaskListView.tsx:80,101`
- **Fix:** Pass `limit/offset` from TaskPager state; add `statusIn` filter server-side for the `active` pseudo-filter (L94-95). Defer until data sizes warrant.
### PERF-20 — Limiter dict growth; tier from JWT · Low
- **File:** `api/app/api/middleware/rate_limit.py:81,97-98,107-128`
- **Fix:** Delete empty lists after trim; superseded entirely if SEC-17's Redis lands. Tier-from-JWT staleness: document or read from DB (cheap once PERF-09's joined query exists).
### PERF-21 — Unbounded reads in drizzle-executor · Low
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts:623-630` (page details), `:535-548` (pdf/docx/image base64 uncapped)
- **Fix:** Apply default limit 50 (as `handleSelect` does) to the `_all` page-detail queries; enforce the 500 KB cap on binary reads too (reject larger with explicit error).
### PERF-22 — Context re-render hotspots · Low
- **Files:** `adiuvAI/src/renderer/context/HeaderContext.tsx:30` (unmemoized value) · `ContextualChatContext.tsx:253-270` + `hooks/useContextualScope.ts:5`
- **Problem:** Every route re-renders per streamed sidebar token (scope consumers subscribe to the same context as `streamingContent`).
- **Fix:** `useMemo` the HeaderContext value. Split ContextualChatContext into stable-actions context (`setScope`, `open`, `toggle`) and volatile stream context (`messages`, `streamingContent`, `isStreaming`); `useContextualScope` consumes only the stable one.
---
# 4. Dead Code
### DEAD-01 — Five dead Python deps · High (mechanical)
- **File:** `api/requirements.txt` lines 12, 25, 26, 27, 31
- **Problem:** `boto3`, `moto[s3]`, `pinecone`, `qdrant-client`, `google-auth-oauthlib` — zero imports/refs anywhere (grep-verified; pgvector is the live vector path; `oauth_providers.py:71` explicitly states it does NOT use google-auth-oauthlib).
- **Fix:** Delete those 5 lines. Run `pytest` after. Keep `python-dotenv` (pydantic-settings `env_file` needs it) and `google-auth-httplib2` (transitive need of googleapiclient).
### DEAD-02 — Orphaned scout_registry.py · High (mechanical)
- **File:** `api/app/core/scout_registry.py` — `BaseAgent`, zero importers, zero subclasses, references deleted vector-store era. **Fix:** delete file; run pytest.
### DEAD-03 — slowapi dead · Medium (mechanical)
- **Files:** `api/app/api/middleware/rate_limit.py:26-28,52,66-67` · `middleware/__init__.py:10,17` · `requirements.txt:13`
- **Problem:** `Limiter` exported "for optional route-level decoration" — zero `@limiter` decorated routes exist.
- **Fix:** Remove the Limiter creation/exports and the requirement. **Sequencing:** do AFTER SEC-02 (which may or may not reuse slowapi — recommended fix reuses the custom window, so slowapi still goes).
### DEAD-04 — chat.py HTTP routes production-unused ⚠️ REVIEW (owner decision)
- **Files:** `api/app/api/routes/chat.py:40,63,105`; stale comments `adiuvAI/src/main/api/backend-client.ts:9,14`
- **Problem:** Electron client hits none of `POST /chat`, `/chat/brief`, `/chat/embed` (all traffic on `WS /api/v1/ws/device`). Routes are test-covered but production-dead — unless an external consumer exists.
- **Fix:** Roberto confirms no external consumers → delete routes + their tests (`test_brief_agent.py`/`test_middleware.py` portions that exercise them — keep middleware tests by retargeting another POST route). Either way, fix the stale doc comments in backend-client.ts (mentions nonexistent `WS /api/v1/chat/stream`) and `adiuvAI/src/shared/api-types.ts:338,390,425` (references nonexistent `/agents/*` HTTP routes). Note CLAUDE.md path drift: actual WS route is `/api/v1/ws/device`, docs say `/api/v1/device`.
### DEAD-05 — Four unused npm deps · Medium (mechanical)
- **File:** `adiuvAI/package.json:56,76,77,78`
- **Fix:** `npm uninstall next-themes mammoth pdf-parse @hello-pangea/dnd` (knip + grep verified zero imports; mammoth/pdf-parse are LanceDB-era leftovers).
### DEAD-06 — Dead Electron modules · Medium
- **Files (knip-verified, delete):** `adiuvAI/src/shared/batch-types.ts` (~5 KB), `src/renderer/hooks/useChatStream.ts` (has the CORR-04 bug — delete, don't fix), `src/renderer/hooks/useTaskBriefCache.ts`, `src/renderer/components/agents/ScoutRunLog.tsx`, `src/renderer/components/ai/blocks/index.tsx` (dead barrel).
- **⚠️ REVIEW one:** `blocks/ChatTableBlock.tsx` is dead only because the barrel is — check `api/app/core/output_formatter.py` first: if the backend emits table segments, wire ChatTableBlock into ChatSurface's segment switch instead of deleting (missing feature, not dead code).
### DEAD-07 — backup-key.ts unwired ⚠️ REVIEW (owner decision)
- **File:** `adiuvAI/src/main/auth/backup-key.ts` — zero importers, but CLAUDE.md documents it as deliberate architecture. **Fix:** Roberto decides: wire into the backup feature roadmap or delete (recoverable from git). Interacts with SEC-21 (its storage fallback) and SEC-28 (could encrypt attachments).
### DEAD-08 — knip.json missing web entries · Medium (mechanical)
- **File:** `adiuvAI/knip.json`
- **Problem:** `web-main.tsx` + `lib/httpLink.ts` false-flagged (live via `web.html:11`).
- **Fix:** Add `"web.html"` and `"src/renderer/web-main.tsx"` to the `entry` array; optionally add ignore rule for TanStack `Route` exports. Do this FIRST so subsequent knip runs are trustworthy.
### DEAD-09 — langchain meta-package; websockets pin · Medium (mechanical)
- **File:** `api/requirements.txt:4,20`
- **Fix:** Replace `langchain>=0.3.0` with `langchain-core>=0.3.0` (only `langchain_core`/`langchain_openai`/`langchain_litellm` are imported). Drop explicit `websockets` pin (uvicorn[standard] bundles it). Run pytest.
### DEAD-10 — Ruff F401 batch · Low (mechanical)
- **Fix:** `cd api && ruff check . --select F401 --fix` (13 findings: quota.py:7, deep_agent.py:1247, 11 test files). Keep the ERA001-flagged comment blocks (scouts.py:438-457, email_html.py:22 — documentation, add `# noqa: ERA001` if noisy).
### DEAD-11 — Dead TS exports · Low (mechanical)
- **Fix:** Remove `export` (or delete bodies): `getDbPath`/`getRawSqlite`/`closeDb` (`src/main/db/index.ts:118-137`), `generateAndCacheBrief` (`orchestrator.ts:257` — superseded if PERF-12 lands; coordinate), `attachmentsRoot` (`attachments/storage.ts:17`), `parseMutationsToEntityTags` (`useAIChat.ts:76`), `formatTime` (`lib/date.ts:72`), `parseDateRange` (`lib/parseDate.ts:151`). Keep error classes exported (instanceof checks); keep `webPlatform` until DEAD-08 confirms.
### DEAD-12 — Dev deps in runtime requirements · Low (mechanical)
- **File:** `api/requirements.txt:22-25,41`
- **Fix:** Move `pytest`, `pytest-asyncio`, `aiosqlite`, `ruff` to a new `requirements-dev.txt`; update Dockerfile to install only runtime file; CI installs both.
---
# 5. Dependencies
### DEPS-01 — ws 8.19.0 vulnerable · High (mechanical)
- **File:** `adiuvAI/package.json` — GHSA-58qx-3vcg-4xpx (uninitialized memory disclosure, affects ≤8.20.0). Only runtime-reachable vuln of the 58 npm audit hits. **Fix:** `npm audit fix` (→ ^8.21.0, semver-compatible).
### DEPS-02 — python-jose floor permits CVE versions · High (mechanical)
- **File:** `api/requirements.txt:10` — floor 3.3.0 permits CVE-2024-33663 (algorithm confusion) / CVE-2024-33664 (JWE DoS), fixed in 3.4.0. App pins HS256 with explicit `algorithms=[...]` everywhere (mitigates the confusion path), but raise anyway. **Fix:** `python-jose[cryptography]>=3.4.0`. **⚠️ REVIEW (later):** consider migrating to PyJWT (python-jose barely maintained) — separate ticket.
### DEPS-03 — cryptography floor · Medium (mechanical)
- **File:** `api/requirements.txt:34` — floor 42.0.0 permits CVE-2024-26130 versions. **Fix:** `cryptography>=43.0.1`.
### DEPS-04 — No Python lock file · Medium
- **Problem:** All `>=` floors; installed versions unauditable. **Fix:** Adopt `uv lock` or `pip-compile`; commit the lock; run `pip-audit` against it in CI. ⚠️ REVIEW tool choice.
### DEPS-05 — eslint 8 + @typescript-eslint 5 EOL ⚠️ REVIEW
- **File:** `adiuvAI/package.json:38-39` — ts-eslint v5 predates TS 5.x support; lint coverage degraded. **Fix:** Dedicated PR: eslint 9/10 flat-config migration + typescript-eslint 8. Medium effort (config rewrite).
### DEPS-06 — Electron 40 → 42 ⚠️ REVIEW
- **Fix:** Dedicated PR bumping electron; rebuild/retest better-sqlite3 (forge cross-compilation hooks download platform binaries — verify versions), full smoke of packaged app.
### DEPS-07 — Transitive toolchain advisories · Low (no action)
- `tmp`/`tar` via @electron-forge 7.x, `esbuild` via drizzle-kit — build-machine-only, **no fix available**. Do NOT run `npm audit fix --force` (downgrades forge/drizzle-kit). Monitor forge releases.
### DEPS-08 — @types/ws placement · Low (mechanical)
- **File:** `adiuvAI/package.json:65` — move to devDependencies.
---
# 6. Code Quality
> All QUAL items execute in **Phase 6**, after behavior fixes, because they move code and invalidate line references. Items marked ⚠️ involve behavior-preservation judgment.
### QUAL-01 — Split router/index.ts (1967 LOC) · High
- **Fix:** Split along existing boundaries into `src/main/router/{trpc.ts,clients.ts,projects.ts,tasks.ts,timeline.ts,notes.ts,ai.ts,scout.ts,auth.ts,memory.ts,settings.ts,attachments.ts}`; boundaries: clients 71-152, projects 154-228, tasks 230-560, timelineEvents 561-725, deps 726-787, notes 788-842, taskComments 843-877, settings 878-922, ai 923-1090, scout* 1091-1516, auth 1517-1729, memory 1730-1784, noteEdits 1785-1867, taskAttachments 1868-1946. `trpc.ts` holds the `t`/`router`/`publicProcedure` factory; `index.ts` becomes the `appRouter` merge + `AppRouter` export. Move brief helpers (L25-65) to `src/main/ai/brief-cache.ts` (with PERF-12).
- **Risk:** Low — routers are independent consts. Verify `AppRouter` type output unchanged (renderer compiles).
### QUAL-02 — Merge _run_single_agent twins ⚠️ REVIEW · High
- **File:** `api/app/core/deep_agent.py:873-1010` vs `:1013-1166` (~90% identical)
- **Fix:** Keep only the streaming generator; non-stream variant = join the tokens. **Preserve** the documented behavior at L1080-1100 (stream version intentionally avoids a second LLM call — past bug) as the single canonical path.
- **Risk:** Every chat path runs through this. Run full agent test suites; manual chat smoke.
### QUAL-03 — One LLM tool loop, four implementations ⚠️ REVIEW · High
- **Files:** `deep_agent.py:873,1013` · `scout_runner.py:222-324` · `scout_setup.py:237-349`
- **Fix:** New `api/app/core/tool_loop.py`: `async def run_tool_loop(llm, tools, messages, *, max_steps, lf_name, on_token=None)` yielding events. Four call sites become configuration. Use `contextlib.ExitStack` for Langfuse observations (kills the manual `__enter__`/`__exit__`). Parametrize observation names per site. Do AFTER QUAL-02 (reduces to three sites).
- **Risk:** Subtle per-site differences (Langfuse naming, error handling) — diff behavior carefully; PERF-07's gather lands here once.
### QUAL-04 — tRPC CRUD factory ⚠️ REVIEW · High
- **File:** `adiuvAI/src/main/router/index.ts` (8 tables repeat list/get/create/update/delete + `if (input.x !== undefined)` set-builders)
- **Fix:** `router/crud-factory.ts`: `makeCrudRouter(table, { createSchema, updateSchema, defaultOrder })`; plus `pickDefined(input, keys)` helper for update-set building. Prototype on `clients` first; domain procedures spread in.
- **Risk:** Generic factories can degrade tRPC type inference (`AppRouter` must stay exact for the renderer). If inference breaks, keep per-table routers and extract only `pickDefined` + create/update helpers.
### QUAL-05 — Split deep_agent.py (1329 LOC) · High
- **Fix:** (a) `app/core/prompt_context.py` ← `_*_injection` functions, `_request_context_block`, `format_folder_manifest`, `_prepare_context` (L43-519); (b) `app/core/agent_tools.py` ← `_memory_tools` (684-826), `_read_only_memory_tools`, `_brief_research_tools`, `_all_tools_for_user`, `_contextual_tools`, `get_page_details` (521-871); (c) `deep_agent.py` keeps runner + `run_*` entries. Pure moves; do after QUAL-02/03.
### QUAL-06 — backend-client.ts split + dispatch extract ⚠️ REVIEW · Medium
- **Fix:** (a) `backend-errors.ts` ← 5 error classes (L134-203); (b) extract `private dispatchFrame(frame)` + per-frame handlers from the 230-line `openDeviceWebSocket` (L876-1106); (c) `sendStreamFrame(frameType, payload, callbacks)` helper deduping the six `send*` methods (L276-660); (d) move `recordRunAction` (L55, SQLite write) to `src/main/scouts/scout-run-log.ts`. Full split into connection/requests modules optional second step.
- **Risk:** Shared mutable state (`streamListeners`, `persistentWs`) — extract as methods first, modules later. Do after CORR-07's guards land.
### QUAL-07 — scout_runner.py split + twin dedupe ⚠️ REVIEW · Medium
- **Fix:** `app/core/scouts/{local_runner.py,cloud_runner.py,runner_common.py}`; extract `_guard_device_online()` and `_process_items(items, process_one) -> RunStats` shared by the 210-line twins (`run_local_agent` 556-766 / `run_cloud_agent` 774-983).
- **Risk:** Per-item error semantics differ slightly between local/cloud — write characterization tests first (suites exist).
### QUAL-08 — device_ws.py handlers → package · Medium
- **Fix:** Keep `device_ws.py` as protocol layer (endpoint, `_message_loop`, `_heartbeat_loop`, `_mark_runs_disconnected`); move the 9 `_handle_*` to `app/api/ws_handlers/{chat,brief,journey,indexing}.py`. Handlers already take `(websocket, user_id, payload)`. Do with QUAL-12.
### QUAL-09 — ProjectSidebar.tsx (1292 LOC, 25+ useState) · Medium
- **Fix:** Extract `ProjectEditDialog.tsx`, `ClientManageDialogs.tsx`, `NewProjectDialog.tsx`, and a shared `ClientSelect` (the client+sub-client+create-inline combo duplicated between edit and new-project dialogs with parallel state pairs). Pure UI extraction.
### QUAL-10 — Ownership-check dependency · Medium (mechanical)
- **File:** `api/app/api/routes/scouts.py:344,372,391,407,702` (pattern ×6)
- **Fix:** `async def get_owned_cloud_scout(scout_id: UUID, current_user=Depends(get_current_user), db=Depends(get_session)) -> CloudScoutConfig` raising 404; use as route param. Same pattern available for other resource routes.
### QUAL-11 — Standardize tRPC error contract ⚠️ REVIEW · Medium
- **File:** `adiuvAI/src/main/router/index.ts` — three styles: `{error}` (L109,113), `{success:true}`, `{data,error}` envelopes (1157-1232), zero `TRPCError`.
- **Fix:** Standardize on throwing `TRPCError` (ipcLink propagates, `ipcLink.ts:76`); migrate procedure-by-procedure WITH their renderer consumers (each `useQuery/useMutation` error handling). Coordinates with CORR-04/CORR-19. Do during QUAL-01 split.
- **Risk:** Every consumer touched — easy to miss one. Grep all `\.error` accesses on tRPC results.
### QUAL-12 — Typed WS frames server-side · Medium
- **File:** `api/app/api/routes/device_ws.py:424,620,704,767-820` (raw `json.dumps` dicts) vs pydantic frames elsewhere; 7× `# type: ignore[union-attr]` (L293-294,382-383,491,567-568)
- **Fix:** Define pydantic models for index/journey reply frames in `app/schemas`; use a discriminated union (`Field(discriminator="type")`) for inbound frames, `model_validate` at dispatch, error frame on ValidationError. Kills the type:ignores and the hand-rolled snake/camel fallbacks (L673-676,746-750).
### QUAL-13 — Casing contract at WS boundary · Medium
- **Evidence:** `api/app/agents/note_agent.py:25` reads `row.get("aiSummary") or row.get("ai_summary")` — convention untrusted.
- **Fix:** Document the contract ("frames snake_case, tool-result rows camelCase") in `api/app/core/ws_context.py` docstring + `adiuvAI/src/shared/casing.ts`; delete the dual-read hedges; add a TypedDict for rows (TYPE-07).
### QUAL-14 — OAuth blocks → own files; shared TTLStateStore · Medium
- **Fix:** `app/api/routes/oauth.py` ← auth.py L45-67+299-505 (incl. extracting `link_or_create_oauth_user()` from the 124-line `oauth_callback`); `app/api/routes/scout_gmail_oauth.py` ← scouts.py L456-807. One `app/auth/state_store.py` `TTLStateStore` replacing both `_pending_states` dicts — the seam where SEC-18's Redis backend plugs in. Sequence: build TTLStateStore during SEC-18, move files in Phase 6.
### QUAL-15 — Agent helpers dedupe · Low (mechanical)
- **Fix:** `api/app/agents/_common.py` with `is_uuid()` (copies at task_agent.py:14-20, note_agent.py:15-22) and `format_rows(rows, line_fn, empty_msg)`.
---
# 7. Type Safety
### TYPE-01 — Broken relative imports, types silently `any` · High (mechanical)
- **Files:** `adiuvAI/src/renderer/components/ai/blocks/ChatEntityBlock.tsx:13`, `ChatChartBlock.tsx:27`, `ChatTimelineBlock.tsx:5`
- **Problem:** `'../../../../../shared/api-types'` (5 levels = repo root) — one `../` too many. `import type` so esbuild strips it without resolving; tsc confirms TS2307. Block data types are `any` throughout.
- **Fix:** Remove one `../` in each of the three imports (→ `'../../../../shared/api-types'`).
### TYPE-02 — No Python type checker · High ⚠️ REVIEW (config choices)
- **Fix:** Add `api/pyproject.toml` with `[tool.mypy]`: start `check_untyped_defs = true`, `warn_return_any = true`; per-module strict for `app/core`, `app/api`. Wire into CI beside `ruff check`. First run will surface real bugs — budget a cleanup pass.
### TYPE-03 — tsc doesn't pass; no CI typecheck · Medium
- **Evidence:** pre-existing errors at `backend-client.ts:963`, `drizzle-executor.ts:367`, `OnboardingFlow.tsx:157/165/291` (+ TYPE-01 cascade).
- **Fix:** After TYPE-01, fix the remaining tsc errors (inspect each — may be real bugs), then add `"typecheck": "tsc --noEmit"` script + CI step.
### TYPE-04 — RequestContext model for deep_agent · Medium
- **File:** `api/app/core/deep_agent.py` (19× `dict[str, Any]`; `context` threaded through L43-592+)
- **Fix:** `RequestContext` TypedDict/pydantic model in `app/schemas/` (trace_id, session_id, format_prefs, proactive hints, relational memory, …); `_trace_id_from_context`/`_session_id_from_context` (L574-590) become attribute access and get deleted. Do with QUAL-05.
### TYPE-05 — drizzle-executor cast cluster · Medium (mechanical)
- **File:** `adiuvAI/src/main/api/drizzle-executor.ts:73,159,262,317,335` (table-as-Record ×5), `:230,249` (`Boolean as unknown as (x) => x is SQL` — dead weight, conditions already `SQL[]`)
- **Fix:** One helper using drizzle's `getTableColumns()`: `getColumn(table, name): SQLiteColumn | undefined`; replace the record casts. Delete the Boolean predicate casts (`and(...conditions)` suffices).
### TYPE-06 — window.electronAI escapes · Low (mechanical)
- **Files:** `ContextualChatContext.tsx:148-150`, `CloudScoutConfigPanel.tsx:39`, `CloudScoutCreationFlow.tsx:47`, `httpLink.ts:17`
- **Fix:** `src/renderer/types/electron-api.d.ts` with `declare global { interface Window { electronAI?: ElectronAI; electronTRPC?: …; electronDialog?: … } }`; add `"types": ["vite/client"]` to tsconfig (fixes `(import.meta as any).env`).
### TYPE-07 — Boundary typing misc · Low
- `ToolResult` TypedDict in `api/app/core/ws_context.py` (`rows`, `row`, `error`) — types the most-trafficked boundary; pairs with QUAL-13.
- 11 missing return annotations: `api/app/api/routes/scouts.py` cloud CRUD handlers (302, 313, 337, 366, 758), `device_ws.get_session_buffer:321` — add once TYPE-02's checker exists.
- `Provider` Protocol in `app/integrations/__init__.py` for `fetch_messages`/`fetch_emails` (kills 2 type:ignores in scout_runner.py:851+).
- `adiuvAI/src/main/ipc.ts:55` `(response: any)` → type the tRPC response envelope; `SerializedTRPCResponse` in `src/shared/` closes the ipcLink seam (`ipcLink.ts:66,76`).
---
## Notes for the Executor
1. **Line numbers reference `main` @ `315c5d0`.** Re-verify each quoted snippet exists before editing; if it moved, find it by content, not line.
2. **Never execute ⚠️ REVIEW items unattended.** Security fixes especially: a sanitizer with a bypass or a broken auth check is worse than the original bug.
3. **After each Phase, run:** `cd api && ruff check . && pytest` · `cd adiuvAI && npm run lint && npx tsc --noEmit && npm run knip` (tsc becomes meaningful after TYPE-01/TYPE-03). There is no Electron test suite — manual smoke for renderer-affecting changes.
4. **Two-repo changes** (SEC-13, PERF-17, CORR-12, QUAL-13) must land backward-compatibly: backend accepts both old and new shapes for one release.
5. **Run `graphify update .` after each modifying session** (project rule).
6. Items needing **owner decisions before any execution:** SEC-16, SEC-22, SEC-28, DEAD-04, DEAD-07, CORR-08 (approach choice), DEPS-04 (tool choice).

203
docs/REFACTOR_PROGRESS.md Normal file
View File

@@ -0,0 +1,203 @@
# Refactor Execution Progress
Companion to [REFACTOR_PLAN.md](REFACTOR_PLAN.md). The plan is **read-only**: never edit it. All progress, deviations, and lessons go HERE.
## Rules for the executing model
1. Before starting an item: set its Status to `in-progress`.
2. After finishing: set Status (`done` / `blocked` / `needs-review` / `skipped`), fill Commit with the short SHA, add Notes if anything was non-obvious.
3. **⚠️ items:** implement on the branch, set Status to `needs-review`, do NOT merge. A human or stronger model reviews the diff.
4. If the code does not match the plan's description (moved, already fixed, different shape): set `blocked`, write what you found in Notes, move to the next item. Do not improvise.
5. If you learn something that affects later items (e.g. "FK cascade route chosen in CORR-05, so DEAD-11's helper is gone"), add it to **Lessons Learned** at the bottom — next session reads it before starting.
6. One commit per item: `<ID>: <one-line description>`.
7. End of session: append a row to **Session Log**.
**Status values:** `pending` · `in-progress` · `done` · `needs-review` (⚠️ implemented, awaiting human) · `blocked` (mismatch/failed, see Notes) · `skipped` (owner decision) · `n/a`
## Owner decisions required (blocks the marked items)
| Decision | Blocks | Roberto's answer |
|---|---|---|
| Encrypt relational labels vs document + KEK only | SEC-16 | _pending_ |
| Web SPA token: httpOnly cookie vs in-memory vs defer | SEC-22 | _pending_ |
| Local at-rest encryption: SQLCipher / OS FDE / backup-key | SEC-28 | _pending_ |
| Delete chat.py HTTP routes or keep as public API | DEAD-04 | _pending_ |
| Wire backup-key.ts or delete | DEAD-07 | _pending_ |
| Decay fix: pure-function vs last_decayed_at column | CORR-08 | _pending_ |
| Python lock tool: uv vs pip-compile | DEPS-04 | _pending_ |
---
## Phase 0 — Mechanical quick wins
Branch: `refactor/phase-0` · Verify after each: `cd api && ruff check . && pytest` / `cd adiuvAI && npm run lint && npx tsc --noEmit`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| TYPE-01 | | pending | | |
| DEPS-01 | | pending | | |
| DEAD-10 | | pending | | |
| DEAD-01 | | pending | | |
| DEAD-02 | | pending | | |
| DEAD-05 | | pending | | |
| DEAD-08 | | pending | | |
| DEPS-08 | | pending | | |
| DEAD-11 | | pending | | |
| DEAD-12 | | pending | | |
| PERF-14 | | pending | | |
| PERF-18 | | pending | | |
| CORR-20 | | pending | | |
## Phase 1 — Critical & High security (ALL ⚠️ — implement, never merge unreviewed)
Branch: `refactor/phase-1-security`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| SEC-01 | ⚠️ | pending | | |
| SEC-05 | ⚠️ | pending | | |
| SEC-06 | ⚠️ | pending | | |
| SEC-07 | ⚠️ | pending | | |
| SEC-20 | ⚠️ | pending | | |
| SEC-02 | ⚠️ | pending | | |
| SEC-14 | ⚠️ | pending | | do before SEC-19 (shared startup guard) |
| SEC-19 | ⚠️ | pending | | |
| SEC-09 | ⚠️ | pending | | |
| SEC-08 | ⚠️ | pending | | deploy together with env value |
| SEC-04 | ⚠️ | pending | | |
| SEC-15 | ⚠️ | pending | | |
| SEC-03 | ⚠️ | pending | | migration design — human-led |
## Phase 2 — Critical & High correctness
Branch: `refactor/phase-2-correctness`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| CORR-01 | ⚠️ | pending | | with CORR-02, same files |
| CORR-02 | ⚠️ | pending | | |
| CORR-12 | | pending | | before CORR-04 (frame contract) |
| CORR-04 | | pending | | |
| CORR-06 | | pending | | |
| CORR-05 | ⚠️ | pending | | deletes data — human review |
| CORR-03 | | pending | | |
## Phase 3 — Medium security + remaining correctness
Branch: `refactor/phase-3`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| SEC-10 | ⚠️ | pending | | |
| SEC-11 | ⚠️ | pending | | mechanical |
| SEC-12 | ⚠️ | pending | | two-repo |
| SEC-13 | ⚠️ | pending | | two-repo, keep fallback one release |
| SEC-17 | ⚠️ | pending | | Redis intro — with SEC-18 |
| SEC-18 | ⚠️ | pending | | |
| SEC-27 | ⚠️ | pending | | |
| SEC-21 | ⚠️ | pending | | |
| SEC-23 | ⚠️ | pending | | mechanical |
| SEC-24 | ⚠️ | pending | | |
| SEC-25 | ⚠️ | pending | | needs SEC-08 first |
| SEC-26 | ⚠️ | pending | | mechanical |
| SEC-16 | ⚠️ | skipped | | awaiting owner decision |
| SEC-22 | ⚠️ | skipped | | awaiting owner decision |
| SEC-28 | ⚠️ | skipped | | awaiting owner decision |
| SEC-29 | ⚠️ | pending | | no code change — verify + close |
| SEC-30 | ⚠️ | pending | | mechanical |
| SEC-31 | ⚠️ | pending | | |
| SEC-32 | ⚠️ | pending | | |
| SEC-33 | | pending | | |
| SEC-34 | ⚠️ | pending | | infra, not repo code |
| SEC-35 | ⚠️ | pending | | mechanical |
| SEC-36 | ⚠️ | pending | | full packaged-app regression |
| CORR-07 | | pending | | |
| CORR-08 | | skipped | | awaiting owner decision |
| CORR-09 | ⚠️ | pending | | migration + dedupe |
| CORR-10 | ⚠️ | pending | | cap value judgment |
| CORR-11 | | pending | | |
| CORR-13 | ⚠️ | pending | | classify call sites |
| CORR-14 | | pending | | |
| CORR-15 | | pending | | |
| CORR-16 | ⚠️ | pending | | irreversible path — review |
| CORR-17 | | pending | | |
| CORR-18 | | pending | | |
| CORR-19 | | pending | | |
| CORR-21 | | pending | | |
## Phase 4 — Performance
Branch: `refactor/phase-4-perf`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| PERF-01 | | pending | | |
| PERF-02 | ⚠️ | pending | | strong-model territory |
| PERF-03 | | pending | | LIMIT part urgent + mechanical |
| PERF-04 | | pending | | test migration on populated dev DB |
| PERF-05 | | pending | | coordinate with CORR-09 |
| PERF-09 | | pending | | |
| PERF-10 | | pending | | |
| PERF-06 | | pending | | |
| PERF-07 | | pending | | or fold into QUAL-03 later |
| PERF-08 | | pending | | |
| PERF-11 | | pending | | |
| PERF-12 | | pending | | |
| PERF-13 | | pending | | |
| PERF-15 | | pending | | |
| PERF-16 | | pending | | |
| PERF-17 | | pending | | two-repo |
| PERF-19 | | pending | | defer OK |
| PERF-20 | | pending | | superseded if SEC-17 Redis lands |
| PERF-21 | | pending | | |
| PERF-22 | | pending | | |
## Phase 5 — Dependencies & tooling
Branch: `refactor/phase-5-deps`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| DEPS-02 | | pending | | |
| DEPS-03 | | pending | | |
| DEPS-04 | | skipped | | awaiting owner decision (tool) |
| TYPE-02 | ⚠️ | pending | | config choices |
| TYPE-03 | | pending | | after TYPE-01 |
| DEPS-05 | ⚠️ | pending | | dedicated PR |
| DEPS-06 | ⚠️ | pending | | dedicated PR, native rebuild |
| DEPS-07 | | n/a | | monitor only |
## Phase 6 — Structural refactors (LAST — invalidates plan line numbers)
Branch: `refactor/phase-6-quality`
| ID | ⚠️ | Status | Commit | Notes |
|---|---|---|---|---|
| QUAL-02 | ⚠️ | pending | | before QUAL-03 |
| QUAL-03 | ⚠️ | pending | | |
| QUAL-01 | | pending | | with QUAL-11 |
| QUAL-04 | ⚠️ | pending | | prototype on clients first |
| QUAL-05 | | pending | | |
| QUAL-06 | ⚠️ | pending | | after CORR-07 |
| QUAL-07 | ⚠️ | pending | | characterization tests first |
| QUAL-08 | | pending | | with QUAL-12 |
| QUAL-09 | | pending | | |
| QUAL-10 | | pending | | |
| QUAL-11 | ⚠️ | pending | | |
| QUAL-12 | | pending | | |
| QUAL-13 | | pending | | |
| QUAL-14 | | pending | | store built in SEC-18; file moves here |
| QUAL-15 | | pending | | |
| TYPE-04 | | pending | | with QUAL-05 |
| TYPE-05 | | pending | | |
| TYPE-06 | | pending | | |
| TYPE-07 | | pending | | |
| DEAD-04 | ⚠️ | skipped | | awaiting owner decision |
| DEAD-07 | ⚠️ | skipped | | awaiting owner decision |
---
## Lessons Learned
_Append findings that affect later items. Format: `- [ID] lesson`_
(none yet)
## Session Log
| Date | Model | Phase | Items touched | Outcome |
|---|---|---|---|---|
| | | | | |

418
docs/llm-provider-report.md Normal file
View File

@@ -0,0 +1,418 @@
# Report Provider LLM per adiuvAI — Aprile 2026
> Analisi comparativa dei provider per i **11 agenti AI** configurati in `api/.env.example`. Selezione ottimizzata per costo, qualità, latenza e privacy dei dati, con **due mapping distinti**: Production (Zero Data Retention obbligatorio) e Development (cost-efficient).
---
## Architettura — Gli 11 Agenti
Ogni variabile `LLM_MODEL_*` in `api/.env.example` controlla un agente con profilo d'uso specifico. Analisi dei requisiti emersa dall'ispezione del codice in `api/app/agents/`, `api/app/core/` e `api/app/memory/`.
| # | Env Var | Agente | Tool Calling | Latenza | Qualità | Volume/utente | Real-time? |
|---|---------|--------|--------------|---------|---------|--------------|------------|
| 1 | `LLM_MODEL_CLASSIFIER` | **Intent Classifier** — smista i messaggi del floating panel verso task/project/note/timeline | No, output JSON | Alta (<200 ms) | Bassa (output deterministico) | Alto | |
| 2 | `LLM_MODEL_HOME_AGENT` | **Home Agent** chat principale con tutti i tool (CRUD task/project/note) | Multi-turno (≤6 step) | Alta (<3 s perceived) | Alta (user-facing) | Alto | , WS stream |
| 3 | `LLM_MODEL_FLOATING_AGENT` | **Floating Agent** chat contestuale da task/project/note | Multi-turno (≤6 step) | Alta | Mediaalta | Alto | , WS stream |
| 4 | `LLM_MODEL_UNIFIED_PROCESSOR` | **Unified Processor** processa file del filesystem locale | Tool loop (≤12 step) | Bassa (batch) | Media | Medio/occasionale | Background |
| 5 | `LLM_MODEL_CLOUD_PROCESSOR` | **Cloud Processor** fetch e processing di Gmail/Teams/Outlook | Tool loop (≤12 step) | Bassa | Media | Schedulato | Background |
| 6 | `LLM_MODEL_BRIEF_AGENT` | **Brief Agent** daily brief home + project (streaming, read-only tool) | Singolo step, tool read-only | Alta (<4 s) | Alta (prosa curata) | Medio | |
| 7 | `LLM_MODEL_SETUP_AGENT` | **Setup Agent** journey conversazionale per costruire `AgentConfig` JSON | Multi-turno (≤15) | Media | Alta (UX critica) | Basso (una tantum) | , WS |
| 8 | `LLM_MODEL_MEMORY_EXTRACTOR` | **Memory Extractor** pipeline Mem0 extract+decide (2 call/turno) | No, JSON strutturato | Bassa (off-path) | Bassa (filtrato a valle) | Alto (ogni turno chat) | Background |
| 9 | `LLM_MODEL_MEMORY_MINER` | **Memory Miner** pattern mining orario su storia episodica (Power+) | No | Bassa | Media | Orario (Power+) | Cron |
| 10 | `LLM_MODEL_MEMORY_AUDITOR` | **Memory Auditor** audit settimanale: contraddizioni + canonicalizzazione relazioni | No, reasoning su fatti | Bassa | Alta (richiede reasoning) | Settimanale | Cron |
| 11 | `LLM_EMBED_MODEL` | **Embeddings** vettori 1536-dim per ricerca semantica (LanceDB/Qdrant) | | Media | Deterministico | Alto | In-request |
> **Nota architetturale (Processors):** Il Batch API dei provider LLM **non è utilizzabile** per Unified e Cloud Processor: il loop tool-calling richiede risultati sincroni dal client Electron via WebSocket. Si usa **API Standard** a prezzi di listino.
---
## Conformità — Policy Privacy dei Provider
Per Production è richiesto un **strict Zero Data Retention** (ZDR): nessuna conservazione prompt/response, nessun logging, nessun uso per training garantito contrattualmente. Per Development è sufficiente l'opt-out di default dal training.
| Provider | Sede | ZDR strict (prod) | Opt-out training (dev) | Note |
|----------|------|:-----------------:|:----------------------:|------|
| 🇺🇸 OpenAI | USA | con **ZDR addendum Enterprise** | default API | Standard API: 30gg retention logs |
| 🇺🇸 Anthropic | USA | con **Enterprise ZDR** | default API | 30gg retention su standard tier |
| 🇺🇸 Google Vertex AI | USA | **contrattuale** (Vertex, non AI Studio) | paid tier | Free AI Studio usa dati per training |
| 🇫🇷 Mistral | Francia (EU) | **ZDR disponibile** | default | GDPR-native, ottimo per EU residency |
| 🇺🇸 Groq | USA | **via DPA dedicato** | default | Cloud inference Llama/Qwen |
| 🇺🇸 Cerebras | USA | **nessuna conservazione by default** | | ZDR out-of-the-box il più rigoroso |
| 🇺🇸 Voyage AI | USA | ZDR enterprise | | Embeddings only |
| 🇨🇳 DeepSeek | Cina | | opt-out limitato, dati in Cina | **Solo Dev, con dati sintetici** |
| 🇨🇳 Zhipu (GLM) | Cina | | non verificabile | **Solo Dev** |
---
## Confronto Modelli — Miglior Modello per Agente
Prezzi in USD per milione di token (MTok), aggiornati Aprile 2026.
### 1. Intent Classifier
*Output JSON deterministico, latenza critica, volume alto*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| OpenAI | `GPT-4.1 Nano` | $0.10 | $0.40 | ent | Veloce, JSON mode affidabile |
| **Google Vertex** | `Gemini 2.5 Flash-Lite` | **$0.10** | **$0.40** | | **Migliore prezzo+ZDR+latenza** |
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ent | Overkill per pura classificazione |
| Groq | `Llama 3.1 8B` | $0.05 | $0.08 | DPA | Economico, 840 TPS |
| Cerebras | `Llama 3.1 8B` | $0.10 | $0.10 | | ZDR by default, velocissimo |
### 2. Home Agent
*Multi-turno + tool calling completo + streaming*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **Anthropic** | `Claude Sonnet 4.6` | $3.00 | $15.00 | ent | **Top tool use**, caching -90%, 1M ctx |
| OpenAI | `GPT-4.1` | $2.00 | $8.00 | ent | Solido, JSON mode, 1M ctx |
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | | Miglior rapporto Q/P per prod |
| Mistral | `Mistral Medium 3` | $1.00 | $3.00 | | EU residency |
| Groq | `Llama 3.3 70B` | $0.59 | $0.79 | DPA | Tool calling inferiore ai proprietari |
### 3. Floating Agent
*Single+multi-turno, contestuale, più compatto del Home*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| OpenAI | `GPT-4.1 Mini` | $0.40 | $1.60 | ent | Bilanciato |
| **Anthropic** | `Claude Haiku 4.5` | $1.00 | $5.00 | ent | **Tool use affidabile, bassa latenza** |
| Google Vertex | `Gemini 2.5 Flash-Lite` | $0.10 | $0.40 | | Economico; qualità tool inferiore a Haiku |
| Mistral | `Mistral Small 3.1` | $0.20 | $0.60 | | EU |
### 4. Unified Processor (locale)
*Batch, multi-turno (≤12), qualità estrazione importa*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ent | **Tool loop affidabile, costo contenuto** |
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ent | Qualità top se budget permette |
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | | Valida alternativa, input economico |
| Mistral | `Mistral Large 3` | $2.00 | $6.00 | | EU residency |
### 5. Cloud Processor (Gmail/Teams/Outlook)
*Batch, multi-turno (≤12), dati sensibili*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ent | **Robusto su email parsing** |
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ent | Miglior reasoning su thread email |
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | | 1M context utile per thread lunghi |
### 6. Brief Agent (daily brief)
*Singolo step, prosa curata, read-only tool*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ent | **Prosa di qualità, streaming affidabile** |
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ent | Prosa eccellente, più costoso |
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | | Alternativa economica |
| Mistral | `Mistral Small 3.1` | $0.20 | $0.60 | | Economico con EU residency |
### 7. Setup Agent (journey di configurazione)
*Conversazione multi-turno (≤15), JSON finale, UX critica*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **Anthropic** | `Claude Sonnet 4.6` | **$3.00** | **$15.00** | ent | **Miglior instruction-following, JSON affidabile** |
| OpenAI | `GPT-4.1` | $2.00 | $8.00 | ent | Eccellente bilanciamento |
| Google Vertex | `Gemini 2.5 Pro` | $1.25 | $10.00 | | Reasoning solido |
> Volume bassissimo (≈2 sessioni/mese per utente): il costo è trascurabile anche col modello premium.
### 8. Memory Extractor (Mem0 extract+decide)
*2 call/turno, JSON strutturato, deterministico, off request-path*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1 Nano` | **$0.10** | **$0.40** | ent | **Cheapest OpenAI, JSON mode affidabile** |
| Google Vertex | `Gemini 2.5 Flash-Lite` | $0.10 | $0.40 | | Pari prezzo, valido |
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ent | Troppo costoso per volume alto |
### 9. Memory Miner (cron orario, Power+)
*Pattern mining su episodi, input medio, occasionale*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1 Mini` | **$0.40** | **$1.60** | ent | **Reasoning sufficiente per pattern detection** |
| Google Vertex | `Gemini 2.5 Flash` | $0.30 | $2.50 | | Buona alternativa |
| Anthropic | `Claude Haiku 4.5` | $1.00 | $5.00 | ent | Qualità superiore, costo +2x |
### 10. Memory Auditor (cron settimanale)
*Reasoning per contraddizioni + canonicalizzazione, rarissimo*
| Provider | Modello | In $/MTok | Out $/MTok | ZDR | Note |
|----------|---------|----------|-----------|:---:|------|
| **OpenAI** | `GPT-4.1` | **$2.00** | **$8.00** | ent | **Reasoning robusto, volume trascurabile** |
| Anthropic | `Claude Sonnet 4.6` | $3.00 | $15.00 | ent | Alternativa premium |
| Google Vertex | `Gemini 2.5 Pro` | $1.25 | $10.00 | | Ottimo reasoning |
### 11. Embeddings
*Semantic search, 1536-dim, volume alto*
| Provider | Modello | $/MTok | Dim | ZDR | Note |
|----------|---------|--------|-----|:---:|------|
| **OpenAI** | `text-embedding-3-small` | **$0.02** | 1536 | ent | **Standard de facto, già in uso (LanceDB 1536-dim)** |
| Voyage AI | `voyage-3.5-lite` | $0.02 | 1024 | | Qualità superiore ma richiede reindex |
| Google Vertex | `Gemini Embedding` | $0.15 | variabile | | 7.5x più costoso, nessun vantaggio |
---
## 🔒 Mapping Production (Zero Data Retention obbligatorio)
Tutti i provider selezionati hanno ZDR contrattualmente garantito. Prevalgono qualità e affidabilità; il costo è ottimizzato entro il vincolo ZDR.
| # | Agente | Provider | Modello | Razionale |
|---|--------|----------|---------|-----------|
| 1 | Classifier | **OpenAI** | `gpt-4.1-nano` | $0.10/$0.40, JSON mode affidabile, stesso contratto ZDR del resto OpenAI |
| 2 | Home Agent | **Anthropic** | `claude-sonnet-4-6` | Miglior tool calling del mercato; caching -90% riduce il costo; esperienza chat premium |
| 3 | Floating Agent | **Anthropic** | `claude-haiku-4-5` | Tool calling affidabile + bassa latenza; qualità coerente con Home Agent |
| 4 | Unified Processor | **OpenAI** | `gpt-4.1-mini` | Tool loop affidabile a costo contenuto; critico visto che il loop moltiplica le call |
| 5 | Cloud Processor | **OpenAI** | `gpt-4.1-mini` | Stesso profilo del locale; parsing di email/chat consolidato |
| 6 | Brief Agent | **OpenAI** | `gpt-4.1-mini` | Prosa curata, streaming, read-only tools ottimo bilanciamento |
| 7 | Setup Agent | **Anthropic** | `claude-sonnet-4-6` | Journey conversazionale critica per UX; volume bassissimo giustifica il premium |
| 8 | Memory Extractor | **OpenAI** | `gpt-4.1-nano` | 2 call per turno chat: servono i prezzi più bassi con JSON mode |
| 9 | Memory Miner | **OpenAI** | `gpt-4.1-mini` | Cron orario su Power+: reasoning sufficiente, costo contenuto |
| 10 | Memory Auditor | **OpenAI** | `gpt-4.1` | Reasoning più avanzato per contraddizioni; frequenza settimanale = costo trascurabile |
| 11 | Embeddings | **OpenAI** | `text-embedding-3-small` | Già in uso, 1536-dim compatibile con schema LanceDB/Qdrant |
### Valori `.env` — Production
```bash
# Default fallback
LLM_MODEL=gpt-4.1-mini
LLM_EMBED_MODEL=text-embedding-3-small
# Per-agent overrides (LiteLLM model IDs)
LLM_MODEL_CLASSIFIER=gpt-4.1-nano
LLM_MODEL_HOME_AGENT=anthropic/claude-sonnet-4-6
LLM_MODEL_FLOATING_AGENT=anthropic/claude-haiku-4-5
LLM_MODEL_UNIFIED_PROCESSOR=gpt-4.1-mini
LLM_MODEL_CLOUD_PROCESSOR=gpt-4.1-mini
LLM_MODEL_BRIEF_AGENT=gpt-4.1-mini
LLM_MODEL_SETUP_AGENT=anthropic/claude-sonnet-4-6
LLM_MODEL_MEMORY_EXTRACTOR=gpt-4.1-nano
LLM_MODEL_MEMORY_MINER=gpt-4.1-mini
LLM_MODEL_MEMORY_AUDITOR=gpt-4.1
```
> **2 API key richieste**: OpenAI (Enterprise + ZDR addendum) e Anthropic (Commercial + ZDR addendum). Vedi sezione **[Come attivare ZDR](#come-attivare-zdr-con-openai-e-anthropic)** per la procedura contrattuale.
---
## 💰 Mapping Development (cost-efficient, ZDR non richiesto)
Priorità: costo minimo e velocità di iterazione. **Niente dati utente reali in questo ambiente** solo dati sintetici o mock. Nessun vincolo ZDR consente di includere Groq, Cerebras e opzionali DeepSeek.
| # | Agente | Provider | Modello | Razionale |
|---|--------|----------|---------|-----------|
| 1 | Classifier | **Groq** | `llama-3.1-8b-instant` | $0.05/$0.08: il più economico con 840 TPS |
| 2 | Home Agent | **Google AI Studio** | `gemini-2.5-flash` | 67x meno di Sonnet, tool use nativo, 1M ctx |
| 3 | Floating Agent | **Google AI Studio** | `gemini-2.5-flash-lite` | $0.10/$0.40 sufficiente per single-turn |
| 4 | Unified Processor | **Google AI Studio** | `gemini-2.5-flash` | Tool loop funzionante a costo minimo |
| 5 | Cloud Processor | **DeepSeek** | `deepseek-chat` ($0.28/$0.42) | Costo minimo per batch con dati sintetici |
| 6 | Brief Agent | **Groq** | `llama-3.1-8b-instant` | $0.05/$0.08, prosa accettabile per QA |
| 7 | Setup Agent | **Google AI Studio** | `gemini-2.5-flash` | Conversazione decente a costo minimo |
| 8 | Memory Extractor | **Groq** | `llama-3.1-8b-instant` | JSON extraction funziona con fallback retry |
| 9 | Memory Miner | **Groq** | `llama-3.3-70b-versatile` | Pattern mining richiede reasoning; 70B a $0.59/$0.79 |
| 10 | Memory Auditor | **Google AI Studio** | `gemini-2.5-flash` | Reasoning accettabile, quasi gratis a scala dev |
| 11 | Embeddings | **OpenAI** | `text-embedding-3-small` | Stesso dim del prod (1536) evita reindex al promote |
### Valori `.env` — Development
```bash
# Default fallback
LLM_MODEL=gemini/gemini-2.5-flash
LLM_EMBED_MODEL=text-embedding-3-small
# Per-agent overrides
LLM_MODEL_CLASSIFIER=groq/llama-3.1-8b-instant
LLM_MODEL_HOME_AGENT=gemini/gemini-2.5-flash
LLM_MODEL_FLOATING_AGENT=gemini/gemini-2.5-flash-lite
LLM_MODEL_UNIFIED_PROCESSOR=gemini/gemini-2.5-flash
LLM_MODEL_CLOUD_PROCESSOR=deepseek/deepseek-chat
LLM_MODEL_BRIEF_AGENT=groq/llama-3.1-8b-instant
LLM_MODEL_SETUP_AGENT=gemini/gemini-2.5-flash
LLM_MODEL_MEMORY_EXTRACTOR=groq/llama-3.1-8b-instant
LLM_MODEL_MEMORY_MINER=groq/llama-3.3-70b-versatile
LLM_MODEL_MEMORY_AUDITOR=gemini/gemini-2.5-flash
```
> ⚠️ **Non immettere dati utente reali**. Gli embeddings restano `text-embedding-3-small` per non dover reindicizzare passando in Production (stesso schema 1536-dim).
---
## Simulazione — Costo Mensile per Utente
Utilizzo tipico: 500 Home, 300 Floating, 210 Brief, 100 Unified Processor, 80 Cloud Processor, 10 Setup turn (≈2 sessioni), 1500 Memory Extractor turn, 720 Miner (30gg × 24h Power+), 4 Auditor, 1000 embeddings.
### Production
| Agente | Modello | In tok | Out tok | $/mese |
|--------|---------|--------|---------|--------|
| Classifier | GPT-4.1 Nano | 150K | 30K | $0.027 |
| Home Agent | Sonnet 4.6 | 1M | 500K | $10.50 |
| Floating | Haiku 4.5 | 150K | 90K | $0.60 |
| Unified Processor | GPT-4.1 Mini | 300K | 200K | $0.44 |
| Cloud Processor | GPT-4.1 Mini | 240K | 160K | $0.35 |
| Brief Agent | GPT-4.1 Mini | 315K | 105K | $0.29 |
| Setup Agent | Sonnet 4.6 | 40K | 5K | $0.20 |
| Memory Extractor | GPT-4.1 Nano | 750K | 150K | $0.14 |
| Memory Miner | GPT-4.1 Mini | 1.4M | 150K | $0.80 |
| Memory Auditor | GPT-4.1 | 20K | 5K | $0.08 |
| Embeddings | text-embedding-3-small | 500K | | $0.01 |
| | | | **Totale Production** | **~$13.48/utente/mese** |
> Con prompt caching Anthropic al 90% sui system prompt ripetuti, Home Agent scende a ~$45/mese → totale **~$78/utente/mese**.
### Development (dev team ≈ 100 sessioni test/mese totali, non per utente)
| Agente | Modello | $/mese totali |
|--------|---------|--------------|
| Classifier | Groq Llama 3.1 8B | $0.004 |
| Home Agent | Gemini 2.5 Flash | $0.58 |
| Floating | Gemini 2.5 Flash-Lite | $0.04 |
| Unified Processor | Gemini 2.5 Flash | $0.09 |
| Cloud Processor | DeepSeek Chat | $0.13 |
| Brief Agent | Groq Llama 3.1 8B | $0.02 |
| Setup Agent | Gemini 2.5 Flash | $0.02 |
| Memory Extractor | Groq Llama 3.1 8B | $0.05 |
| Memory Miner | Groq Llama 3.3 70B | $0.35 |
| Memory Auditor | Gemini 2.5 Flash | $0.02 |
| Embeddings | text-embedding-3-small | $0.01 |
| | **Totale Dev team** | **~$1.35/mese** |
---
## Motivazioni — Decisioni Chiave
### 🔒 Perché questa separazione Production/Development
I dati utente di adiuvAI sono E2E-encrypted, ma i prompt agentici contengono metadati operativi (titoli task, nomi progetti, contesto chat) che fluiscono in chiaro verso il LLM provider. Per Production, ZDR contrattuale è non-negoziabile. In Development si usano dati sintetici, quindi i provider più economici senza garanzie ZDR sono perfetti per iterare rapidamente senza bruciare budget.
### 💬 Claude Sonnet 4.6 per Home Agent (prod) vs Gemini Flash (dev)
Home Agent è il touchpoint principale: la qualità del tool calling determina la percezione del prodotto. Sonnet 4.6 è il benchmark su tool use. Il caching di Anthropic (-90% sui system prompt) rende il costo sostenibile a scala. In dev, Gemini 2.5 Flash costa **20x meno** con tool calling sufficiente per validare flussi, test di regressione e UI.
### ⚙️ GPT-4.1 Mini per entrambi i Processor
Unified e Cloud Processor sono l'unica superficie dove il **tool loop moltiplica il costo** (≤12 turni per file). Il prezzo medio deve essere basso **e** la qualità tool calling alta, altrimenti errori in cascata. GPT-4.1 Mini è lo sweet spot: tool calling OpenAI è robusto, il prezzo è 5x inferiore a Sonnet. Si sconsiglia Groq qui: la qualità tool calling di Llama introduce retry che annullano il risparmio.
### 🧠 Stratificazione Memory Agents
- **Extractor** (2 call/turno, volume altissimo) Nano (cheapest)
- **Miner** (orario Power+, reasoning su pattern) Mini (compromesso)
- **Auditor** (settimanale, reasoning avanzato) GPT-4.1 full (il volume azzera il premium)
Ogni tier di memory ha un profilo costo/qualità diverso: collassarli tutti su un unico modello spreca o sul basso (Auditor poco accurato) o sull'alto (Extractor 10x più caro del necessario).
### 🇪🇺 Perché non Mistral in prod di default
Mistral è un'ottima alternativa EU-residency, ma Sonnet 4.6 e GPT-4.1 hanno tool calling ancora superiore nei benchmark di Aprile 2026. Se la priorità diventa **data residency EU** (clienti enterprise europei, GDPR stretto), raccomando uno switch mirato:
- Home Agent `mistral/mistral-medium-3`
- Background processor `mistral/mistral-large-3`
### 🚫 Cina esclusa da Production
DeepSeek e GLM offrono costi imbattibili ma i dati risiedono in Cina senza garanzie ZDR verificabili per utenti internazionali. **Accettabili in Development solo con dati sintetici**.
### ⚡ Groq e Cerebras come alternative budget
In Development, Groq domina per pricing + velocità (394840 TPS). In Production sono qualificati ZDR (tramite DPA) ma la qualità tool calling di Llama rimane inferiore ai modelli proprietari su flussi multi-turno complessi. Cerebras è strict ZDR by default ma il catalogo modelli è limitato.
---
## Come attivare ZDR con OpenAI e Anthropic
**Cosa significa ZDR concretamente:** nessuna conservazione di prompt/output oltre la durata della richiesta, nessun logging di contenuti da parte del provider, nessun uso per training o fine-tuning, abuse monitoring basato su metadati anziché contenuti. Sul tier API standard, invece, OpenAI e Anthropic conservano input/output per **30 giorni** a fini di abuse detection motivo per cui serve il contratto ZDR esplicito.
### 🔵 OpenAI — Enterprise Privacy / ZDR Addendum
**Chi ne ha diritto:** clienti con Enterprise Agreement. Per API a basso volume si può comunque richiedere un **Business Associate Agreement** o **Enterprise Privacy Addendum** senza passare a ChatGPT Enterprise.
**Procedura:**
1. Scrivere a **sales@openai.com** (oppure compilare il form su [openai.com/enterprise](https://openai.com/enterprise/)) indicando:
- Ragione sociale, sede legale, P.IVA, DPO/privacy contact
- Caso d'uso (per adiuvAI: "agentic SaaS con E2E-encrypted user data, API backend")
- Volume stimato mensile (token o $) utile per pricing
- Modelli usati (`gpt-4.1`, `gpt-4.1-mini`, `gpt-4.1-nano`, `text-embedding-3-small`)
- Requisito esplicito: **"Zero Data Retention (0-day retention)"** e disabilitazione abuse monitoring sul contenuto
2. OpenAI invia **Commercial Agreement + Data Processing Addendum (DPA) + Zero Data Retention Addendum**.
3. Firmare via DocuSign. Tempo medio: **24 settimane** dalla prima mail al contratto attivo.
4. OpenAI abilita ZDR a livello di **Organization ID** dell'API non serve modificare il codice, vale per tutte le chiamate future.
5. Verifica in dashboard: `platform.openai.com` Settings Organization Data Controls. Deve apparire "Zero Data Retention: Enabled".
**Costo:** l'Enterprise non ha prezzo pubblico; a bassi volumi si può ottenere senza commitment minimo (talvolta con un modesto uplift sulla tariffa API).
**Documenti utili:** [OpenAI Enterprise Privacy](https://openai.com/enterprise-privacy/), [OpenAI DPA](https://openai.com/policies/data-processing-addendum/), [OpenAI API Data Usage](https://platform.openai.com/docs/models/how-we-use-your-data).
### 🟣 Anthropic — Commercial Terms + Zero Retention Addendum
**Chi ne ha diritto:** qualsiasi cliente commerciale. Anthropic è più flessibile di OpenAI: ZDR viene concesso anche a volumi contenuti tramite un addendum al contratto standard.
**Procedura:**
1. Scrivere a **sales@anthropic.com** (oppure via il form [anthropic.com/contact-sales](https://www.anthropic.com/contact-sales)) indicando:
- Ragione sociale, sede legale, P.IVA
- Caso d'uso e modelli (`claude-sonnet-4-6`, `claude-haiku-4-5`)
- Volume stimato mensile
- Richiesta esplicita: **"Zero Retention Addendum to the Commercial Terms"**
- Se applicabile: GDPR DPA, BAA (per HIPAA)
2. Anthropic invia **Commercial Agreement + DPA + Zero Retention Addendum** (clausola dedicata).
3. Firma via DocuSign. Tempo medio: **13 settimane**.
4. Attivazione sull'**Organization** del Claude Console. Verifica in [console.anthropic.com](https://console.anthropic.com) Settings Organization Privacy.
5. Dal contratto attivo: 0-day retention di prompt/response, abuse monitoring basato solo su metadati.
**Costo:** nessun uplift in genere; il contratto ZDR è incluso nel Commercial Agreement.
**Documenti utili:** [Anthropic Privacy Policy](https://www.anthropic.com/legal/privacy), [Anthropic Commercial Terms](https://www.anthropic.com/legal/commercial-terms), [Anthropic Trust Center](https://trust.anthropic.com) per SOC 2 / ISO 27001.
### Checklist pre-firma (entrambi)
Prima di firmare verifica che il contratto copra:
- [ ] **Zero retention** esplicita (0 giorni, non "short retention" che può significare 24h o 72h)
- [ ] **No training** sui prompt/output (default API, confermare per scritto)
- [ ] **Abuse monitoring** basato su metadati, non contenuto (altrimenti il provider legge comunque i prompt)
- [ ] **Sub-processor list** consultabile (subcontractor del provider: datacenter, CDN, ecc.)
- [ ] **DPA art. 28 GDPR** firmato contestualmente (obbligatorio se processi dati di utenti EU)
- [ ] **Breach notification SLA** 72 ore (requisito GDPR)
- [ ] **Data residency** chiedere conferma region processing (US vs EU). Per adiuvAI può valere la pena pretendere routing EU se la clientela è europea
- [ ] **Audit right** possibilità di richiedere audit indipendente (rilevante per clienti enterprise propri)
### Timeline realistica end-to-end
| Fase | Durata |
|------|--------|
| Primo contatto sales + NDA | 35 giorni |
| Legal review interno contratti provider | 12 settimane |
| Negoziazione clausole sensibili (residency, audit, pricing) | 12 settimane |
| Firma DocuSign + attivazione ZDR su Org ID | 23 giorni |
| **Totale** | **47 settimane** per provider |
> 💡 **Consiglio pratico:** parti **in parallelo** con sales@openai.com e sales@anthropic.com lo stesso giorno. Il processo è indipendente e avere entrambi i contratti attivi contemporaneamente è indispensabile per il mapping proposto.
---
## Note & Fonti
Prezzi aggiornati ad Aprile 2026. Verificare sempre le pagine ufficiali prima di decisioni finali il mercato LLM cambia mensilmente.
**Fonti:**
- [OpenAI API Pricing](https://openai.com/api/pricing/)
- [OpenAI Enterprise Privacy + ZDR](https://openai.com/enterprise-privacy/)
- [Anthropic Claude Models](https://platform.claude.com/docs/en/docs/about-claude/models)
- [Anthropic ZDR](https://www.anthropic.com/legal/privacy)
- [Google Vertex AI Pricing](https://cloud.google.com/vertex-ai/pricing)
- [Google Vertex Data Governance](https://cloud.google.com/vertex-ai/docs/generative-ai/data-governance)
- [Mistral AI Pricing](https://mistral.ai/pricing)
- [Mistral Privacy Policy](https://legal.mistral.ai/terms/privacy-policy)
- [Groq On-Demand Pricing](https://groq.com/pricing)
- [Cerebras Privacy Policy](https://www.cerebras.ai/policies)
- [DeepSeek API Pricing](https://api-docs.deepseek.com/quick_start/pricing/)
- [LiteLLM Supported Models](https://docs.litellm.ai/docs/providers)
- [Best AI for Tool Calling 2026](https://llm-stats.com/leaderboards/best-ai-for-tool-calling)
- [AI Cost Board — LLM Pricing 2026](https://aicostboard.com/blog/posts/llm-api-pricing-comparison-2026)
---
*Report generato per adiuvAI · Aprile 2026 · Aggiornato per coprire tutti gli 11 agenti di `api/.env.example` con mapping separati Production/Development.*

415
docs/marketing-strategy.md Normal file
View File

@@ -0,0 +1,415 @@
# adiuvAI — Marketing Strategy & Positioning
> **Document version:** 1.1 — April 11, 2026
> **Status:** Revised (Round 1 feedback applied)
> **Changes:** Removed BYOK positioning, merged one-liner options, selected tagline E + hero C + pitch B, added Telegram & mobile app, updated all copy for "just works" philosophy.
---
## Table of Contents
1. [Market Research & Competitive Landscape](#1-market-research--competitive-landscape)
2. [Positioning Strategy](#2-positioning-strategy)
3. [Messaging Framework](#3-messaging-framework)
4. [Waitlist Landing Page Copy](#4-waitlist-landing-page-copy)
5. [Go-to-Market Recommendations](#5-go-to-market-recommendations)
---
## 1. Market Research & Competitive Landscape
### 1.1 Market Category
adiuvAI sits at the intersection of three booming categories:
| Category | Market Size (2026 est.) | Growth |
|----------|------------------------|--------|
| AI Productivity Tools | $14B+ | ~35% CAGR |
| Project Management Software | $9B+ | ~13% CAGR |
| AI Meeting/Email Assistants | $3B+ | ~40% CAGR |
The convergence of these three categories into **one AI-first personal assistant** is the core opportunity. No incumbent owns this combined space yet — they all specialize in one slice.
### 1.2 Competitive Map
<!-- ✅ REVISED — Removed BYOK column per feedback -->
| Competitor | What They Do | Price | Local-First? | Privacy Model | EU AI Act? |
|-----------|-------------|-------|:---:|---------------|:---:|
| **Motion** (usemotion.com) | AI tasks + projects + calendar + docs + meetings | $19-34/user/mo | No (cloud SaaS) | SOC2, GDPR | Not stated |
| **Reclaim.ai** | AI calendar optimizer + scheduling | Free$18/user/mo | No (cloud SaaS) | SOC2, GDPR | Not stated |
| **Granola** | AI meeting notepad (desktop) | Free$19/mo | Partial (desktop app, cloud sync) | Standard privacy policy | Not stated |
| **Superhuman** | AI email + docs + assistant suite | $25-30/user/mo | No (cloud SaaS) | Standard | Not stated |
| **Shortwave** | AI-powered email client (Gmail) | Free$25/mo | No (cloud SaaS) | CASA Tier 2 | Not stated |
| **SaneBox** | AI email filtering/triage | $7-36/mo | No (cloud SaaS) | Google Verified, audited | Not stated |
| **Microsoft Copilot** | AI across M365 suite | $30/user/mo | No (Microsoft cloud) | Enterprise compliance | Partial |
| **Notion AI** | AI inside Notion workspace | $10/mo add-on | No (cloud SaaS) | SOC2 | Not stated |
### 1.3 Key Gaps in the Market
**Gap 1: No one is local-first.** Every competitor stores your data on their servers. adiuvAI stores everything on your device, encrypted. This is a structural advantage, not a feature toggle.
**Gap 2: No one bridges email + tasks + meetings in one private workspace.** You either use Superhuman for email, Motion for tasks, and Granola for meetings — or you compromise. adiuvAI combines all three with a single AI that understands the full context.
**Gap 3: EU AI Act compliance is unclaimed territory.** The EU AI Act entered into force in 2024 and is now being enforced. No major competitor prominently advertises compliance. For European buyers (and increasingly, US companies with EU customers), this is a purchasing requirement.
<!-- ✅ REVISED — Replaced BYOK gap with "it just works" philosophy -->
**Gap 4: AI complexity is always visible.** Every competitor requires you to understand prompts, models, and configurations. adiuvAI makes AI invisible — the right model is automatically selected for optimal cost and performance. The user never thinks about AI. It just works as a personal secretary.
### 1.4 Competitor Positioning Analysis
| Competitor | Tagline | Emotional Angle | Weakness vs adiuvAI |
|-----------|---------|-----------------|---------------------|
| Motion | "Get an unfair advantage by using AI to double productivity" | Ambition, performance | Cloud-only, no privacy story, AI complexity visible |
| Reclaim | "#1 AI calendar app for work" | Optimization, control | Calendar-only, no email/meeting/task integration |
| Granola | "The AI notepad for people in back-to-back meetings" | Simplicity, focused | Meetings only, no project management |
| Superhuman | "Superpowers, everywhere you work" | Aspiration, speed | Email-centric, expensive, no local data |
| Shortwave | "Automate your email with AI" | Efficiency, automation | Email-only, Gmail-dependent |
---
## 2. Positioning Strategy
<!-- ✅ REVISED — Merged Option A + B per feedback, added "helps you complete tasks" -->
### 2.1 One-Liner
> "adiuvAI is your AI-powered personal secretary that reads your email, organizes your work, helps you complete tasks, and briefs you every morning — turning the chaos of emails, meetings, and files into a clear plan for your day, privately, on your machine."
---
<!-- ✅ REVISED — Kept Option B (Outcome-Led) per feedback -->
### 2.2 Elevator Pitch (30 seconds)
> "Imagine starting every morning with a personalized brief: here are your 5 priorities today, here's what changed overnight in your projects, and here's a follow-up email you forgot to send. That's adiuvAI — an AI secretary that runs entirely on your computer, reads your email and files, and tells you exactly what matters. It even helps you complete the work — drafting follow-ups, organizing notes, and keeping your projects on track. No cloud dependency, no data leaks, no AI complexity. Just clarity."
---
### 2.3 Positioning Statement
<!-- ✅ REVISED — Added "helps you complete your work" per feedback -->
> **For** busy professionals who lose track of what matters across email, chat, files, and meetings,
> **adiuvAI** is the **AI personal secretary**
> **that** reads your communications, organizes your work, gives you a clear daily plan, and helps you complete your tasks —
> **unlike** Motion, Superhuman, or Notion AI,
> **because** it runs entirely on your device, your data never touches a cloud server, and it's compliant with GDPR and the EU AI Act by design.
---
### 2.4 USP Hierarchy
| Rank | Feature | User Benefit | Why It Matters |
|:---:|---------|-------------|----------------|
<!-- ✅ REVISED — Removed BYOK, added Telegram + mobile app per feedback -->
| **1** | **AI personal secretary (invisible AI)** | You don't configure agents or write prompts — adiuvAI just reads your world and tells you what to do | This is the primary emotional hook. "I want something that just works." THE reason someone joins the waitlist. |
| **2** | **Daily brief + activity carousel** | Start every day knowing exactly what matters | Tangible, visualizable, demo-able. This is what you show in the landing page hero. |
| **3** | **Local-first / your data stays yours** | Full privacy without sacrificing AI intelligence | Strong differentiator in post-GDPR Europe. Increasingly important globally after repeated data breaches. |
| **4** | **Email + files + chat → tasks automatically** | Stop manually copying things from email to your task list | This is the "extraction" magic — the AI reads your email and creates tasks/notes for you. |
| **5** | **EU AI Act + GDPR compliant** | Peace of mind for European professionals and companies | Competitive moat — none of the US-based competitors advertise this. |
| **6** | **Telegram integration** | Your secretary is in your pocket, in the app you already use | Extends adiuvAI to mobile without building a full app first. Low friction, high reach. |
| **7** | **Voice assistant joins your calls** *(coming soon)* | Takes meeting notes, suggests responses, extracts action items | Future feature — creates a complete "secretary" experience. Powerful for waitlist anticipation. |
| **8** | **Mobile companion app** *(coming soon)* | Access your daily brief and tasks on the go | Expected by every user — "how do I use this on my phone?" |
---
## 3. Messaging Framework
<!-- ✅ REVISED — Selected Option E per feedback -->
### 3.1 Tagline
> **"Meet your new chief of staff."**
Positions adiuvAI as a person, not a tool. "Chief of staff" implies someone who filters information, prioritizes, briefs you, and helps you execute — exactly the secretary metaphor. More premium than "assistant."
---
<!-- ✅ REVISED — Selected Option C (Intrigue/Minimal) per feedback -->
### 3.2 Hero Copy for Waitlist Landing Page
**Headline:**
"What if AI could be your secretary?"
**Subheadline:**
Not a chatbot. Not another app. A real AI that reads your email, knows your projects, and tells you what to focus on — without ever seeing your data.
**CTA:** See how it works →
---
### 3.3 Feature-Benefit Mapping
Use these on the landing page as feature sections below the hero:
| Feature (Technical) | Benefit (User-Facing) | Landing Page Copy |
|---------------------|----------------------|-------------------|
| Daily Brief engine | Know what to focus on | **"Start every day with clarity."** Your AI secretary reviews overnight emails, due tasks, and project changes — then gives you a 2-minute briefing with today's priorities. |
| Email/file/chat extraction | Stop manual data entry | **"It reads so you don't have to."** adiuvAI scans your inbox, files, and chats. Important items become tasks, notes, or calendar events — automatically. |
| Local-first architecture | Your data never leaves | **"Private by design, not by promise."** Everything runs on your device. Your data lives in an encrypted local database. No cloud server ever sees your content. |
| EU AI Act + GDPR | Compliance without effort | **"Built for the new rules."** Fully compliant with GDPR and the EU AI Act. No data training on your content. Audit-ready from day one. |
<!-- ✅ REVISED — Removed BYOK row, added Telegram + mobile app per feedback -->
| Voice assistant *(coming soon)* | Meetings handled for you | **"It joins your calls so you can focus."** adiuvAI listens, takes notes, and suggests next steps — all in real-time during your meetings. *(Coming soon)* |
| Multi-agent orchestration | Complex tasks handled simply | **"One request, five agents working."** Behind the scenes, specialized AI agents handle tasks, projects, notes, and timelines — you just talk naturally. |
| Activity carousel | Visual daily plan | **"Swipe through your day."** A visual carousel of today's key activities. Tap to dive in, swipe to move on. Like stories for your workday. |
| Telegram integration | Your secretary in your pocket | **"Talk to your secretary on Telegram."** Send a message, get your brief, check tasks, add notes — all from the app you already have on your phone. |
| Mobile companion app *(coming soon)* | adiuvAI on the go | **"Your daily brief, wherever you are."** A lightweight mobile app to review your plan, check off tasks, and stay in sync with your desktop. *(Coming soon)* |
---
### 3.4 Objection Handling
| Objection | Response |
|-----------|----------|
| "I already use Notion/Motion/Todoist for tasks" | Those are task managers you have to maintain. adiuvAI reads your email and creates tasks for you. It's the difference between a notebook and a secretary. |
| "How is this different from ChatGPT/Copilot?" | ChatGPT answers questions. adiuvAI *watches your work* — email, files, meetings — and proactively tells you what needs attention. It's not a chatbot, it's a secretary. |
| "Can I trust AI with my email/files?" | Your data never leaves your device. adiuvAI runs locally with end-to-end encryption. All AI processing uses privacy-respecting contracts — your data is never used for training. We literally can't see your data. |
| "Is this just another AI wrapper?" | adiuvAI is a native desktop app with its own local database, vector search, and multi-agent AI system. It doesn't wrap an API — it orchestrates 5 specialized agents on your behalf. |
| "What about team collaboration?" | adiuvAI starts as your personal secretary. Team features (shared workspace, SSO) are on the roadmap for the Team tier. Join the waitlist to help shape what we build. |
| "Is it only for developers/tech people?" | Not at all. The entire design philosophy is to *hide* AI complexity. You never see prompts, models, or configurations. It just works like a smart assistant. |
<!-- ✅ REVISED — Removed BYOK references, updated AI trust answer -->
| "Why desktop and not web/mobile?" | Desktop gives us access to your local files, email client, and meetings without routing through a cloud. It's a privacy decision. Mobile app and Telegram integration are on the roadmap. |
| "EU AI Act — how are you compliant?" | Local-first architecture means no centralized data processing. We select AI models with privacy-respecting contracts — your data is never used for training. No profiling, no high-risk classification triggers. |
---
## 4. Waitlist Landing Page Copy
### 4.1 Recommended Page Structure
```
1. HERO — Headline + subheadline + email input + CTA
2. SOCIAL PROOF BAR — "Built by an AI Enterprise Solution Architect at HPE" + beta timeline
3. DAILY BRIEF DEMO — Visual mockup or animation showing the morning brief experience
4. 3 PILLARS — AI Secretary | Private by Design | EU Compliant
5. HOW IT WORKS — 3-step visual flow (Connect → Extract → Brief)
6. FEATURES PREVIEW — 4-6 feature cards with Coming Soon tags where applicable
7. FOUNDER NOTE — Short personal message + credibility
8. FINAL CTA — Email input + "Join X others on the waitlist"
9. FOOTER — Links, legal, social
```
---
### 4.2 Full Copy Draft
#### HERO
**Pre-headline badge:** `Beta launching June 2026`
<!-- ✅ REVISED — Updated hero to match selected Option C + tagline E -->
**Pre-headline badge:** `Beta launching June 2026`
**Tagline:** Meet your new chief of staff.
**Headline:**
What if AI could be your secretary?
**Subheadline:**
Not a chatbot. Not another app. A real AI that reads your email, knows your projects, and tells you what to focus on — without ever seeing your data.
**CTA:** `[Your email] [See how it works →]`
**Sub-CTA text:** Free to start. No credit card. Early adopters get priority access.
---
#### SOCIAL PROOF BAR
> Built by an AI Enterprise Solution Architect · Integrates with Gmail, Outlook, Teams · Runs 100% on your device
---
#### THE PROBLEM (optional emotional section)
**Headline:** You're juggling too many tools to stay organized.
**Body:**
Your important emails hide between newsletters. Your tasks live in three different apps. Meeting notes sit in a doc you'll never open again.
You don't need another tool. You need someone who reads everything and tells you what matters.
---
#### 3 PILLARS
**Pillar 1: AI Secretary**
adiuvAI reads your email, monitors your files, and watches your calendar. It extracts what's important and creates tasks, notes, and reminders — without you lifting a finger.
**Pillar 2: Private by Design**
Everything runs on your machine. Your data lives in an encrypted local database. No cloud server ever touches your content. You own your data, fully.
**Pillar 3: EU AI Act Compliant**
Built from the ground up for the new regulatory landscape. No training on user data. No profiling. GDPR and EU AI Act compliant by architecture, not by policy.
---
#### HOW IT WORKS
**Step 1: Connect**
Link your Gmail, Outlook, or local folders. adiuvAI starts learning what matters to you.
**Step 2: Extract**
AI agents scan your email, files, and meetings. They detect tasks, deadlines, and key information — and organize it into your personal workspace.
**Step 3: Brief**
Every morning, get a personalized briefing. Today's priorities, what changed overnight, and what needs your attention. Swipe through your day like stories.
---
#### FEATURES PREVIEW
| Feature | Status |
|---------|--------|
| Daily Brief & Activity Carousel | ✅ Beta |
| Email → Task Extraction (Gmail, Outlook) | ✅ Beta |
| Project & Task Management | ✅ Beta |
| Markdown Notes with AI Search | ✅ Beta |
| Timeline & Milestone Tracking | ✅ Beta |
| File & Folder Monitoring Agents | ✅ Beta |
| Telegram Bot Integration | ✅ Beta |
| Voice: Join Calls & Take Notes | 🔜 Coming Soon |
| Teams/Slack Chat Monitoring | 🔜 Coming Soon |
| Mobile Companion App | 🔜 Coming Soon |
| Team Workspace & SSO | 🔜 Roadmap |
---
#### FOUNDER NOTE
> **From the maker:**
> I'm Roberto, an AI Enterprise Solution Architect. I built adiuvAI because I was tired of promising my clients intelligent AI solutions while my own workday was chaos — emails piling up, tasks scattered across apps, meetings with no follow-through.
>
> adiuvAI is the tool I needed: an AI that actually reads my world and tells me what to do, without shipping my data to someone else's server.
>
> If that resonates with you, join the waitlist. Early adopters will shape what we build.
>
> — Roberto
---
#### FINAL CTA
**Headline:** Be the first to meet your AI secretary.
**Sub-text:** Beta launches June 2026. Early adopters get free priority access and a voice in what we build next.
**CTA:** `[Your email] [Join the waitlist →]`
---
## 5. Go-to-Market Recommendations
### 5.1 Launch Channels
| Channel | Action | Why | Priority |
|---------|--------|-----|:---:|
| **Product Hunt** | Launch on PH with "AI secretary" angle + privacy story | PH audience loves privacy-first + indie dev stories. Granola, Shortwave, Motion all launched here. | 🔴 High |
| **Hacker News** | "Show HN: I built a local-first AI secretary" post | HN audience cares deeply about local-first, E2E encryption, BYOK. Natural fit. | 🔴 High |
| **Reddit** (/r/productivity, /r/selfhosted, /r/artificial) | Authentic "I built this" post + engage in comments | Privacy-focused communities will champion a local-first AI tool. | 🔴 High |
| **LinkedIn** | Personal posts from your profile (HPE architect building AI tool) | Your credibility as an enterprise AI architect IS the story. LinkedIn loves founder journeys. | 🔴 High |
| **Twitter/X** | Build-in-public thread: "I'm building an AI secretary that runs locally" | AI Twitter is hungry for novel approaches. Local-first + privacy-by-design is contrarian. | 🟡 Medium |
| **Indie Hackers** | Product launch + revenue/growth updates | Indie audience loves solo founders with real products. | 🟡 Medium |
| **EU Tech Communities** | Position as "first EU AI Act compliant AI secretary" | Italian/European tech Twitter, EU startup events, AI regulation communities. | 🟡 Medium |
| **YouTube** | 3-5 min demo video: "My AI reads my email every morning" | Visual proof. Show the daily brief, the carousel, email extraction. | 🟡 Medium |
---
### 5.2 Content Strategy
**Pre-launch (now → beta):**
| Week | Content | Channel |
|------|---------|---------|
| 1 | "Why I'm building an AI secretary that never touches the cloud" (founder story) | LinkedIn, Twitter |
| 2 | "The problem with AI productivity tools: they all want your data" (thought leadership) | LinkedIn, HN |
| 3 | Demo video: Daily Brief walkthrough (30 sec) | Twitter, YouTube short |
| 4 | "EU AI Act is here — none of the big tools are ready" (positioning) | LinkedIn, Reddit |
| 5 | "Building adiuvAI: local-first architecture deep-dive" (technical) | HN, Dev communities |
| 6 | "adiuvAI beta is coming June 2026" (waitlist push) | All channels |
**Post-beta:**
- Weekly "What I shipped this week" updates (build in public)
- User testimonials from waitlist early adopters
- Comparison content: "adiuvAI vs Motion: why local-first matters"
- EU AI Act explainer content (SEO play)
---
### 5.3 Feature Roadmap Priorities (for Market Impact)
| Priority | Feature | Market Impact | Effort |
|:---:|---------|--------------|--------|
| **1** | Daily Brief carousel UI (visual, demo-able) | This is the hero feature for the landing page. You need a visual to show. | Medium |
| **2** | Gmail real-time sync (not just fetch) | "It reads my email" needs to actually work automatically for beta | High |
| **3** | Voice assistant (call recording + notes) | This is the "wow" future feature that drives waitlist signups | High |
| **4** | Outlook/Teams integration | Expands addressable market to enterprise/Microsoft users | Medium |
| **5** | Mobile companion (push daily brief to phone) | "How do I use it on the go?" is the first question people will ask | High |
---
### 5.4 Quick Wins (Low Effort, High Impact)
1. **Rename/rebrand the website** — Change from "CLAUDE.md" to "adiuvAI" immediately. The current name is confusing.
2. **Switch Landing page to English** — You said global reach. The current Italian page limits you.
3. **Add an email signup form today** — Even before redesigning the full page, add a Waitlist component. Use Buttondown, Mailchimp, or a simple Supabase/Airtable form.
4. **Record a 60-second Loom** — Show the daily brief, email extraction, and task creation. Embed on the waitlist page.
5. **Write your "Why I'm building this" LinkedIn post** — Your HPE AI Architect background is your unfair advantage for credibility. Use it.
<!-- ✅ REVISED — Logo already exists in adiuvAI/assets/, updated accordingly -->
6. **~~Create a brand logo~~** — ✅ Already exists in `adiuvAI/assets/`. Use it on the new waitlist page.
7. **Claim @adiuvai handles** — Twitter, LinkedIn page, Product Hunt, GitHub, Reddit. Do this now before someone else does.
**This week's plan:** Items 1 (rebrand to adiuvAI), 2 (English page), and 3 (email signup form) are confirmed as immediate priorities.
---
### 5.5 Pricing Considerations for Waitlist
For the **waitlist page**, I recommend NOT showing detailed pricing (Free/Pro/Power tiers). Instead:
- **"Free to get started"** — signals low friction
- **"Pro plans for power users"** — signals there's a business model
- **"Early adopters get priority access"** — signals exclusivity
Save the full pricing reveal for the beta launch.
**Rationale:** Your current pricing (Free/€15 Pro/€29 Power) is competitive with Motion ($19-34), but pricing pages kill waitlist conversion rates. People join waitlists for the vision, not the price. Show pricing when they can actually buy.
<!-- ✅ REVISED — Confirmed: no pricing on waitlist page -->
---
## Summary: The adiuvAI Story in One Paragraph
<!-- ✅ REVISED — Removed BYOK, added task completion, updated tone -->
> adiuvAI is an AI-powered personal secretary that runs entirely on your desktop. It connects to your email, files, and calendar, automatically extracts what matters, gives you a personalized daily brief every morning, and helps you complete your work — drafting follow-ups, organizing projects, and keeping everything on track. Unlike cloud-based tools like Motion, Superhuman, or Notion AI, adiuvAI stores all data locally with end-to-end encryption — your data literally never leaves your device. It's the first productivity AI built from the ground up for GDPR and EU AI Act compliance. No configuration needed — the AI just works. Beta launches June 2026.
---
*When you're happy with this strategy, switch to `@creative-director` to turn it into visual deliverables (landing page design, animations, promo materials). The creative director will read this file and brainstorm designs with you.*

253
docs/multi-region-guide.md Normal file
View File

@@ -0,0 +1,253 @@
# Guida Multi-Region — adiuvAI API
> Stato attuale: FastAPI containerizzata (docker-compose) su singolo VPS Hetzner in Europa.
> Obiettivo: ridurre la latenza per utenti fuori dall'Europa.
---
## Fase 1 — Ottimizzare Cloudflare (già in uso)
### 1.1 Argo Smart Routing
- **Dashboard → Traffic → Argo** — attivalo (~$5/mese + $0.10/GB)
- Usa i backbone privati Cloudflare invece dell'internet pubblico
- Riduce la latenza del 30-40% senza toccare nulla lato server
- Singolo cambiamento con miglior rapporto costo/beneficio
### 1.2 SSL/TLS
- **Dashboard → SSL/TLS → Overview** → mode **Full (Strict)** (non "Flexible", causa redirect loop)
- Abilita **TLS 1.3** (meno round-trip nell'handshake)
- Abilita **Early Hints** (103) in Speed → Optimization
### 1.3 Cache Rules
Di default Cloudflare non cachea le risposte API (Content-Type `application/json`). Per gli endpoint pubblici (es. `/api/v1/health`):
- **Dashboard → Caching → Cache Rules** → crea regola:
- Match: `URI Path starts with /api/v1/health`
- Action: Cache, Edge TTL 30s, Browser TTL 10s
- Lato codice: aggiungere header `Cache-Control: public, s-maxage=30` e `CDN-Cache-Control: public, max-age=30` all'health endpoint
- **NON** cacheate endpoint autenticati (il JWT rende ogni richiesta unica)
### 1.4 Response Compression
- **Dashboard → Speed → Optimization → Content Optimization**
- Abilita **Brotli** (più efficiente di gzip per payload JSON)
- Le risposte JSON vengono compresse automaticamente al transit
### 1.5 WebSocket
- **Dashboard → Network** → verifica che **WebSockets** sia ON (default nel piano Free)
- Il `/chat/stream` WebSocket viene proxato ma non cacheato
- Il keepalive di 30s che già avete mantiene la connessione viva attraverso Cloudflare
### 1.6 Tiered Cache (piano Pro+)
- **Dashboard → Caching → Tiered Cache** → attiva **Smart Tiered Caching**
- Cloudflare usa data center "upper-tier" come cache intermedia
- Riduce le hit al tuo origin server
### 1.7 Timeout
- **Dashboard → Network → WebSocket timeout**: aumenta se gli utenti hanno sessioni chat lunghe
- **Proxy Read Timeout**: default 100s, sufficiente per le LLM call (il tool loop ha cap 5 iterazioni)
---
## Fase 2 — Secondo nodo in US East
### Architettura target
```
┌─── Cloudflare (Geo Steering) ───┐
│ │
utenti EU/Africa utenti Americas
│ │
┌────────▼─────────┐ ┌──────────▼─────────┐
│ VPS EU (attuale) │ │ VPS US (nuovo) │
│ docker-compose │ │ docker-compose │
│ app + PG primary │ │ app + PG replica │
└────────┬──────────┘ └──────────┬──────────┘
│ │
└── PG streaming replication ────────┘
(async, read-only replica)
```
### Opzione A: Secondo VPS Hetzner (Ashburn) + Cloudflare Load Balancing
Estensione naturale del setup attuale — minimo cambiamento architetturale.
#### Step 1 — Provisioning del VPS US
1. Crea un VPS Hetzner in **Ashburn (us-east)** (stesse specs del nodo EU)
2. Setup identico: Docker, Docker Compose, git
3. Configura un **tunnel WireGuard** tra EU e US per il traffico DB (mai esporre PG sulla rete pubblica)
#### Step 2 — PostgreSQL Streaming Replication
**Sul PRIMARY (EU):**
1. Creare un utente replication:
```sql
CREATE ROLE replicator WITH REPLICATION LOGIN PASSWORD '<strong_password>';
```
2. Creare un replication slot:
```sql
SELECT pg_create_physical_replication_slot('replica_us_east');
```
3. Configurare `pg_hba.conf` per permettere connessioni dal VPS US:
```
host replication replicator <US_VPS_WIREGUARD_IP>/32 scram-sha-256
```
4. Esporre la porta PG solo sull'IP WireGuard nel `docker-compose.yml`:
```yaml
services:
db:
ports:
- "10.0.0.1:5432:5432" # solo interfaccia WireGuard
```
**Sul REPLICA (US):**
1. Base backup dal primary:
```bash
docker run --rm \
-v postgres_data:/var/lib/postgresql/data \
pgvector/pgvector:pg16 \
bash -c "pg_basebackup -h <PRIMARY_WG_IP> -U replicator \
-D /var/lib/postgresql/data -Fp -Xs -P -R"
```
Il flag `-R` crea automaticamente `standby.signal` e scrive `primary_conninfo` in `postgresql.auto.conf`.
2. Avviare PG in modalita replica (legge `standby.signal` e si connette al primary)
3. Verificare:
- Sul primary: `SELECT * FROM pg_stat_replication;` (deve mostrare il replica)
- Sul replica: `SELECT pg_is_in_recovery();` (deve restituire `t`)
#### Step 3 — Modifiche al codice FastAPI
Modifiche necessarie in `app/config/settings.py`:
- Aggiungere `DATABASE_READ_URL: str = ""` — URL del replica locale per le letture
- Aggiungere `REGION: str = "eu"` — identificativo regione per health check e observability
Modifiche in `app/db.py`:
- Creare un secondo engine `read_engine` che usa `DATABASE_READ_URL` (fallback a `DATABASE_URL` se vuoto)
- Esporre un `get_read_session()` dependency da usare nelle query read-only
Modifiche in `app/main.py`:
- L'health endpoint deve restituire `region` nel payload
- Aggiungere header `Cache-Control` / `CDN-Cache-Control` per il caching all'edge
Nelle route, per le query di sola lettura pesanti (es. ricerca, listing):
- Usare `db: AsyncSession = Depends(get_read_session)` invece di `get_session`
- Le scritture (auth, billing, update) continuano a usare `get_session` (→ primary EU)
#### Step 4 — Docker Compose per il nodo US
Creare un `docker-compose.replica.yml` (override) che:
- Sovrascrive le env dell'app con `DATABASE_READ_URL` verso il DB locale e `DATABASE_URL` verso il primary EU
- Imposta `REGION=us-east`
- Avvia PG in modalita replica (con `primary_conninfo` che punta al primary EU via WireGuard)
Il `.env` sul nodo US:
```env
DATABASE_URL=postgresql+asyncpg://postgres:<pass>@<PRIMARY_WG_IP>:5432/adiuvai
DATABASE_READ_URL=postgresql+asyncpg://postgres:postgres@db:5432/adiuvai
REGION=us-east
PRIMARY_DB_HOST=<PRIMARY_WG_IP>
REPLICATOR_PASSWORD=<strong_password>
# ... resto delle variabili (JWT_SECRET, STRIPE, LLM keys, etc.) identiche al nodo EU
```
Avvio: `docker compose -f docker-compose.yml -f docker-compose.replica.yml up -d`
#### Step 5 — Deploy CI multi-region
Estendere il workflow `.gitea/workflows/deploy.yaml` con un secondo job `deploy-us`:
- Identico a `deploy` ma con SSH verso il VPS US
- Usa `secrets.SSH_HOST_US`, `secrets.SSH_USER_US`, `secrets.SSH_KEY_US`
- Il comando di deploy usa `-f docker-compose.yml -f docker-compose.replica.yml`
- **NON** esegue `alembic upgrade head` — le migrazioni girano solo sul primary (il replica riceve le DDL via replication)
I due job `deploy` e `deploy-us` possono girare in parallelo (entrambi dipendono solo da `test`).
#### Step 6 — Cloudflare Geo Steering
1. **Dashboard → Traffic → Load Balancing** (piano Pro, ~$5/mese per pool)
2. Creare due **Origin Pools**:
- `eu-pool`: origin = IP del VPS EU, health check = `GET /api/v1/health`
- `us-pool`: origin = IP del VPS US, health check = `GET /api/v1/health`
3. Creare un **Load Balancer** su `api.adiuvai.com`:
- Steering policy: **Geo**
- EU/Africa → `eu-pool`
- Americas → `us-pool`
- Fallback: `eu-pool`
4. Impostare health monitor: `GET /api/v1/health`, interval 60s, timeout 5s
- Se un nodo va giù, tutto il traffico va al nodo sano (automatic failover)
### Opzione B: Fly.io (alternativa più semplice, meno controllo)
Se preferisci evitare la gestione manuale di un secondo VPS:
1. Crea un `fly.toml` nella root del progetto API
2. `fly launch` — Fly rileva il Dockerfile e deploya
3. `fly regions add iad` — aggiunge US East (Ashburn)
4. Fly gestisce: routing anycast, health checks, TLS, auto-scaling
5. Il DB resta su Hetzner EU — Fly non risolve il problema del database, ma elimina tutta la gestione infrastrutturale dell'app layer
6. Costo: ~$5-15/mese per region (dipende dalle risorse)
7. Contro: meno controllo, vendor lock-in, il DB non ha replica locale
### Opzione C: Hetzner Cloud Load Balancer + geo DNS esterno
- Hetzner offre load balancer nativi, ma sono single-region (non cross-region)
- Non adatto per geo-routing, utile solo per HA nella stessa region
---
## Fase 3 — Terzo nodo in Asia (futuro)
Stessa procedura della Fase 2:
1. VPS Hetzner Singapore (o AWS ap-southeast-1)
2. Secondo PG replica con slot `replica_asia`
3. Terzo pool in Cloudflare Load Balancing con geo steering per Asia-Pacific
4. Terzo job `deploy-asia` nel CI
Da valutare solo quando il traffico dall'Asia lo giustifica.
---
## Sicurezza della rete tra i nodi
| Metodo | Pro | Contro |
|--------|-----|--------|
| **WireGuard** | Semplice, veloce, <1ms overhead, kernel-level | Setup manuale per nodo |
| **Hetzner vSwitch** | Zero config se entrambi su Hetzner | Solo stessa region |
| **Tailscale** | WireGuard gestito, zero config rete | Dipendenza esterna |
| **SSH tunnel** | Nessun software extra | Overhead maggiore, meno stabile |
**Raccomandazione**: WireGuard (o Tailscale per semplicita) tra tutti i nodi. Mai esporre PostgreSQL 5432 sull'IP pubblico.
---
## Considerazioni specifiche per adiuvAI
- **L'app e local-first**: la maggior parte delle operazioni (tasks, notes, projects) avviene in SQLite locale nell'Electron app. Il backend serve solo auth, chat streaming, cloud storage e billing. Questo significa che la latenza del backend impatta meno di quanto sembrerebbe.
- **WebSocket `/chat/stream`**: il geo steering porta l'utente al nodo piu vicino, ma la risposta LLM dipende dalla latenza verso OpenAI/Anthropic (non verso il tuo server). Il beneficio principale e nel tempo di handshake e nel primo token.
- **`_pending_states` in-memory per OAuth**: gia documentato come non scalabile su multi-worker. Con multi-region diventa critico — servira Redis condiviso o spostare lo state su DB.
- **JWT_SECRET deve essere identico** su tutti i nodi — un token emesso dal nodo EU deve essere validato dal nodo US.
- **Alembic migrations**: eseguire SOLO sul primary. Il replica riceve le DDL via streaming replication.
---
## Stima costi
| Componente | Costo mensile |
|------------|---------------|
| Argo Smart Routing | ~$5 + $0.10/GB |
| Cloudflare Load Balancing | ~$5/pool |
| VPS Hetzner US (CX22) | ~$5-10 |
| WireGuard | Gratis |
| **Totale Fase 1** | **~$5** |
| **Totale Fase 2** | **~$15-20** |

View File

@@ -0,0 +1 @@
C:\Users\PC-Roby\Documents\_adiuvai_workspace

View File

@@ -0,0 +1,250 @@
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\index.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\web.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\brand-showcase.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\README.md
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\requirements.txt
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_action.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_date.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_info.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\agent_runner_v2\data\email_no_project.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\journey_v2\data\email_action.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\journey_v2\data\email_info.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_action.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_heavy.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_single.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\email_thread.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\fallback.txt
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\generic_page.html
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\fixtures\preprocessors\data\notes.txt
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\favicon.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-black.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-full.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-icon.png
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-icon.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-mark.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-white.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\logo\logo-wordmark.svg
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\home.png
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\home_chat.png
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\projects.png
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\assets\screenshot\task.png
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\drizzle.config.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\forge.config.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\forge.env.d.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\scripts\seed-fake-data.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\index.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\ipc.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\store.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\agents\agent-scheduler.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\ai\orchestrator.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\api\backend-client.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\api\drizzle-executor.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\auth-manager.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\backup-key.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\auth\locale-defaults.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\index.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\schema.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\db\vectordb.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\main\router\index.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\preload\index.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\preload\trpc.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\i18n.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\index.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\router.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routeTree.gen.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\web-main.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\theme-provider.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\agents\AgentRunLog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\AIChatPanel.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\ChatInputBox.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\FloatingChat.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatChartBlock.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatEntityBlock.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatTableBlock.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\ChatTimelineBlock.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ai\blocks\index.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\auth\LoginForm.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\layout\AppShell.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\notes\MilkdownEditor.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\onboarding\OnboardingFlow.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\onboarding\onboardingOptions.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\KanbanBoard.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectDetail.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectSidebar.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\projects\ProjectTabBar.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AccountSection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentRow.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentRunHistorySheet.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AgentsSection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AppearanceSection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\AvatarCropDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\BillingSection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\CloudAgentConfigPanel.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\InlineAgentCreationStepper.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\JourneyDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\LocalAgentConfigPanel.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\MemorySection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\ProfileSection.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\PromptBuilderChat.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\SettingsCard.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\TemplateSelectCard.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\settings\types.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\EditTaskDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\NewTaskDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\PriorityBadge.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\task-utils.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskCard.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskDetailDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\tasks\TaskRow.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\AddEventDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\EditEventDialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\history-types.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\ProjectTimeline.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\ProjectTimelineBox.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\TimelineAxisHeader.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\timeline\TimelineGanttView.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\alert-dialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\avatar.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\badge.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\breadcrumb.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\button.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\calendar.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\card.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\chart.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\checkbox.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\collapsible.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\context-menu.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\dialog.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\dropdown-menu.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\empty.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\field.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\gradual-blur.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\input-group.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\input.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\item.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\label.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\popover.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\scroll-area.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\select.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\separator.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sheet.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sidebar.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\skeleton.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\slider.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\sonner.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\switch.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\table.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\tabs.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\textarea.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\toggle-group.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\toggle.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\components\ui\tooltip.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\context\ExpandedClientsContext.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\context\FloatingChatContext.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\use-mobile.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useAIChat.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useDoubleClickAI.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useNotify.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\hooks\useTimelineHistory.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\date.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\httpLink.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\ipcLink.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\platform.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\trpc.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\lib\utils.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\index.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\notes.$noteId.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\projects.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\settings.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\tasks.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\timeline.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\renderer\routes\__root.tsx
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\api-types.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\batch-types.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\adiuvAI\src\shared\casing.ts
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\env.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\001_initial_schema.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\003_agent_tables.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\004_add_memory_tables.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\005_associative_pgvector.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\006_memory_relations.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\1f5975a4f3f4_add_extraction_queue.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\818478c251dc_add_name_and_surname_to_users_table.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\9a1f2d0b6c7e_deprecate_backend_agent_config_tables.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\a3b9c0d1e2f3_add_agent_config_to_local_agents.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\b4c0d1e2f3a4_add_oauth_and_avatar.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\c5d1e2f3a4b5_add_onboarding_completed_at.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\alembic\versions\e04100e88ace_avatar_url_varchar_to_text.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\db.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\main.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\models.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\schemas.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\filesystem_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\note_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\project_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\task_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\timeline_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\agents\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\deps.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\auth.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\rate_limit.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\sanitizer.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\middleware\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\agents.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\agent_setup.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\auth.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\billing.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\chat.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\device_ws.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\memory.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\api\routes\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\auth\oauth_providers.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\auth\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\stripe_service.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\tier_manager.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\billing\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\config\settings.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\config\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_registry.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_runner.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\agent_session_buffer.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\brief_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\deep_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\device_manager.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\embeddings.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\langfuse_client.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\llm.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_extraction.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_maintenance.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\memory_middleware.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\output_formatter.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\ws_context.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\base.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\email_html.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\core\preprocessors\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\gmail.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\ms_graph.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\app\integrations\__init__.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\conftest.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_agent_runner_v2.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_auth.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_brief_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_deep_agent.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_device_ws.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_integrations.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_journey_v2.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_audit.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_extraction.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_middleware.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_models.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_proactive.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_memory_relations.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_middleware.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_output_formatter.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_preprocessors.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_schemas_v3.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\test_ws_unified.py
C:\Users\PC-Roby\Documents\_adiuvai_workspace\api\tests\__init__.py

View File

@@ -0,0 +1,728 @@
# Graph Report - . (2026-06-12)
## Corpus Check
- 380 files · ~250,681 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 3244 nodes · 7215 edges · 180 communities (152 shown, 28 thin omitted)
- Extraction: 87% EXTRACTED · 13% INFERRED · 0% AMBIGUOUS · INFERRED: 955 edges (avg confidence: 0.63)
- Token cost: 0 input · 0 output
## Community Hubs (Navigation)
- [[_COMMUNITY_Auth UI Forms|Auth UI Forms]]
- [[_COMMUNITY_Folder & Listbox UI|Folder & Listbox UI]]
- [[_COMMUNITY_Chat Entity Blocks|Chat Entity Blocks]]
- [[_COMMUNITY_Scout Run UI|Scout Run UI]]
- [[_COMMUNITY_Auth & Memory ORM|Auth & Memory ORM]]
- [[_COMMUNITY_Scout Config Models|Scout Config Models]]
- [[_COMMUNITY_Client Agent|Client Agent]]
- [[_COMMUNITY_Billing & Request Context|Billing & Request Context]]
- [[_COMMUNITY_Filesystem Agent|Filesystem Agent]]
- [[_COMMUNITY_Project Folder Agent|Project Folder Agent]]
- [[_COMMUNITY_Shared API Types|Shared API Types]]
- [[_COMMUNITY_Files Section UI|Files Section UI]]
- [[_COMMUNITY_Drizzle Executor|Drizzle Executor]]
- [[_COMMUNITY_Device & Langfuse|Device & Langfuse]]
- [[_COMMUNITY_Cloud Scout Triage|Cloud Scout Triage]]
- [[_COMMUNITY_Memory Middleware|Memory Middleware]]
- [[_COMMUNITY_API Test Harness|API Test Harness]]
- [[_COMMUNITY_Middleware Tests|Middleware Tests]]
- [[_COMMUNITY_LLM Factory|LLM Factory]]
- [[_COMMUNITY_WS Session Buffer|WS Session Buffer]]
- [[_COMMUNITY_Prompt Formatting|Prompt Formatting]]
- [[_COMMUNITY_NPM Dependencies|NPM Dependencies]]
- [[_COMMUNITY_Community 22|Community 22]]
- [[_COMMUNITY_Community 23|Community 23]]
- [[_COMMUNITY_Community 24|Community 24]]
- [[_COMMUNITY_Community 25|Community 25]]
- [[_COMMUNITY_Community 26|Community 26]]
- [[_COMMUNITY_Community 27|Community 27]]
- [[_COMMUNITY_Community 28|Community 28]]
- [[_COMMUNITY_Community 29|Community 29]]
- [[_COMMUNITY_Community 30|Community 30]]
- [[_COMMUNITY_Community 31|Community 31]]
- [[_COMMUNITY_Community 32|Community 32]]
- [[_COMMUNITY_Community 33|Community 33]]
- [[_COMMUNITY_Community 34|Community 34]]
- [[_COMMUNITY_Community 35|Community 35]]
- [[_COMMUNITY_Community 36|Community 36]]
- [[_COMMUNITY_Community 37|Community 37]]
- [[_COMMUNITY_Community 38|Community 38]]
- [[_COMMUNITY_Community 39|Community 39]]
- [[_COMMUNITY_Community 40|Community 40]]
- [[_COMMUNITY_Community 41|Community 41]]
- [[_COMMUNITY_Community 42|Community 42]]
- [[_COMMUNITY_Community 43|Community 43]]
- [[_COMMUNITY_Community 44|Community 44]]
- [[_COMMUNITY_Community 45|Community 45]]
- [[_COMMUNITY_Community 46|Community 46]]
- [[_COMMUNITY_Community 47|Community 47]]
- [[_COMMUNITY_Community 48|Community 48]]
- [[_COMMUNITY_Community 49|Community 49]]
- [[_COMMUNITY_Community 50|Community 50]]
- [[_COMMUNITY_Community 51|Community 51]]
- [[_COMMUNITY_Community 52|Community 52]]
- [[_COMMUNITY_Community 53|Community 53]]
- [[_COMMUNITY_Community 54|Community 54]]
- [[_COMMUNITY_Community 55|Community 55]]
- [[_COMMUNITY_Community 56|Community 56]]
- [[_COMMUNITY_Community 57|Community 57]]
- [[_COMMUNITY_Community 58|Community 58]]
- [[_COMMUNITY_Community 59|Community 59]]
- [[_COMMUNITY_Community 60|Community 60]]
- [[_COMMUNITY_Community 61|Community 61]]
- [[_COMMUNITY_Community 62|Community 62]]
- [[_COMMUNITY_Community 63|Community 63]]
- [[_COMMUNITY_Community 64|Community 64]]
- [[_COMMUNITY_Community 65|Community 65]]
- [[_COMMUNITY_Community 66|Community 66]]
- [[_COMMUNITY_Community 67|Community 67]]
- [[_COMMUNITY_Community 68|Community 68]]
- [[_COMMUNITY_Community 69|Community 69]]
- [[_COMMUNITY_Community 70|Community 70]]
- [[_COMMUNITY_Community 71|Community 71]]
- [[_COMMUNITY_Community 72|Community 72]]
- [[_COMMUNITY_Community 73|Community 73]]
- [[_COMMUNITY_Community 74|Community 74]]
- [[_COMMUNITY_Community 75|Community 75]]
- [[_COMMUNITY_Community 76|Community 76]]
- [[_COMMUNITY_Community 77|Community 77]]
- [[_COMMUNITY_Community 78|Community 78]]
- [[_COMMUNITY_Community 79|Community 79]]
- [[_COMMUNITY_Community 80|Community 80]]
- [[_COMMUNITY_Community 81|Community 81]]
- [[_COMMUNITY_Community 82|Community 82]]
- [[_COMMUNITY_Community 83|Community 83]]
- [[_COMMUNITY_Community 84|Community 84]]
- [[_COMMUNITY_Community 85|Community 85]]
- [[_COMMUNITY_Community 86|Community 86]]
- [[_COMMUNITY_Community 87|Community 87]]
- [[_COMMUNITY_Community 88|Community 88]]
- [[_COMMUNITY_Community 90|Community 90]]
- [[_COMMUNITY_Community 91|Community 91]]
- [[_COMMUNITY_Community 92|Community 92]]
- [[_COMMUNITY_Community 93|Community 93]]
- [[_COMMUNITY_Community 94|Community 94]]
- [[_COMMUNITY_Community 95|Community 95]]
- [[_COMMUNITY_Community 96|Community 96]]
- [[_COMMUNITY_Community 97|Community 97]]
- [[_COMMUNITY_Community 98|Community 98]]
- [[_COMMUNITY_Community 99|Community 99]]
- [[_COMMUNITY_Community 100|Community 100]]
- [[_COMMUNITY_Community 101|Community 101]]
- [[_COMMUNITY_Community 102|Community 102]]
- [[_COMMUNITY_Community 103|Community 103]]
- [[_COMMUNITY_Community 104|Community 104]]
- [[_COMMUNITY_Community 105|Community 105]]
- [[_COMMUNITY_Community 106|Community 106]]
- [[_COMMUNITY_Community 107|Community 107]]
- [[_COMMUNITY_Community 108|Community 108]]
- [[_COMMUNITY_Community 109|Community 109]]
- [[_COMMUNITY_Community 110|Community 110]]
- [[_COMMUNITY_Community 111|Community 111]]
- [[_COMMUNITY_Community 112|Community 112]]
- [[_COMMUNITY_Community 113|Community 113]]
- [[_COMMUNITY_Community 114|Community 114]]
- [[_COMMUNITY_Community 115|Community 115]]
- [[_COMMUNITY_Community 116|Community 116]]
- [[_COMMUNITY_Community 117|Community 117]]
- [[_COMMUNITY_Community 118|Community 118]]
- [[_COMMUNITY_Community 119|Community 119]]
- [[_COMMUNITY_Community 120|Community 120]]
- [[_COMMUNITY_Community 121|Community 121]]
- [[_COMMUNITY_Community 122|Community 122]]
- [[_COMMUNITY_Community 123|Community 123]]
- [[_COMMUNITY_Community 124|Community 124]]
- [[_COMMUNITY_Community 125|Community 125]]
- [[_COMMUNITY_Community 126|Community 126]]
- [[_COMMUNITY_Community 127|Community 127]]
- [[_COMMUNITY_Community 128|Community 128]]
- [[_COMMUNITY_Community 129|Community 129]]
- [[_COMMUNITY_Community 130|Community 130]]
- [[_COMMUNITY_Community 131|Community 131]]
- [[_COMMUNITY_Community 133|Community 133]]
- [[_COMMUNITY_Community 134|Community 134]]
- [[_COMMUNITY_Community 135|Community 135]]
- [[_COMMUNITY_Community 136|Community 136]]
- [[_COMMUNITY_Community 137|Community 137]]
- [[_COMMUNITY_Community 153|Community 153]]
- [[_COMMUNITY_Community 154|Community 154]]
- [[_COMMUNITY_Community 155|Community 155]]
- [[_COMMUNITY_Community 156|Community 156]]
- [[_COMMUNITY_Community 157|Community 157]]
- [[_COMMUNITY_Community 158|Community 158]]
- [[_COMMUNITY_Community 159|Community 159]]
- [[_COMMUNITY_Community 160|Community 160]]
- [[_COMMUNITY_Community 161|Community 161]]
- [[_COMMUNITY_Community 162|Community 162]]
- [[_COMMUNITY_Community 163|Community 163]]
- [[_COMMUNITY_Community 164|Community 164]]
- [[_COMMUNITY_Community 165|Community 165]]
- [[_COMMUNITY_Community 166|Community 166]]
- [[_COMMUNITY_Community 175|Community 175]]
- [[_COMMUNITY_Community 178|Community 178]]
## God Nodes (most connected - your core abstractions)
1. `cn()` - 239 edges
2. `MemoryMiddleware` - 110 edges
3. `useNotify()` - 63 edges
4. `trpc` - 50 edges
5. `MemoryProactive` - 47 edges
6. `execute_on_client()` - 46 edges
7. `Button()` - 46 edges
8. `CloudScoutConfig` - 43 edges
9. `MemoryAssociative` - 42 edges
10. `User` - 40 edges
## Surprising Connections (you probably didn't know these)
- `api/requirements.txt` --references--> `core/llm.py (LiteLLM factory)` [INFERRED]
api/requirements.txt → .claude/CLAUDE.md
- `Any` --uses--> `MemoryMiddleware` [INFERRED]
api/app/agents/relations_agent.py → api/app/core/memory_middleware.py
- `ChatRequest` --uses--> `MemoryMiddleware` [INFERRED]
api/app/api/routes/chat.py → api/app/core/memory_middleware.py
- `JSONResponse` --uses--> `MemoryMiddleware` [INFERRED]
api/app/api/routes/chat.py → api/app/core/memory_middleware.py
- `AsyncSession` --uses--> `Subscription` [INFERRED]
api/app/billing/tier_manager.py → api/app/models.py
## Import Cycles
- 1-file cycle: `api/app/models.py -> api/app/models.py`
- 1-file cycle: `api/app/main.py -> api/app/main.py`
- 1-file cycle: `api/app/api/routes/scouts.py -> api/app/api/routes/scouts.py`
- 1-file cycle: `api/app/billing/stripe_service.py -> api/app/billing/stripe_service.py`
- 1-file cycle: `api/app/core/memory_middleware.py -> api/app/core/memory_middleware.py`
- 1-file cycle: `api/app/core/scout_runner.py -> api/app/core/scout_runner.py`
- 1-file cycle: `api/app/integrations/gmail.py -> api/app/integrations/gmail.py`
- 1-file cycle: `api/app/integrations/ms_graph.py -> api/app/integrations/ms_graph.py`
## Hyperedges (group relationships)
- **WS Tool-Call Round-Trip Flow** — backend_client, device_ws, drizzle_executor, deep_agent, device_manager [INFERRED 0.85]
- **Encrypted Memory Subsystem** — memory_middleware, memory_maintenance, memory_extraction, fernet_encryption, orm_models [INFERRED 0.80]
- **Redis-Backed Shared State Refactor** — redis_introduction, rate_limit_middleware, ttl_state_store, oauth_providers [INFERRED 0.78]
## Communities (180 total, 28 thin omitted)
### Community 0 - "Auth UI Forms"
Cohesion: 0.03
Nodes (106): LoginForm(), SignInForm(), SignUpForm(), ExpandedClientsContext, ExpandedClientsContextValue, ExpandedClientsProvider(), useExpandedClients(), HeaderContext (+98 more)
### Community 1 - "Folder & Listbox UI"
Cohesion: 0.06
Nodes (65): FolderUnlinkDialogProps, ListboxItemProps, useListboxKeys(), UseListboxKeysOptions, NotifyOptions, ToastVariant, trpc, ProjectSidebarProps (+57 more)
### Community 2 - "Chat Entity Blocks"
Cohesion: 0.05
Nodes (68): TaskEntityBlock(), TimelineEventEntityBlock(), useRovingFocus(), detectBrowserFormatPrefs(), formatDate(), formatDateTime(), formatDueDate(), formatRelative() (+60 more)
### Community 3 - "Scout Run UI"
Cohesion: 0.05
Nodes (61): RunRow(), ScoutRunSummary, statusBadge(), FolderChip(), FolderChipProps, Filter, FILTERS, FolderFileList() (+53 more)
### Community 4 - "Auth & Memory ORM"
Cohesion: 0.09
Nodes (71): AsyncSession, RedirectResponse, UserProfile, MemoryRelation, MemoryAssociative, OAuthAccount, Per-user semantic memory: encrypted content + pgvector embedding for similarity, RefreshToken (+63 more)
### Community 5 - "Scout Config Models"
Cohesion: 0.07
Nodes (69): AsyncSession, CloudScoutConfig, datetime, RedirectResponse, ScoutRunLog, UserProfile, CloudScoutConfig, LocalScoutConfig (+61 more)
### Community 6 - "Client Agent"
Cohesion: 0.04
Nodes (61): get_client(), list_clients(), Client agent — read-only tools for the clients table., List clients, optionally filtered by a name/email substring search. searc, Get full details for one client by UUID. id: the client's UUID., create_project(), delete_project(), get_project() (+53 more)
### Community 7 - "Billing & Request Context"
Cohesion: 0.05
Nodes (51): Any, AsyncSession, Request, UserProfile, AsyncSession, BillingTier, AsyncSession, BillingTier (+43 more)
### Community 8 - "Filesystem Agent"
Cohesion: 0.06
Nodes (53): get_file_metadata(), list_directory(), make_directory_tools(), Filesystem agent — tools for reading local directories and files on Electron., Return filesystem tools that resolve relative paths against *base_directory*., Resolve *path* against *base* when *path* is relative. The LLM often pass, List files and folders in a local directory on the user's device. Returns, Read the text content of a local file on the user's device. Returns the f (+45 more)
### Community 9 - "Project Folder Agent"
Cohesion: 0.06
Nodes (48): _decode(), _fetch_file(), _is_unsafe_path(), Scoped file-read and search tools for the project folder feature., Search a project folder file for a query string (case-insensitive substring)., Return the raw Electron tool_result dict for a file read., Decode a tool_result into (text, kind, total_size). For pdf/docx, extracts, Read a slice of a file inside the project's linked folder. Args: (+40 more)
### Community 10 - "Shared API Types"
Cohesion: 0.04
Nodes (56): AgentCatalogItemSchema, AgentManifest, AgentManifestSchema, AgentRunLogSchema, BillingTier, BillingTierSchema, CloudScoutConfigSchema, JourneyMessage (+48 more)
### Community 11 - "Files Section UI"
Cohesion: 0.09
Nodes (41): FilesSection(), FilesSectionProps, FolderLinkCard(), FolderLinkCardProps, FolderUnlinkDialog(), useNotify(), NavUser(), usePlatform() (+33 more)
### Community 12 - "Drizzle Executor"
Cohesion: 0.04
Nodes (48): AnyTable, buildOrderBy(), ExecutorError, RESERVED_KEYS, TABLE_REGISTRY, TableName, AiChatMessage, AiChatSession (+40 more)
### Community 13 - "Device & Langfuse"
Cohesion: 0.07
Nodes (48): Any, Any, datetime, DeviceConnectionManager, ScoutRunLog, compile_prompt(), extract_usage(), get_langfuse() (+40 more)
### Community 14 - "Cloud Scout Triage"
Cohesion: 0.08
Nodes (34): Request, CloudScoutConfig, ItemContent, ItemRef, CloudScoutConfig, ScoutTriageQueue, Register a SourceConnector instance under its ``source_type``. Calling tw, register_connector() (+26 more)
### Community 15 - "Memory Middleware"
Cohesion: 0.07
Nodes (23): Fernet, _encrypt(), _now(), Memory Middleware — enrich requests with memory context and store interactions., Summarise and store a completed interaction in episodic memory. The s, Route extraction to realtime task or batch queue based on user tier., Upsert a core memory key/value for a user., Return core memory as editable blocks (label/value). (+15 more)
### Community 16 - "API Test Harness"
Cohesion: 0.06
Nodes (31): AsyncSession, datetime, AsyncSession, TestClient, Base, get_session(), Database engine, session factory, and base model. All app code uses the async, Shared declarative base for all ORM models. (+23 more)
### Community 17 - "Middleware Tests"
Cohesion: 0.11
Nodes (16): TestClient, _auth_header(), _make_jwt(), _override_db(), Tests for Step 9 middleware: auth, rate limiting, and sanitizer. Auth tests:, Each test uses a fresh unique user_id so windows never collide., POST /auth/register is exempt — 25 calls should never return 429., POST /auth/login is exempt — multiple failed attempts are not rate-limited. (+8 more)
### Community 18 - "LLM Factory"
Cohesion: 0.10
Nodes (39): ChatLiteLLM, ChatOpenAI, _api_key_for_model(), get_agent_llm(), get_llm(), model_for_agent(), LLM factory — centralised model instantiation via LiteLLM. Every agent and th, Return the resolved model string for *agent_name* (for Langfuse tracking). (+31 more)
### Community 19 - "WS Session Buffer"
Cohesion: 0.10
Nodes (38): WebSocket, ContextualBufferProxy, Thin wrapper around _SessionBuffer that closes over user_id + session_id., clear_client_executor(), Bind *fn* as the executor for the current async context (task/coroutine)., Remove the executor binding (best-effort; ContextVar resets on task exit)., set_client_executor(), device_ws() (+30 more)
### Community 20 - "Prompt Formatting"
Cohesion: 0.08
Nodes (39): Path, _format_metadata(), _format_projects(), _get_extraction_rules(), _get_no_match_behavior(), Format the project list for the unified system prompt., Format preprocessor metadata as a compact context block., Return the extraction_prompt for *content_type* from *agent_config*. Fall (+31 more)
### Community 21 - "NPM Dependencies"
Cohesion: 0.05
Nodes (40): dependencies, better-sqlite3, class-variance-authority, clsx, date-fns, drizzle-orm, electron-squirrel-startup, electron-store (+32 more)
### Community 22 - "Community 22"
Cohesion: 0.09
Nodes (36): Any, AsyncSession, _apply_candidate(), _content_to_key(), decide_action(), MemoryCandidate, Mem0-style Extract/Update pipeline — Phase 2. Runs after every ``store_episod, Decide what to do with a candidate given existing memories in the same tier. (+28 more)
### Community 23 - "Community 23"
Cohesion: 0.07
Nodes (34): absolutePath(), attachmentsRoot(), copyIntoTask(), deleteStored(), deleteTaskDir(), sanitizeFilename(), detectFormatPrefs(), detectLanguage() (+26 more)
### Community 24 - "Community 24"
Cohesion: 0.14
Nodes (36): alias, AsyncSession, MemoryRelation, UserProfile, Any, AsyncSession, datetime, MemoryRelation (+28 more)
### Community 25 - "Community 25"
Cohesion: 0.08
Nodes (26): ItemContent, ItemRef, _extract_plain_text_body(), _get_gmail_service(), _gmail_service_from_token(), GmailConnector, Gmail SourceConnector — wraps the existing GmailClient. Responsibilities:, Fetch subject, sender, snippet only — uses Gmail metadata format (no body). (+18 more)
### Community 26 - "Community 26"
Cohesion: 0.08
Nodes (27): NewTaskDialog(), Props, TaskFormValues, OrderBy, StatusFilter, buildWindow(), TaskPager(), TaskTable() (+19 more)
### Community 27 - "Community 27"
Cohesion: 0.10
Nodes (29): AsyncSession, UserProfile, MemoryMiddleware, Delete a core memory block by label. Returns True if deleted., Query relation rows for a user with optional filters., Enrich orchestrator context with memory and persist interactions after., get_current_user(), Validate a Bearer JWT and return the authenticated user. The JWT is used (+21 more)
### Community 28 - "Community 28"
Cohesion: 0.10
Nodes (33): BaseModel, AuthTokens, ChatContext, ChatRequest, ChatResponse, CloudScoutCreateRequest, CloudScoutResponse, CloudScoutUpdateRequest (+25 more)
### Community 29 - "Community 29"
Cohesion: 0.12
Nodes (27): checkConnectivity(), dailyBrief(), generateAndCacheBrief(), getBriefTimeSlot(), getCachedBrief(), getCurrentSlotKey(), invalidateBriefCache(), markCurrentSlotAsGenerated() (+19 more)
### Community 30 - "Community 30"
Cohesion: 0.11
Nodes (22): bootstrapMigrationsLedger(), DbInstance, initDb(), resolveMigrationsFolder(), projectFolderFiles, projects, DOCX_EXTS, IMAGE_EXTS (+14 more)
### Community 31 - "Community 31"
Cohesion: 0.11
Nodes (20): Any, datetime, EmailMessage, ChatMessage, _build_email_filter(), MSGraphClient, _odata_datetime(), Microsoft Graph API client for Outlook and Teams cloud agent integration. Han (+12 more)
### Community 32 - "Community 32"
Cohesion: 0.10
Nodes (16): _make_gmail_message(), _make_graph_email(), _make_graph_teams_message(), Tests for Step 3.6: cloud provider integration clients. Coverage: Unit \u2, Build a minimal Gmail API message response dict., GmailClient.fetch_messages tests with mocked Google API., Build a minimal MS Graph message item dict., MSGraphClient.fetch_emails tests with mocked httpx. (+8 more)
### Community 33 - "Community 33"
Cohesion: 0.09
Nodes (27): PreprocessResult, PreprocessResult, Path, PreprocessResult, Base types for the preprocessor system., Output of a preprocessor handler. Attributes ---------- content, _extract_metadata(), preprocess_email_html() (+19 more)
### Community 34 - "Community 34"
Cohesion: 0.06
Nodes (31): devDependencies, drizzle-kit, electron, @electron-forge/cli, @electron-forge/maker-deb, @electron-forge/maker-rpm, @electron-forge/maker-squirrel, @electron-forge/maker-zip (+23 more)
### Community 35 - "Community 35"
Cohesion: 0.08
Nodes (20): AuthExpiredError, IndexSessionListener, JourneyListener, logHttp(), logHttpResponse(), logWsRecv(), OfflineError, QuotaError (+12 more)
### Community 36 - "Community 36"
Cohesion: 0.11
Nodes (29): _datetime_context_injection(), Build a comprehensive DATE CONTEXT block with pre-computed ms-epoch boundaries f, Return a small block with per-request scope and resolved project context., _request_context_block(), _fp(), _parse_ms(), Unit tests for single-agent deep_agent flows with mocked tool results., Extract [start, end] from a 'key [start, end]' line in the DATE CONTEXT block. (+21 more)
### Community 38 - "Community 38"
Cohesion: 0.07
Nodes (26): BatchAction, BatchActionSchema, BatchActionType, BatchActionTypeSchema, BatchAnalysis, BatchAnalysisSchema, BatchConfig, BatchConfigSchema (+18 more)
### Community 39 - "Community 39"
Cohesion: 0.10
Nodes (19): BriefChatHeader(), BriefChatHeaderProps, PRIORITY_STYLES, relativeDate(), CanvasPlaceholder(), CanvasPlaceholderProps, KIND_META, CarouselControls() (+11 more)
### Community 41 - "Community 41"
Cohesion: 0.13
Nodes (23): _build_system_prompt(), _candidate_tokens(), _fetch_project_manifest(), get_page_details(), _is_upcoming_timeline_query(), _needs_project_resolution(), _normalize_tagged_list_lines(), _prepare_context() (+15 more)
### Community 42 - "Community 42"
Cohesion: 0.09
Nodes (15): WebSocket, DeviceConnectionManager, DeviceConnection, DeviceConnectionManager, Device connection manager. Maintains in-memory state for all active Electron, Send *frame* as a JSON text message to the device. Raises ``RuntimeEr, Register a Future that will be resolved when the tool_result arrives., Fulfil the Future registered under *call_id* with the Electron result. (+7 more)
### Community 43 - "Community 43"
Cohesion: 0.12
Nodes (14): Fernet, decrypt_token(), EmailMessage, encrypt_token(), _get_fernet(), Cloud provider integration utilities. Provides: * Shared message dataclass, Fernet-encrypt an OAuth credential dict and return a base64 string. Store, Decrypt a Fernet-encrypted token string and return the credential dict. R (+6 more)
### Community 44 - "Community 44"
Cohesion: 0.13
Nodes (17): Any, datetime, EmailMessage, _build_gmail_query(), GmailClient, _parse_body(), _parse_date(), Gmail API client for cloud agent integration. Wraps the Google Gmail REST API (+9 more)
### Community 45 - "Community 45"
Cohesion: 0.17
Nodes (20): scoutRuns, AppSettings, deleteLocalScout(), getDeviceId(), getFormatPrefs(), getLocalScout(), getLocalScouts(), getStore() (+12 more)
### Community 46 - "Community 46"
Cohesion: 0.13
Nodes (20): AppShell(), FileRoutesByFullPath, FileRoutesById, FileRoutesByPath, FileRoutesByTo, FileRouteTypes, IndexRoute, NotesNoteIdRoute (+12 more)
### Community 47 - "Community 47"
Cohesion: 0.11
Nodes (20): create_note(), delete_note(), _fmt_summary(), get_note(), _is_uuid(), list_notes(), propose_note_edit(), Note agent — Markdown note management (list, get, create, update, propose edit). (+12 more)
### Community 48 - "Community 48"
Cohesion: 0.12
Nodes (18): AIChatPanel(), AIChatPanelProps, briefModule, fadeUp, getTimeGreeting(), stagger, SUGGESTION_CHIPS, ChatMarkdown() (+10 more)
### Community 49 - "Community 49"
Cohesion: 0.16
Nodes (21): AsyncSession, Fernet, _memory_cron_tick(), Hourly cron: drain Free-tier extraction queue + mine proactive patterns for Powe, audit_memory(), _audit_memory_inner(), _decay_proactive_patterns(), decay_relations() (+13 more)
### Community 50 - "Community 50"
Cohesion: 0.19
Nodes (11): Any, AsyncSession, datetime, Stripe service: checkout sessions, webhook handling, subscription management., Return the subscription record for ``user_id``, or ``None`` if absent., Cancel the user's Stripe subscription and downgrade them to free. Rai, Return recent invoices for the user from Stripe. Returns an empty lis, Wraps all Stripe interactions and owns subscription persistence. (+3 more)
### Community 51 - "Community 51"
Cohesion: 0.12
Nodes (13): Any, get_connector(), Connector registry — single source of truth for source_type -> connector., Return the registered connector for ``source_type`` or raise KeyError., Clear the registry — for use in pytest fixtures only., _reset_for_tests(), _clean_registry(), _DummyConnector (+5 more)
### Community 52 - "Community 52"
Cohesion: 0.20
Nodes (9): OAuthUserInfo, GET /auth/oauth/google/authorize and POST /auth/oauth/google/callback., Call /authorize and return the fresh state token., POST /callback with mocked provider exchange_code + get_userinfo., First-time Google login creates a new user and returns valid tokens., Second Google login with the same account re-uses the existing user., Verified Google email matching an existing password user links the accounts., Unverified Google email matching an existing account returns 409, not 500. (+1 more)
### Community 53 - "Community 53"
Cohesion: 0.10
Nodes (19): free_user_with_key(), pro_user_with_key(), Tests for Phase 3 — relational tier (Mem0g-light). Coverage: 1. upsert_rel, Free user: upsert_relation is silently skipped (no row created)., enrich_context includes relational_memory key for Pro user., Free user: relational_memory is empty list in enrich_context., decay_relations reduces confidence on stale rows., decay_relations deletes rows whose confidence drops below 0.2 threshold. (+11 more)
### Community 54 - "Community 54"
Cohesion: 0.33
Nodes (4): buildConditions(), DrizzleExecutor, getDb(), WsToolCall
### Community 55 - "Community 55"
Cohesion: 0.10
Nodes (19): compilerOptions, allowJs, baseUrl, esModuleInterop, jsx, lib, module, moduleResolution (+11 more)
### Community 56 - "Community 56"
Cohesion: 0.12
Nodes (13): AIMessage, AIMessageProps, blockAnimation, ChatSurface, ChatSurfaceProps, ContentSegment, INLINE_TAG_RE, MARKDOWN_COMPONENTS (+5 more)
### Community 57 - "Community 57"
Cohesion: 0.11
Nodes (5): manager(), _override_db(), Tests for Step 3.3: DeviceConnectionManager and device WS endpoint. Coverage:, Route all get_session calls to the test SQLite session., Fresh manager instance for each test.
### Community 58 - "Community 58"
Cohesion: 0.11
Nodes (3): Tests for v3 WebSocket frame protocol schemas., Backward compat: v2 types must remain., test_v2_frame_types_still_exist()
### Community 59 - "Community 59"
Cohesion: 0.18
Nodes (17): Any, _build_read_tools(), Brief agent — produces plain-text home and project status briefs. Read-only t, Stream a plain-text daily home brief. Yields (event_type, data) tuples id, Stream a plain-text project status brief for project_id. Yields (event_ty, _resolve_language(), run_home_brief(), run_project_brief() (+9 more)
### Community 60 - "Community 60"
Cohesion: 0.23
Nodes (18): Any, _all_tools(), _all_tools_for_user(), _as_text(), _contextual_tools(), _history_to_messages(), _memory_tools(), Run the contextual agent for a single user turn. Injects the rendered sco (+10 more)
### Community 61 - "Community 61"
Cohesion: 0.17
Nodes (14): Any, extract_canvas_block(), Output formatter for deep-agent stream events., Strip the first <canvas kind="...">...</canvas> block from *text*. Return, Convert `(event_type, data)` stream events into websocket frame models., StreamFormatter, StreamFormatter, _collect() (+6 more)
### Community 62 - "Community 62"
Cohesion: 0.16
Nodes (13): ChatChartBlock(), renderChart(), ChatEntityBlock(), ChatTableBlock(), ChartBlockData, ChartConfig, ChartContainer(), ChartContext (+5 more)
### Community 63 - "Community 63"
Cohesion: 0.13
Nodes (13): FormatPrefs, buttonVariants, Calendar(), CalendarDayButton(), DateTimeField(), DateTimeFieldProps, LayoutEntry, SegDef (+5 more)
### Community 64 - "Community 64"
Cohesion: 0.16
Nodes (16): UserProfile, ChatRequest, embed(), Return an embedding vector for *text*. Uses ``settings.LLM_EMBED_MODEL``, JSONResponse, brief(), _BriefRequest, _BriefResponse (+8 more)
### Community 65 - "Community 65"
Cohesion: 0.17
Nodes (4): logWsSend(), deepConvertKeys(), toCamelCase(), toSnakeCase()
### Community 66 - "Community 66"
Cohesion: 0.24
Nodes (13): ChatTimelineBlock(), ProjectTimeline(), TimelineEvent, ProjectGroup, ProjectTimelineBox(), DAY_PX_BY_ZOOM, TimelineGanttViewInner(), TimelineGanttViewProps (+5 more)
### Community 67 - "Community 67"
Cohesion: 0.12
Nodes (16): aliases, components, hooks, lib, ui, utils, rsc, $schema (+8 more)
### Community 68 - "Community 68"
Cohesion: 0.14
Nodes (14): Any, auth_header(), auth_headers_free(), Return an Authorization header dict for the given tier., Authorization header for the seeded free-tier user., _fake_token_stream(), Tests for Phase 3: brief agent WS frame + REST fallback. Coverage: - run_h, Fake _run_single_agent_stream that yields two token events. (+6 more)
### Community 69 - "Community 69"
Cohesion: 0.23
Nodes (15): _decrypt(), _encrypt(), _fernet_key(), Tests for Step 6 — memory ORM models and User.encryption_key. Uses the SQLite, POST /api/v1/auth/register creates a user with a valid Fernet key., User model has encryption_key column and it can be set., Deleting a user cascades to memory_core., test_memory_associative_create_and_read() (+7 more)
### Community 70 - "Community 70"
Cohesion: 0.17
Nodes (15): agent_runner_v2: email_action.html, agent_runner_v2: email_date.html, journey_v2: email_action.html, Action Item Extraction, Date / Meeting Extraction, Email Header Parsing (From/To/Subject/Date), Email HTML Preprocessing, Heavy HTML / Table Layout Stripping (+7 more)
### Community 71 - "Community 71"
Cohesion: 0.15
Nodes (13): ChatInputBox, ChatInputBoxHandle, ChatInputBoxProps, ChatInputBoxVariant, VARIANT_STYLES, BriefingResult, BriefMarkdown, ChatMessage (+5 more)
### Community 72 - "Community 72"
Cohesion: 0.30
Nodes (12): AsyncClient, _auth_headers(), Tests for cloud scout CRUD routes., test_authorize_draft_returns_url_and_no_scout_created(), test_create_cloud_scout_defaults_schedule(), test_delete_cloud_scout(), test_finalize_creates_scout_from_session(), test_finalize_rejects_unknown_session() (+4 more)
### Community 73 - "Community 73"
Cohesion: 0.13
Nodes (9): AuthError, PendingOAuth, RelationOut, RelationPatch, TOKEN_KEYS, OnboardingFlowProps, AuthTokensSchema, UserProfile (+1 more)
### Community 74 - "Community 74"
Cohesion: 0.19
Nodes (8): ItemContent, ItemMetadata, ItemRef, Source connector Protocol and shared item types. A SourceConnector adapts a t, Adapter for a third-party data source (Gmail, Slack, ...)., SourceConnector, TriageVerdict, Protocol
### Community 75 - "Community 75"
Cohesion: 0.13
Nodes (5): Unit tests for gmail._build_gmail_query., since=Feb is more recent than date_range.from=Jan, so after: should be Feb., date_range.from=Feb is more recent than since=Jan, so after: should be Feb., An invalid date string in filter_config must not raise, just be skipped., TestBuildGmailQuery
### Community 76 - "Community 76"
Cohesion: 0.18
Nodes (10): ContextualChatState, CachedChatState, ChatMessage, chatSessionCache, parseMutationsToEntityTags(), TABLE_TO_ENTITY, UIChatContext, UseAIChatReturn (+2 more)
### Community 77 - "Community 77"
Cohesion: 0.14
Nodes (13): env, browser, es6, node, extends, node, typescript, extensions (+5 more)
### Community 78 - "Community 78"
Cohesion: 0.19
Nodes (8): OnboardingFlow(), spring, Step, STEP_ORDER, INDUSTRIES, JOB_ROLES, TONES, USE_CASES
### Community 79 - "Community 79"
Cohesion: 0.16
Nodes (11): Integration tests for the unified WebSocket handler (Step 5). Tests the devic, tool_result for unknown call_id is silently ignored — no crash., Connection with bad token is closed before or after accept., Receive frames until stream_end or max_frames., home_request → stream_start, stream_text+, stream_end., request_id in home_request is echoed in all response frames., _recv_until_end(), test_home_request_produces_stream_frames() (+3 more)
### Community 80 - "Community 80"
Cohesion: 0.19
Nodes (13): adiuvAI Monorepo, .claude/CLAUDE.md (workspace guide), Root CLAUDE.md (project instructions), Custom ipcLink (not electron-trpc), Electron Desktop App, electron/index.html (Electron entry), electron/web.html (Web SPA entry), Gitea Electron Release Workflow (+5 more)
### Community 81 - "Community 81"
Cohesion: 0.19
Nodes (13): core/agent_runner.py, Agent Runner V2 eval cases, core/deep_agent.py (main agent runner), agents/filesystem_agent.py, Journey V2 eval cases, core/langfuse_client.py (prompt + tracing), core/llm.py (LiteLLM factory), core/output_formatter.py (+5 more)
### Community 82 - "Community 82"
Cohesion: 0.23
Nodes (13): api/backend-client.ts (WS client), Correctness Findings (CORR-01..CORR-21), core/device_manager.py (WS session tracking), device_ws.py — unified WS /device endpoint, Cross-Project WS Integration (/api/v1/device), api/drizzle-executor.ts (tool-call executor), Electron Main Process, noteEdits HITL Table (+5 more)
### Community 83 - "Community 83"
Cohesion: 0.18
Nodes (13): make_jwt(), Create a signed test JWT. Uses the fixed ``TEST_USER_IDS`` mapping so the, _device_hello(), Connect, send device_hello, receive ping, then close., Non-device_hello first frame should close the connection., tool_result frame is routed to the DeviceConnectionManager., On disconnect, _mark_runs_disconnected is called with the correct user_id., test_ws_device_disconnect_marks_run_logs_as_error() (+5 more)
### Community 84 - "Community 84"
Cohesion: 0.23
Nodes (9): AdiuvaIcon(), AdiuvaIconProps, AdiuvaTriggerButton(), clamp(), ContextualChatProvider(), Ctx, readNumber(), useContextualChat() (+1 more)
### Community 85 - "Community 85"
Cohesion: 0.18
Nodes (9): ASGIApp, Request, Response, BaseHTTPMiddleware, _get_user_id_from_jwt(), Tier-aware rate limiting middleware. Uses a per-user sliding-window counter (, Key function for the slowapi Limiter: returns JWT sub or remote IP., Sliding-window rate limiter applied globally across all non-exempt routes. (+1 more)
### Community 86 - "Community 86"
Cohesion: 0.24
Nodes (8): MemoryProactive, Per-user inferred behavioral patterns, encrypted at rest. Confidence in [, ExtractionResult, _enc(), Tests for Phase 5 — proactive hints surfacing. Coverage: 1. _proactive_hin, test_enrich_context_excludes_low_confidence_proactive(), test_enrich_context_returns_proactive_hints(), test_proactive_hints_in_system_prompt_string()
### Community 87 - "Community 87"
Cohesion: 0.33
Nodes (9): TableBlockData, Table(), TableBody(), TableCaption(), TableCell(), TableFooter(), TableHead(), TableHeader() (+1 more)
### Community 88 - "Community 88"
Cohesion: 0.21
Nodes (8): ThemeProvider(), createHttpLink(), webPlatform, Register, router, routeTree, App(), rootElement
### Community 90 - "Community 90"
Cohesion: 0.24
Nodes (9): _get_url(), Alembic migration environment — async-compatible. At runtime the app uses ``p, Convert an asyncpg URL to a psycopg2 URL for Alembic CLI., Emit SQL without a live DB connection., Run migrations against a live DB using the async engine., run_migrations_offline(), run_migrations_online(), run_migrations_online_async() (+1 more)
### Community 91 - "Community 91"
Cohesion: 0.20
Nodes (8): ASGIApp, Request, Response, Response sanitizer middleware. Scans JSON responses from the /api/v1/chat end, Scan *text* for prompt fragments and replace matches with ``[REDACTED]``., Strip prompt IP from /api/v1/chat JSON responses., _sanitize_text(), SanitizerMiddleware
### Community 92 - "Community 92"
Cohesion: 0.24
Nodes (4): BaseMessage, In-process TTL buffer for per-session LangChain message history. Stores the f, Append a synthetic system message to the buffer for the given session., _SessionBuffer
### Community 93 - "Community 93"
Cohesion: 0.20
Nodes (9): aiChatMessages, aiChatSessions, createIPCHandler(), IPCRequest, TRPCContext, aiChatRouter, ChannelSchema, RoleSchema (+1 more)
### Community 94 - "Community 94"
Cohesion: 0.24
Nodes (9): ipcLink(), electronPlatform, FileDialogOptions, FileDialogResult, Platform, PlatformContext, PlatformProvider(), App() (+1 more)
### Community 95 - "Community 95"
Cohesion: 0.36
Nodes (10): addDays(), addMonths(), DateKeywords, parseDate(), parseDateRange(), ParseInput, parseKeyword(), parseNumeric() (+2 more)
### Community 96 - "Community 96"
Cohesion: 0.24
Nodes (8): ContextualScope, Contextual sidebar scope schema and prompt block renderer. ContextualScope mi, Scope payload sent by the Electron renderer for contextual chat. The rend, Produce a single-paragraph human-readable summary of the current view for i, render_scope_block(), test_render_list_scope_no_entity(), test_render_note_scope_includes_char_count(), test_render_project_scope()
### Community 97 - "Community 97"
Cohesion: 0.22
Nodes (5): ABC, Any, BaseAgent, Minimal agent base types retained for compatibility with batch runners., Common base for non-chat agents still using the old base contract.
### Community 98 - "Community 98"
Cohesion: 0.27
Nodes (10): api/README.md (dev run instructions), routes/chat.py (HTTP /chat, /chat/brief, /chat/embed), GitHub Actions CI (lint/test/docker), docker-compose.yml (app + pgvector db), FastAPI Backend (api/), Gitea API Deploy Workflow (Docker LXC via SSH), routes/memory.py, core/note_summarizer.py (gpt-4o-mini) (+2 more)
### Community 99 - "Community 99"
Cohesion: 0.24
Nodes (9): create_app(), lifespan(), _memory_audit_cron_tick(), Every-24-hour cron: re-issue Gmail users.watch for scouts expiring within 24h., Weekly cron: contradiction scan + label canonicalization for all users (Phase 7), Every-15-min cron: poll enabled cloud scouts (cron-fallback; push is primary)., _scout_cron_tick(), _scout_watch_renewal_tick() (+1 more)
### Community 100 - "Community 100"
Cohesion: 0.20
Nodes (9): author, description, keywords, license, main, name, private, productName (+1 more)
### Community 101 - "Community 101"
Cohesion: 0.20
Nodes (10): scripts, build:web, dev:web, knip, lint, make, package, preview:web (+2 more)
### Community 103 - "Community 103"
Cohesion: 0.22
Nodes (8): make_query_relations_tool(), Relations agent — read-only tool wrapping MemoryMiddleware.query_relations., Return a query_relations tool bound to *user_id*., Any, _brief_research_tools(), Return memory tools that only read — safe for the read-only brief-agent subset., Return the full tool palette for Stage-1 task brief research (read-only)., _read_only_memory_tools()
### Community 104 - "Community 104"
Cohesion: 0.28
Nodes (9): auth/auth-manager.ts, auth/backup-key.ts (device AES-256 key), Google OAuth Flow (deep-link bounce), auth/oauth_providers.py (GoogleOAuthProvider), middleware/rate_limit.py (TierRateLimit), Redis Introduction (SEC-17/SEC-18 shared infra), Security Findings (SEC-01..SEC-36), ai/token.ts (two-tier token storage) (+1 more)
### Community 105 - "Community 105"
Cohesion: 0.28
Nodes (7): initialState, Theme, ThemeProviderContext, ThemeProviderProps, ThemeProviderState, useTheme(), Toaster()
### Community 106 - "Community 106"
Cohesion: 0.28
Nodes (8): build_brief_multi_project_manifest(), format_folder_manifest(), Build a compact multi-project manifest for the daily brief agent. Calls e, Format a folder manifest into the <linked_folder> block. Truncates by mti, test_brief_multi_project_manifest_top_5_per_project(), test_format_folder_manifest_basic(), test_format_folder_manifest_null_returns_empty(), test_format_folder_manifest_truncates_past_budget()
### Community 107 - "Community 107"
Cohesion: 0.22
Nodes (3): Tests for auth routes: register, login, refresh, me, OAuth social login. Exer, POST /api/v1/auth/register, TestRegister
### Community 108 - "Community 108"
Cohesion: 0.29
Nodes (8): routes/auth.py (register/login/OAuth/onboarding), Per-user Fernet Memory Encryption, i18n (i18next, 5 languages), core/memory_middleware.py (encrypted core memory), Onboarding Wizard (5 fields), app/models.py (SQLAlchemy 2.0 ORM), PostgreSQL pgvector:pg16 service, Zero-Trust Data Model
### Community 109 - "Community 109"
Cohesion: 0.25
Nodes (4): BriefingResult, sessionCache, UseTaskBriefCacheReturn, UseTaskBriefingReturn
### Community 110 - "Community 110"
Cohesion: 0.25
Nodes (3): Tier comes from the live subscription row, not the JWT claim., A JWT with ``exp`` in the past must be rejected., TestMe
### Community 111 - "Community 111"
Cohesion: 0.25
Nodes (3): _FakeLLM, _FakeTool, test_run_home_uses_mocked_tool_result()
### Community 113 - "Community 113"
Cohesion: 0.32
Nodes (7): _pubsub_payload(), Tests for the Gmail Pub/Sub webhook route. Covers: - Happy path: valid JWT, Build a minimal Pub/Sub push envelope., 204 returned and ScoutEngine.trigger_scout awaited for the matching scout., 401 returned when JWT verification fails., test_webhook_rejects_unverified_jwt(), test_webhook_triggers_scout_for_matching_user()
### Community 114 - "Community 114"
Cohesion: 0.43
Nodes (3): get_provider(), Return the correct provider client for *provider*. Parameters ------, TestGetProvider
### Community 115 - "Community 115"
Cohesion: 0.29
Nodes (6): ElectronAI, ElectronDialog, ElectronTRPC, TRPCResponse, V3StreamEvent, Window
### Community 116 - "Community 116"
Cohesion: 0.38
Nodes (3): POST /api/v1/auth/refresh, After rotation, the original refresh token must be rejected., TestRefresh
### Community 117 - "Community 117"
Cohesion: 0.33
Nodes (6): APScheduler Lifespan Crons (memory hourly + audit weekly), routes/billing.py (Stripe), core/memory_extraction.py, core/memory_maintenance.py, billing/stripe_service.py, billing/tier_manager.py (Tier System source of truth)
### Community 118 - "Community 118"
Cohesion: 0.33
Nodes (4): OAuthUserInfo, OAuth 2.0 + PKCE provider abstractions. Each provider implements a three-step, Fetch the authenticated user's identity from Google., Normalized user identity returned by any provider.
### Community 119 - "Community 119"
Cohesion: 0.33
Nodes (5): entry, ignore, ignoreDependencies, $schema, tags
### Community 122 - "Community 122"
Cohesion: 0.60
Nodes (6): ActivityBar(), CheckpointEvent(), getEventColor(), getEventState(), MilestoneEvent(), useDrag()
### Community 123 - "Community 123"
Cohesion: 0.40
Nodes (5): adiuvAI Brand Identity Showcase, Generic (Non-Email) Page Detection, Plain Text Fallback, preprocessors: fallback.txt, preprocessors: generic_page.html
### Community 125 - "Community 125"
Cohesion: 0.50
Nodes (3): embed_text(), OpenAI embedding helper for associative memory tier. Single public function:, Call OpenAI text-embedding-3-small. Return None on failure (caller falls back to
### Community 126 - "Community 126"
Cohesion: 0.50
Nodes (3): RovingDirection, RovingItemProps, UseRovingFocusOptions
### Community 127 - "Community 127"
Cohesion: 0.50
Nodes (3): ChatMessage, A single Teams chat or channel message fetched from MS Graph., Return a human-readable text representation for LLM extraction.
### Community 129 - "Community 129"
Cohesion: 0.50
Nodes (3): Tests for contextual WS frame handlers. These tests only exercise the new han, _handle_contextual_scope_update must: - call append_system_message on the s, test_handle_contextual_scope_update_appends_system_message_no_llm()
### Community 130 - "Community 130"
Cohesion: 0.50
Nodes (3): Tests for run_contextual_stream. These tests monkeypatch _run_single_agent_st, run_contextual_stream must inject the scope block into the system prompt an, test_run_contextual_stream_includes_scope_block()
### Community 131 - "Community 131"
Cohesion: 1.00
Nodes (3): agent_runner_v2: email_info.html, journey_v2: email_info.html, Informational Email (No Action Needed)
### Community 135 - "Community 135"
Cohesion: 0.67
Nodes (3): Enum, WsFrameType, str
## Ambiguous Edges - Review These
- `Generic (Non-Email) Page Detection` → `adiuvAI Brand Identity Showcase` [AMBIGUOUS]
electron/assets/logo/brand-showcase.html · relation: conceptually_related_to
## Knowledge Gaps
- **492 isolated node(s):** `Any`, `ASGIApp`, `Response`, `ASGIApp`, `Request` (+487 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **28 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **What is the exact relationship between `Generic (Non-Email) Page Detection` and `adiuvAI Brand Identity Showcase`?**
_Edge tagged AMBIGUOUS (relation: conceptually_related_to) - confidence is low._
- **Why does `run_cloud_agent()` connect `Device & Langfuse` to `Community 43`, `Community 114`, `WS Session Buffer`, `Scout Config Models`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._
- **Why does `get_provider()` connect `Community 114` to `Community 43`, `Community 44`, `Device & Langfuse`, `Community 31`?**
_High betweenness centrality (0.041) - this node is a cross-community bridge._
- **Why does `cn()` connect `Auth UI Forms` to `Folder & Listbox UI`, `Chat Entity Blocks`, `Scout Run UI`, `Community 66`, `Community 39`, `Files Section UI`, `Community 78`, `Community 87`, `Community 26`, `Community 62`, `Community 63`?**
_High betweenness centrality (0.037) - this node is a cross-community bridge._
- **Are the 85 inferred relationships involving `MemoryMiddleware` (e.g. with `alias` and `Any`) actually correct?**
_`MemoryMiddleware` has 85 INFERRED edges - model-reasoned connections that need verification._
- **What connects `Alembic migration environment — async-compatible. At runtime the app uses ``p`, `Convert an asyncpg URL to a psycopg2 URL for Alembic CLI.`, `Emit SQL without a live DB connection.` to the rest of the system?**
_1043 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Auth UI Forms` be split into smaller, more focused modules?**
_Cohesion score 0.03288314738696418 - nodes in this community are weakly interconnected._

307
graphify-out/graph.html Normal file

File diff suppressed because one or more lines are too long

108760
graphify-out/graph.json Normal file

File diff suppressed because it is too large Load Diff

25
skills-lock.json Normal file
View File

@@ -0,0 +1,25 @@
{
"version": 1,
"skills": {
"boost-prompt": {
"source": "github/awesome-copilot",
"sourceType": "github",
"computedHash": "2621a44fbd9fc2636953d1e6e39e5faeed995f7fb958ec12cc98a2f0576f6fa7"
},
"langfuse": {
"source": "langfuse/skills",
"sourceType": "github",
"computedHash": "a72c15d6329867b84c4e7456f4e04d8c06ce5298d1d0068dbe864b8b00fb406c"
},
"remotion-best-practices": {
"source": "remotion-dev/skills",
"sourceType": "github",
"computedHash": "a6a79c92a109339759d7b465f2d45fe11ce955e307a3e2c03fbcf4f9b624b77a"
},
"shadcn": {
"source": "shadcn/ui",
"sourceType": "github",
"computedHash": "03365c7543539f6e6689a754e65b3d8ea023ba9d6ea12a9b5550462bf060af8c"
}
}
}