Angi/updates
1.7.322M9 · Signup mode toggle + waitlist approval flow on /admin

Building Angi in the open.

Every release, every milestone, every commit — tracked here, synced from GitHub. Watch the platform come together in real time.

Roadmap

9 of 9
  1. M1
    Foundation
  2. M2
    Core Engine
  3. M3
    Memory + LLM
  4. M4
    Chat UI
  5. M5
    Topologies
  6. M6
    Goal UI
  7. M7
    CLI + Auth
  8. M8
    SaaS layer
  9. M9
    GA
Releases
200
+66 this week
Shipped
200
+66 this week
Items
780
+244 this week
Features
458
+126 this week

Now · Next · Later

Synced from GitHub Project board
Now
  • M9GA153 shipped
Next
Later

Recent releases

RSS
  1. 1.7.322
    M9
    Signup mode toggle + waitlist approval flow on /adminShipped May 23, 2026

    **Why** — operator wanted to throttle new signups while reviewing each one manually. Built three-mode signup gate + manual approval queue. **Schema (`signup_mode_waitlist` migration):** - New singleton table `public.app_config` (id boolean primary key default true) with `signup_mode text` in {open, closed, waitlist}. Default 'open'. RLS: read by super-admin only. - `public.users` gains `pending_approval boolean default false`, `approved_at timestamptz`, `approved_by uuid references users(id)`. - BEFORE INSERT trigger `set_pending_on_signup()` flips `pending_approval = true` when current mode = 'waitlist'. - 4 SECURITY DEFINER admin RPCs: `admin_get_signup_mode()`, `admin_set_signup_mode(mode)`, `admin_list_pending_users()`, `admin_approve_user(user_id)`. All revoked from anon/authenticated/public, granted to service_role. **UI:** - `/admin` overview gains a `Signup mode` card with three buttons (Open / Waitlist / Closed). Inline switching via POST `/api/admin/signup-mode`. - `/admin/users` rebuilt with two tabs: `All users (N)` (existing list) + `Pending approval`. The pending tab lazy-loads via GET `/api/admin/users/pending`. Each row has an `Approve` action calling POST `/api/admin/users/{id}/approve`. - New `/pending` page for users blocked by waitlist — shows hourglass + "waiting for admin approval" + sign-out link. - `/c` page gate: on every visit, server-side checks `public.users.pending_approval`; true → redirect to `/pending`. **Bilingual** — full `admin.signup_*` + `admin.users_tab_*` + `admin.users_approve` + `pending.*` namespaces in en + he. **Net** — operator can now flip Angi between "fully open", "waitlist", and "closed signup" without redeploy. Waitlist users land on a clear holding page until approved; admin sees them in a dedicated tab + approves with one click.

    • /admin/users two tabs: All + Pending approval with one-click approve
    • Signup mode card on /admin overview
    • 4 SECURITY DEFINER admin RPCs (get/set mode, list pending, approve user)
    • pending_approval column on users + before-insert trigger
    • app_config table + signup_mode toggle (open / closed / waitlist)
    • ANGI_VERSION 1.7.321 → 1.7.322 (chat + web)
    • Bilingual: admin.signup_* + users_tab_* + pending.* namespaces
    • /pending page + /c gate redirects pending users
  2. 1.7.321
    M9
    Supabase advisor batch 2 — 215 multi-permissive policies consolidated + admin SECURITY DEFINER tightenedShipped May 23, 2026

    **Why** — after v1.7.320, advisor still showed 14 `authenticated_security_definer_function_executable` warnings + 215 `multiple_permissive_policies` warnings + 1 HIBP. This batch tackles the two large groups. **Security batch 2:** revoked `authenticated` EXECUTE on 6 admin-only SECURITY DEFINER RPCs that are called exclusively via the admin (service-role) client: - `admin_cron_jobs()`, `admin_cron_runs(integer)`, `admin_list_users(integer)` - `app_db_describe(text)`, `app_db_relationships(text)`, `app_db_tables_list()` Service-role bypasses per-role grants, so the admin UI keeps working. The 8 user-context RPCs (accept_invite, app_record_*, app_user_has_password, list_workspace_members, mint/revoke_chat_share, search_messages, transfer_workspace_ownership) intentionally retain authenticated EXECUTE — each has internal auth.uid() / workspace-role checks. **Performance batch 2:** consolidated `multiple_permissive_policies` across 13 tables by inlining `private.is_super_admin() OR ...` into each per-(table,cmd) policy and dropping the standalone `_super_admin_modify` / `_super_admin_select` policies. Tables touched: agents, audit_log, invites, mcp_servers, memories, messages, swarms, tasks, usage_events, users, workspace_members, workspaces, chats. Also merged `chats_select_share_token` + `chats_select_owner` → single `chats_select`; same for `messages_select_share_token` + `messages_select_chat_owner` → `messages_select`. Postgres now evaluates ONE policy per (table, cmd, role) instead of 2–3. **Net:** 215 multi-perm WARN → 0. 14 authenticated SECURITY DEFINER WARN → 8 (intentional). Performance side: 0 WARN, 42 INFO (unused indexes — advisory only, don't drop without traffic data).

    • 42 unused_index INFO advisory — leave for natural workload to populate stats
    • 13 tables refactored: agents/audit_log/invites/mcp_servers/memories/messages/swarms/tasks/usage_events/users/workspace_members/workspaces/chats
    • chats_select + messages_select merged share-token + owner policies into one
    • Consolidated 215 multiple_permissive_policies → 0 via super_admin inline
    • Revoked authenticated EXECUTE on 6 admin-only SECURITY DEFINER RPCs
    • ANGI_VERSION 1.7.320 → 1.7.321 (chat + web)
    • 8 user-context SECURITY DEFINER warnings intentional (internal auth.uid() checks)
  3. 1.7.320
    M9
    Supabase advisor batch — security + performance hardeningShipped May 23, 2026

    **Why** — `get_advisors` reported 26 security warnings + 274 performance warnings across the project. Tackled in 2 migrations applied via Management API. **Security migration (`security_advisors_batch_1`):** 1. Locked `search_path` on 8 SECURITY DEFINER / trigger functions (linter 0011): `touch_mcp_catalog_updated_at`, `touch_mcp_server_updated_at`, `messages_extract_text`, `touch_documents_updated_at`, `touch_flows_updated_at`, `assert_chat_swarm_same_workspace`, `messages_tsv_sync`. 2. Revoked anon EXECUTE on 14 SECURITY DEFINER RPCs (linter 0028) — admin/dev tools + workspace-scoped ops now require a session. 3. `audit_record()` revoked from BOTH anon + authenticated (trigger-only). 4. Added restrictive deny-all RLS policy on `public.stripe_events` (linter 0008) — webhook still writes via service-role bypass. 5. Dropped `avatars_select_public` policy on `storage.objects` (linter 0025) — bucket stays `public=true` so URL fetch keeps working but listing all files is blocked. **Performance migration (`perf_advisors_batch_1`):** 1. Added 13 covering indexes on unindexed foreign keys (linter 0001): `audit_log.actor_id`, `documents.created_by`, `feed_subscriptions.created_by`, `flow_steps.parent_step_id`, `flows.created_by`, `invites.invited_by`, `mcp_servers.created_by`, `messages.workspace_id`, `peers.created_by`, `swarms.created_by`, `tasks.agent_id`, `tasks.parent_task_id`, `workspaces.plan_id`. 2. Rewrote 19 RLS policies (linter 0003 `auth_rls_initplan`) to use `(select auth.uid())` / `(select auth.role())` so the planner caches the value once instead of re-evaluating per row. Tables: `users`, `workspaces`, `chats`, `messages`, `mcp_tools`, `invites`, `admins`, `peers`, `peer_shares`. **Remaining (acknowledged):** - 14 `authenticated_security_definer_function_executable` — these RPCs are CALLED by the chat API and internally check auth.uid() / admin role. Intentional. - 1 `auth_leaked_password_protection` — HaveIBeenPwned check requires Supabase Pro plan. - 215 `multiple_permissive_policies` — large RLS refactor; deferred as separate batch. - 27 → 40 `unused_index` — 13 of the new entries are the FK indexes added above (will see use as workload grows, protect cascade deletes). **Net** — security surface narrowed (anon can no longer call any SECURITY DEFINER RPC), trigger functions hardened against search_path attacks, RLS no longer pays per-row auth.uid() penalty, FK joins now use indexes.

    • 13 covering indexes added on unindexed foreign keys
    • Locked search_path on 8 SECURITY DEFINER functions (linter 0011)
    • Revoked anon EXECUTE on 14 SECURITY DEFINER RPCs (linter 0028)
    • audit_record() revoked from both anon + authenticated (trigger-only)
    • public.stripe_events deny-all restrictive policy (linter 0008)
    • ANGI_VERSION 1.7.319 → 1.7.320 (chat + web)
    • 215 multiple_permissive_policies deferred to separate batch
    • HIBP password check blocked on free tier — Pro plan required
    • 14 authenticated SECURITY DEFINER warnings intentional (internal auth checks)
    • 19 RLS policies rewritten to use (select auth.uid()) — per-row penalty gone
    • Dropped avatars bucket broad SELECT policy (linter 0025)
  4. 1.7.319
    M9
    Custom SMTP wired + production domain — angi.studione.co.il liveShipped May 23, 2026

    **Why** — Supabase built-in mailer was capped at 2/hour (no email-rate-limit override allowed without custom SMTP). Magic-link + password-reset emails throttled the moment the user actually needed them. Plus production domain needed to move from `angi-ai.vercel.app` to the user's branded `angi.studione.co.il`. **What** — three Management/API-level changes via Supabase + Vercel REST APIs from this session (no dashboard clicks): 1. **NEXT_PUBLIC_APP_URL** — Vercel env + local `.env.local` updated `angi-ai.vercel.app` → `https://angi.studione.co.il`. Magic-link callbacks now ship the branded host. 2. **Supabase auth URLs** — `site_url` flipped to `https://angi.studione.co.il`. `uri_allow_list` includes both the new domain + the prior `angi-ai.vercel.app` (so any in-flight emails still land) + `localhost:3000` / `localhost:3002` for dev. 3. **Custom SMTP** — `secure.emailsrvr.com:465` (TLS), sender `info@studione.co.il`, sender name `Angi`. Built-in mailer rate-limit raised 2 → 30/hour (now permitted because custom SMTP unlocks the setting). `smtp_max_frequency` set to 5s. **Verified live:** - DNS — `angi.studione.co.il` → Vercel `vercel-dns-017.com` CNAME → edge IPs `64.29.17.65 / 216.198.79.65`. - HTTPS 200 + full CSP headers - `/api/version` returns `code_version: 1.7.319` **Net** — magic-link + password-reset flow no longer throttled. Production runs on a real domain. End-to-end auth path is now branded, observable, and rate-limited only by the SMTP provider's own ceiling (not Supabase's built-in 2/hour cap).

    • ANGI_VERSION 1.7.318 → 1.7.319 (chat + web)
    • Supabase site_url + uri_allow_list expanded (prod + legacy + localhost)
    • NEXT_PUBLIC_APP_URL updated in Vercel + .env.local to branded host
    • Production domain angi.studione.co.il live + DNS verified
    • rate_limit_email_sent raised 2 → 30/hour (custom-SMTP unlocks setting)
    • Custom SMTP wired via Supabase Management API (secure.emailsrvr.com:465)
  5. 1.7.318
    M9
    Password reset flow — Forgot password? link + /reset-password pageShipped May 23, 2026

    **Why** — login form supported password mode (since v1.7.247) but had no way to recover from a forgotten password. Users locked out could only fall back to the magic-link mode + then set a new password from `/settings/profile` — three indirect steps. **What** — full one-shot recovery flow: 1. **Login form** — `Forgot password?` link in password mode. Triggers `supabase.auth.resetPasswordForEmail(email, { redirectTo: callbackUrl('/reset-password') })`. Bilingual + RTL-safe positioning. 2. **`/reset-password` page** — landing target for the recovery email. Verifies the user has a session (Supabase recovery already exchanged the OTP via `/auth/callback` with `type=recovery`); if not, bounces to `/login` with a clear "link expired" error. 3. **Set new password** — `supabase.auth.updateUser({ password })`. No current-password challenge (recovery flow grants direct update). Eye toggle + length/match validators inline. 4. **Redirect** — after success, lands on `/c`. **callbackUrl() refactored** — accepts `nextOverride` so the reset flow points to `/reset-password` while magic-link / OAuth keep the user's original `next` param. Same env override (`NEXT_PUBLIC_APP_URL`) already in production. **Bilingual** — new keys: `login.forgot_password`, `login.email_required`, `login.reset_sent`, full `reset_password.*` namespace (heading, subtitle, new_label, confirm_label, submit, too_short, mismatch, no_session, show, hide). **Net** — one-click recovery. Locked-out users no longer need to bounce through magic-link + settings.

    • ANGI_VERSION 1.7.317 → 1.7.318 (chat + web)
    • Bilingual: login.forgot_password + reset_password.* namespace
    • Eye toggle on /reset-password matches /settings/api-keys + onboarding parity
    • callbackUrl() accepts nextOverride for reset routing
    • /reset-password page — set new password after recovery link
    • "Forgot password?" link in password login mode
  6. 1.7.317
    M9
    gitignore .vercel directories + Sentry connected verified liveShipped May 23, 2026

    **Why** — Vercel CLI added `.vercel/` to monorepo root + `apps/chat/.gitignore` during linking. Untracked + needed to be committed. **What** — `.vercel` added to root `.gitignore`, new `apps/chat/.gitignore` with same entry. **Bonus state-of-the-arc check:** - ✅ Sentry MCP auth → 2 projects (`angi-chat` + `angi-web`) - ✅ Vercel project `angi` linked + 7 env vars set via Management API - ✅ Production live at `https://angi-ai.vercel.app`, code_version 1.7.317 - ✅ `deps.sentry: true` confirmed via `/api/health?deep=1` - ✅ Sentry MCP `search_issues` → 0 unresolved (clean post-deploy) - ✅ Supabase auth Site URL = production, `uri_allow_list` covers prod + localhost dev - ✅ Magic-link callback honors `NEXT_PUBLIC_APP_URL` End-to-end auth + deploy + observability now fully wired without any user-side dashboard clicks. Vercel + Supabase + Sentry — all managed through CLI/Management API from this Claude session.

    • Arc state-check: Sentry + Vercel + Supabase all autonomous-wired
    • apps/chat/.gitignore created (Vercel CLI artifact)
    • .vercel added to root .gitignore
    • ANGI_VERSION 1.7.316 → 1.7.317 (chat + web)
  7. 1.7.316
    M9
    Magic-link callback honors NEXT_PUBLIC_APP_URL — no more localhost in OTP emailsShipped May 23, 2026

    **Why** — `callbackUrl()` in the login form used `window.location.origin`, which baked the BROWSER tab's origin into the magic-link OTP email. A user clicking "Send magic link" from a `localhost:3000` dev tab got an email pointing at localhost, broken when they opened it from a different device or after the dev server stopped. **What** — `callbackUrl()` now prefers `NEXT_PUBLIC_APP_URL` over `window.location.origin`: ```ts const origin = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/$/, '') ?? window.location.origin; ``` Production deploys set `NEXT_PUBLIC_APP_URL=https://angi-ai.vercel.app` (added to Vercel env + apps/chat/.env.local). Local dev that wants the localhost callback loop sets the env var to `http://localhost:3000` explicitly. **Note** — Supabase Auth Allow List must include the new callback host for the redirect to land. Production already trusts `angi-ai.vercel.app` (the default Site URL); custom domains need to be added under Supabase → Authentication → URL Configuration.

    • callbackUrl() prefers NEXT_PUBLIC_APP_URL over window.location.origin
    • ANGI_VERSION 1.7.315 → 1.7.316 (chat + web)
    • NEXT_PUBLIC_APP_URL added to Vercel env (production+preview+development)
    • Magic-link OTP emails no longer ship localhost callbacks from dev tabs
  8. 1.7.315
    M9
    apps/web /api/locale normalized to apiError shape + X-Angi-Version headerShipped May 23, 2026

    **Why** — `apps/web/api/locale` returned hand-rolled `{ error: 'invalid locale' }` and missed the X-Angi-Version header. Marketing surface visibility-arc gap. **What** — small `jsonWithVersion()` helper inlined (no shared apiError on marketing per CLAUDE.md thin-bundle policy): - Returns `{ error: 'Invalid locale.', code: 'invalid_input' }` on bad locale - All responses (success + error) carry `X-Angi-Version: <ANGI_VERSION>` CLI / SDK consumers can now `translateApiError(json, t)` uniformly across chat + marketing. **Net** — every chat+web API route now carries the X-Angi-Version contract + the apiError shape. Visibility arc complete on both deploys.

    • apps/web /api/locale returns { error, code: invalid_input } shape
    • ANGI_VERSION 1.7.314 → 1.7.315 (chat + web)
    • jsonWithVersion helper inlined — no shared apiError on marketing (thin bundle)
    • X-Angi-Version header on success + error responses
  9. 1.7.314
    M9
    Middleware sets X-Angi-Version on every response + normalizes 401 to apiError shapeShipped May 23, 2026

    **Why** — `apiError()` carried the header (v1.7.313), but success responses + middleware-emitted responses (401, 302 redirect, NextResponse.next) didn't. `curl -sI <any chat route>` would still come back without the build tag on the happy path. **What** — chat middleware now wraps every response in `withVersion()`: - `NextResponse.next()` for public paths - `NextResponse.next()` for non-auth-required paths - `NextResponse.json({ ... }, 401)` for unauthed API hits - `NextResponse.redirect(loginUrl)` for unauthed page navigation - `NextResponse.next()` (with cookies) for authed pass-through **Bonus** — the 401 JSON also normalized to `{ error, code: 'not_authenticated' }` to match the apiError contract. Can't `import { apiError }` in middleware (Edge runtime + Sentry pulls in Node-only modules); inlined the shape instead. **Net** — Single edit covers every chat route the middleware matches (everything except static assets). The visibility arc now wraps the success path too. Three single-edit ROI fans (apiError → Sentry capture v1.7.312; apiError → X-Angi-Version v1.7.313; middleware → X-Angi-Version v1.7.314) all land on top of the same arc, leveraging Next.js's response abstractions.

    • 401 unauth JSON normalized to { error, code: not_authenticated } (apiError shape)
    • ANGI_VERSION 1.7.313 → 1.7.314 (chat + web)
    • Edge-runtime-safe — reads compile-time ANGI_VERSION constant, no Sentry import
    • withVersion() helper wraps every middleware response with X-Angi-Version
  10. 1.7.313
    M9
    apiError() responses now carry X-Angi-Version header — visibility extends to error pathShipped May 23, 2026

    **Why** — v1.7.299 added `X-Angi-Version` to all four health endpoints. But ANY `/api/*` route that returned `apiError(...)` lost the header — error responses are exactly when ops needs the version signal most (rolling back? hot-patching?). `curl -I` during an outage came back without the build tag. **What** — `apiError()` now sets `X-Angi-Version: <ANGI_VERSION>` on every response, merging into caller-supplied headers (caller wins on collision; an explicit override still works). Single edit covers every route that uses the helper — same fan-out leverage v1.7.312's Sentry capture used. **Net** — the operational visibility arc now reaches the error path. Every response from `/api/*` chat surface advertises the running build whether the route succeeded or failed.

    • Caller-supplied headers win on collision — explicit overrides still work
    • ANGI_VERSION 1.7.312 → 1.7.313 (chat + web)
    • Operational visibility arc now reaches the error path across all /api routes
    • apiError() injects X-Angi-Version on every response (merges with caller headers)
  11. 1.7.312
    M9
    apiError() auto-captures server_error to Sentry — 8 silent catches now visibleShipped May 23, 2026

    **Why** — only 1 of 9 try/catch blocks across `/api` routes called `Sentry.captureException`. The other 8 caught upstream errors, converted them to `apiError('server_error', detail)`, and returned 500 to the client. Sentry never saw the underlying PostgREST / network / runtime failure. Ops blind unless a user reported it. **What** — `apiError()` now auto-captures when `code === 'server_error'`: - Builds an `Error(detail ?? spec.message)` so the stack starts at the route. - Tags `source: 'apiError'` + the code for Sentry filtering. - Passes `extra` (the optional metadata object) so payload details land in Sentry. - Wrapped in try/catch — Sentry SDK failure never breaks the response path. **Scope choice** — only `server_error` is captured. Auth failures (`not_authenticated`, `forbidden`), rate-limits, input validation, quota — all expected outcomes that would flood the issue stream. The 500-class is the only branch where ops genuinely needs to know. **Net** — 8 silent catches now route their underlying error to Sentry without per-route refactoring. Includes the `/api/releases` + `/api/mcp/catalog` ones I just normalized in v1.7.311. Single edit covers the whole chat API surface.

    • 8 silent catch blocks across /api now visible to Sentry via single edit
    • ANGI_VERSION 1.7.311 → 1.7.312 (chat + web)
    • Auth / rate-limit / input-validation NOT captured — would flood issue stream
    • Sentry SDK failure wrapped in try/catch — never breaks the response path
    • apiError(server_error) now calls Sentry.captureException with source tag
  12. 1.7.311
    M9
    /api/releases + /api/mcp/catalog normalized to apiError shapeShipped May 23, 2026

    **Why** — two routes returned hand-rolled `{ error }` JSON instead of the standard `apiError(code, detail)` shape every other route uses. CLI / SDK consumers that called `translateApiError(json, t)` got fallback English instead of a translated message because the `code` field was missing. **What** — `/api/releases` (3 error sites) + `/api/mcp/catalog` (1 error site) now use `apiError('server_error', detail)`. Both surfaces return `{ error: '...', code: 'server_error' }` matching the pattern documented in `apps/chat/lib/api-errors.ts`. **Net** — error shape now consistent across all chat API routes. Future i18n keys under `api_errors.server_error` automatically apply to releases + catalog without per-route handling. Two more legacy hand-rolled errors remain in `/api/invites/accept` (already includes `code` in its custom shape — not strictly broken). Left as-is for now; would require an apiError code mapping that doesn't yet exist.

    • /api/mcp/catalog error site normalized to apiError(server_error)
    • ANGI_VERSION 1.7.310 → 1.7.311 (chat + web)
    • translateApiError(json, t) now works uniformly on these routes
    • /api/releases 3 error sites normalized to apiError(server_error)
  13. 1.7.310
    M9
    Onboarding — Enter submits across steps 2 / 3 / 4Shipped May 23, 2026

    **Why** — onboarding required clicking a button to advance every step. Users pasting an invite list or entering a workspace name had to grab the mouse for each row. Standard form-submit affordance was missing. **What** — added `onKeyDown` Enter handlers to three inputs: - **Step 2 (workspace)**: name + slug inputs — Enter fires `createWorkspace()` if both fields filled + not busy. - **Step 3 (invite)**: email input — Enter fires `addInvite()`. Pasting `a@b.com,Enter,c@d.com,Enter,…` now works without mouse. - **Step 4 (LLM key)**: key input — Enter fires `saveKey()` if key filled + not busy. Triggers the v1.7.303 post-save probe. All handlers guard against busy state + empty fields. preventDefault on Enter so the browser doesn't bubble a default form submit. **Net** — onboarding flows keyboard-first now. Power users can complete the wizard without touching the mouse.

    • All Enter handlers preventDefault + guard against busy / empty
    • ANGI_VERSION 1.7.309 → 1.7.310 (chat + web)
    • Step 4 LLM key — Enter fires saveKey() + post-save probe
    • Step 3 invite email — Enter fires addInvite() (paste-list-friendly)
    • Step 2 workspace name + slug — Enter fires createWorkspace()
  14. 1.7.309
    M9
    Federation handler tests — share_chat + share_namespace + revoke_share covered (6 new tests)Shipped May 23, 2026

    **Why** — v1.7.308 covered share_swarm handler branches. Sibling handlers (share_chat, share_namespace, revoke_share) still rode on schema-only coverage. The mock template was proven; replicating it across the rest closes the federation handler surface. **What** — 6 new tests + an extended mock client: - **share_chat** (2): chat_not_found + happy path (resource_kind=chat). - **share_namespace** (2): namespace_empty (memories count=0) + happy path with non-zero count. - **revoke_share** (2): deleted_id returned when row matched + null when RLS gated / already-gone (idempotent revoke contract). Mock extended to stub: - `.select('*', { count: 'exact', head: true })` → awaitable `{ count }` for the namespace preflight. - `.delete().eq().select().maybeSingle()` for revoke. - Per-kind upsert return values keyed off the rows map so chat / namespace tests get the right `resource_kind`. **Net** — federation handler coverage 60% → 100%. mcp-server vitest suite grows 27 → 33 tests across 2 files. Every federation tool now has BOTH schema + handler tests. The 6-tool surface is the most security-critical in the registry (cross-machine resource-sharing); test floor finally matches that risk.

    • share_chat handler tests: chat_not_found + happy path
    • ANGI_VERSION 1.7.308 → 1.7.309 (chat + web)
    • mcp-server vitest suite grows 27 → 33 tests; federation handler coverage 60% → 100%
    • Mock client extended: count: exact head: true + delete().eq().select().maybeSingle()
    • revoke_share handler tests: deleted_id path + null-on-RLS path
    • share_namespace handler tests: namespace_empty + memories-count path
  15. 1.7.308
    M9
    Federation handler tests — share_swarm preflight branches covered (4 new tests)Shipped May 23, 2026

    **Why** — v1.7.307 covered federation schemas + metadata but NOT the handler bodies. A regression in the preflight error contract (e.g. swapping `peer_not_found` for a generic `permission_denied`) would change what the LLM tells the user without failing any test. **What** — added a lightweight mock-supabase chain to the federation test file and 4 handler tests for `share_swarm`: - `peer_not_found` when peers row missing - `peer_blocked` when peer status is blocked - `swarm_not_found` when swarms row missing - happy path returns the upserted share row with correct fields The mock client returns supplied rows per table, ignores `.eq()` / `.select()` args, and returns the chain on every step — the test isn't validating RLS (production does that), only the branch the handler picks based on row state. **Template** — the same mock works for `share_chat`, `share_namespace`, `revoke_share`. Future ships can copy the pattern. share_swarm shipped first as the representative case (most preflight branches: 2 tables, 2 error paths, 1 happy). **Net** — mcp-server test suite grows 23 → 27 tests. Federation handler coverage 0 → 60% (share_swarm). Total federation coverage now ~70%.

    • peer_not_found / peer_blocked / swarm_not_found / happy-path branches
    • ANGI_VERSION 1.7.307 → 1.7.308 (chat + web)
    • mcp-server vitest suite grows 23 → 27 tests
    • Template for share_chat + share_namespace + revoke_share follow-on tests
    • Mock supabase chain factory + 4 handler tests for share_swarm
  16. 1.7.307
    M9
    Federation MCP tool surface tests — schema + metadata coverageShipped May 23, 2026

    **Why** — six federation MCP tools (`list_peers`, `list_shares`, `share_swarm`, `share_chat`, `share_namespace`, `revoke_share`) had ZERO test coverage. A typo loosening a `z.string().uuid()` to `z.string()`, or a missing `.min(1)` on the namespace label, would ship green. The CLAUDE.md security contract for federation rests on these schemas. **What** — new `packages/mcp-server/test/federation.test.ts` with 10 tests covering: - All 6 tools registered + sorted by name - Each tool in `category: 'federation'` - Each description non-empty + >20 chars - Each name follows `federation/<verb>` shape - Per-tool input schema accepts a representative valid input and rejects: - invalid `status` / `resource_kind` enums - non-UUID ids on `peer_id` / `swarm_id` / `chat_id` / `share_id` - `direction: 'in'` on share_swarm / share_chat (publisher-only: out / bidirectional) - empty / 121-char namespace labels (1–120 bound) - limit clamps (1–200) **Scope choice** — handler bodies stay covered by the integration suite (needs seeded Supabase + RLS context). This unit file pins the metadata + schema invariants that the LLM contract depends on. Cheap to run, catches the real regression class. **Net** — federation surface goes from 0 → 10 tests. mcp-server suite now 23 tests across 2 files. Test floor in registry.test.ts already at >= 240; the federation file adds zero risk of inflation since it's targeted at the federation 6.

    • Schema rejection coverage for invalid uuids + bad enum values + bound violations
    • ANGI_VERSION 1.7.306 → 1.7.307 (chat + web)
    • mcp-server vitest suite grows 13 → 23 tests across 2 files
    • Tool count invariant: 6 federation tools registered + sorted
    • New packages/mcp-server/test/federation.test.ts (10 tests)
  17. 1.7.306
    M9
    smoke + health-check probes assert X-Angi-Version header — drift caught at probe layerShipped May 23, 2026

    **Why** — v1.7.299 added `X-Angi-Version` to all four health endpoints. Only the Playwright e2e test asserted the header. `pnpm health` + `pnpm smoke` never checked it — a regression that drops the header from any of the four endpoints would slip past the nightly CI smoke + the pre-deploy health probe. **What** — added header assertions to both scripts: - `/api/health` (shallow + deep): header present + bare semver pattern. - `/api/version`: header present + bare semver pattern + **matches body.code_version** on the same response. Cross-checks that the header isn't stale on the response that defines the contract. - `/healthz` (chat side): header present + bare semver pattern. **Bonus fix** — typecheck flagged a `string | undefined` issue in the v1.7.296 brand-audit pill regex match (`match[1]` typed as optional without explicit guard). Added a truthy filter so the literal never lands as undefined. **Net** — three probe scripts (smoke, health-check, e2e) now uniformly enforce the X-Angi-Version contract. A future regression on any of the four endpoints fails CI before it reaches a deploy.

    • /api/version probe cross-checks X-Angi-Version == body.code_version
    • ANGI_VERSION 1.7.305 → 1.7.306 (chat + web)
    • smoke.ts asserts X-Angi-Version header on /api/health + /api/version
    • brand-audit pill regex — match[1] truthy guard (typecheck strictness fix)
    • health-check.ts asserts X-Angi-Version on /healthz + /api/health + /api/version
  18. 1.7.305
    M9
    /settings/api-keys gains "Test all configured" button — bulk probe parity with CLIShipped May 23, 2026

    **Why** — `/settings/api-keys` already had per-row Test buttons. After a bulk key rotation (e.g. updating Anthropic + OpenAI + Google in one sitting), the user had to click N test buttons one at a time. The CLI's ‎`angi integrations test-all` already does this in one shot — UI didn't. **What** — header now has a `Test all configured (N)` button: - Counts configured providers via ‎`useMemo` over the `state` map. - Disabled while loading or when N = 0 (tooltip explains why). - Parallel-probes all configured providers using the existing per-row `testKey()` so the badges + summary stay in sync. - Loader spinner during in-flight; `testingAll` flag prevents double-fire. **No new endpoint** — fan-out is UI-only; the existing `/api/user/api-keys/test` is auth-checked + decryption-bounded per call. Matches the CLI contract precisely. **Net** — bulk key verification matches the CLI parity. After paste-saving N keys, one click validates all of them in parallel.

    • Counts configured providers via useMemo + disables when N = 0
    • ANGI_VERSION 1.7.304 → 1.7.305 (chat + web)
    • New bilingual keys: api_keys_page.test_all + test_all_empty
    • Fan-out via existing /api/user/api-keys/test — no new endpoint added
    • /settings/api-keys "Test all configured (N)" header button — parallel probe
  19. 1.7.304
    M9
    Onboarding key input — eye toggle + type=text for local hostsShipped May 23, 2026

    **Why** — onboarding step 4 used `type="password"` for every provider. Two problems: 1. Users pasting a long API key couldn't verify what they pasted — they had to commit blind and hope. 2. Ollama / LM Studio take URLs (`http://localhost:11434/api`), not secrets. Masking a hostname is theater + makes it harder for users to spot a typo in port or path. **What** — - **Eye toggle** on cloud-provider key inputs (anthropic, openai, google, qwen). Default masked; one click reveals. Aria-labeled (`onboarding.show_key` / `onboarding.hide_key`) for screen readers. - **type="text"** for ollama / lmstudio so the URL is visible by default. The host IS the trust boundary for these providers (no API key to leak); same rationale the LM Studio probe handler uses in v1.7.258. - Padding on the input bumped to `pe-10` so the toggle button doesn't overlap the placeholder. RTL-safe: `end-*` instead of `right-*`. **Net** — visual verification of pasted keys + correct affordance for URL-style "secrets". Standard UX that was missing from the wizard.

    • Eye toggle (show/hide) on cloud-provider key inputs in onboarding
    • RTL-safe toggle position (end-1 not right-1)
    • New bilingual keys: onboarding.show_key + onboarding.hide_key (aria-labels)
    • ANGI_VERSION 1.7.303 → 1.7.304 (chat + web)
    • type=text for ollama / lmstudio — URLs visible by default (host is trust boundary)
  20. 1.7.303
    M9
    Onboarding probes the just-saved LLM key — catches typos in contextShipped May 23, 2026

    **Why** — onboarding step 4 saved a provider key and immediately advanced to step 5 without validating it. A typo'd key only surfaced on the first chat send as a cryptic 401, far from the form that produced it. User context lost, doc link gone, fix path requires backtracking to /settings/api-keys. **What** — post-save probe calls the existing `/api/user/api-keys/test` endpoint right after the POST succeeds. On probe failure, step 5 renders an amber warning with the provider name + truncated error reason + a "Re-enter on /settings/api-keys" link. **Soft warning, not a block** — local Ollama / LM Studio hosts may legitimately fail the probe from a hosted Vercel deploy (worker can't reach localhost). Wording acknowledges this so users running local-only setups don't think the onboarding broke. Step 5 advances either way; the warning just gives them a heads-up + the right fix path. **Bilingual** — new `onboarding.key_probe_failed` + `onboarding.fix_key_link` keys in both en + he with proper interpolation for `{provider}` + `{detail}`. **Net** — bad keys get caught in the right context. First chat send no longer fails with a 401 the user can't trace back to their just-saved key.

    • Step 5 surfaces an amber warning + remediation link on probe failure
    • ANGI_VERSION 1.7.302 → 1.7.303 (chat + web)
    • New en+he keys: onboarding.key_probe_failed + onboarding.fix_key_link
    • Soft warning — local Ollama/LM Studio failures from hosted deploy do not block
    • Onboarding probes /api/user/api-keys/test after saving the key
  21. 1.7.302
    M9
    /admin/ops gains Sentry remediation hint + cross-deploy drift rowShipped May 23, 2026

    **Why** — `/admin/ops` Service Health card showed Sentry-not-configured as an amber dot with `"no DSN — error capture dark"` — accurate but no remediation. Admins had to know which env-vars to set. Cross-deploy drift was also invisible at the dashboard level (only visible via CLI `angi status --marketing` or the public `/status` page). **What** — two polish additions to the Service Health card: 1. **Sentry remediation hint** — when Sentry isn't configured, the card now shows: `Set SENTRY_DSN + NEXT_PUBLIC_SENTRY_DSN in Vercel to wire up error capture.` Direct path from "this is broken" to "this is how to fix it". 2. **Marketing deploy row** — server-side fetch of `${MARKETING_URL}/api/version` (default `https://angi.app`, override via `NEXT_PUBLIC_MARKETING_URL`). Renders: - Green dot + `code_version=X (aligned with chat)` when the two builds agree. - Amber dot + `code_version=X (chat=Y) — half-promoted` + remediation: `Re-promote the apps/web deploy on Vercel so both surfaces advertise the same build.` 3-second timeout — marketing-down does not block the admin dashboard. **Net** — admin can spot AND fix Sentry + cross-deploy drift without leaving `/admin/ops`. Closes the last gap in the operational visibility arc on the admin surface.

    • Cross-deploy drift row (chat vs marketing code_version + re-promote hint)
    • ANGI_VERSION 1.7.301 → 1.7.302 (chat + web)
    • 3s timeout on marketing fetch — outage does not block admin dashboard
    • Sentry remediation hint (SENTRY_DSN + NEXT_PUBLIC_SENTRY_DSN env vars)
  22. 1.7.301
    M9
    angi version + angi health gain --marketing — symmetric with angi statusShipped May 23, 2026

    **Why** — v1.7.300 added cross-deploy probe to `angi status`. The dedicated `angi version` + `angi health` commands still only hit the chat deploy. Operators who wanted a focused version-only or health-only readout across both deploys had to drop down to `angi status --marketing`, which prints the full status block. **What** — both commands now mirror the `angi status --marketing` interface: - `angi version --marketing <url>` — fetches `/api/version` from both deploys, prints side-by-side, warns on `code_version` drift. - `angi health --marketing <url>` — fetches `/api/health?deep=1` from both deploys, prints supabase + sentry dots per side. - Both honor `$ANGI_MARKETING_URL` as the env-var fallback (same as `angi status`). - Exit codes: `0` ok, `2` degraded (any side red, or drift detected), `1` request failed. **Net** — three CLI commands now share the same `--marketing` flag + env-var contract. Pick the one whose output shape you need; cross-deploy probing is uniform.

    • angi health --marketing <url> — cross-deploy health probe
    • ANGI_VERSION 1.7.300 → 1.7.301 (chat + web)
    • Exit code 2 on drift OR degraded state (matches angi status contract)
    • Both honor $ANGI_MARKETING_URL env-var fallback (same as angi status)
    • angi version --marketing <url> — cross-deploy version probe
  23. 1.7.300
    M9
    angi status --marketing — cross-deploy drift detection in one commandShipped May 23, 2026

    **Milestone v1.7.300** — closes the operational visibility arc with a single command that proves both deploys are running the same build. **Why** — `angi status` only probed the chat deploy. A half-promoted Vercel deploy (chat updated, marketing still on the previous build) showed green on `angi status` but broke `/status` page comparison. Operators had to run `curl <marketing>/api/version` separately + eyeball-diff. **What** — `angi status --marketing <url>` (or `$ANGI_MARKETING_URL`) now probes the marketing surface too: - Cross-origin hit on `/api/version` + `/api/health?deep=1` - Render `code_version` + dep dots (supabase + sentry) - **Drift warning** when chat `code_version` != marketing `code_version`: `⚠ drift: chat=X != marketing=Y. Half-promoted deploy.` **Net** — `angi status --marketing https://angi.app` is now the one-liner that proves both deploys are aligned. Closes the visibility arc started in v1.7.286 — 15 ships across endpoints, CLI, admin UI, status page, banner, e2e, audit gates, and now cross-deploy probe. **Arc summary** (v1.7.286 → v1.7.300): - v1.7.286 — MCP registry test floor 200 → 240 - v1.7.287 — `/api/version` `code_version` + `X-Angi-Version` - v1.7.288 — `/api/health` Sentry probe - v1.7.289–290 — release_items query bounded - v1.7.291 — `angi version` + `angi health` CLI - v1.7.292 — `/admin/ops` Service Health card - v1.7.293–294 — deploy-verify + smoke + health-check probes fixed (bare semver) - v1.7.295 — code_version contract reached 5 more consumers - v1.7.296 — brand-audit gates pill-drift - v1.7.297 — apps/web `/api/health` parity (Sentry probe + version header) - v1.7.298 — `/status` page exposes granular deps - v1.7.299 — `X-Angi-Version` on all 4 health endpoints - **v1.7.300** — `angi status --marketing` cross-deploy drift detection

    • $ANGI_MARKETING_URL env var as default for the --marketing flag
    • ANGI_VERSION 1.7.299 → 1.7.300 (chat + web)
    • Milestone v1.7.300 — closes the operational visibility arc started v1.7.286
    • Marketing health row shows supabase + sentry dot status
    • Drift warning when chat code_version != marketing code_version
    • angi status --marketing <url> probes both deploys in one command
  24. 1.7.299
    M9
    X-Angi-Version header on all 4 health endpoints (chat+web × /api/health+/healthz)Shipped May 23, 2026

    **Why** — v1.7.297 added `X-Angi-Version` to `apps/web/api/health`. Three sibling endpoints still missed it: 1. `apps/chat/app/api/health/route.ts` (GET + HEAD) 2. `apps/chat/app/healthz/route.ts` (HEAD; GET inherits via the underlying /api/health response) 3. `apps/web/app/healthz/route.ts` (GET + HEAD) Uptime monitors checking the unprefixed `/healthz` path (Kubernetes / Cloud Run / Render convention) couldn't read the running version without a separate `/api/version` request. **What** — all four health endpoints now emit `X-Angi-Version` from the local `ANGI_VERSION` constant on both GET and HEAD. The chat `/healthz` GET passes through `/api/health`'s response so it inherits the header automatically; HEAD sets it explicitly because it's a local short-circuit. **Net** — `curl -sI <base>/healthz | grep X-Angi-Version` works against either deploy on either path. Uptime monitor + status dashboards no longer need a second hit to learn the version.

    • apps/chat/api/health emits X-Angi-Version on GET + HEAD
    • ANGI_VERSION 1.7.298 → 1.7.299 (chat + web)
    • apps/web/healthz emits X-Angi-Version on GET + HEAD
    • apps/chat/healthz emits X-Angi-Version on HEAD (GET inherits)
  25. 1.7.298
    M9
    /status page exposes marketing deps.sentry + deps.supabase granularlyShipped May 23, 2026

    **Why** — `/status` rendered a single ok/down dot for the marketing surface. v1.7.297 added `deps.sentry` to `apps/web/api/health` (alongside the existing `deps.supabase`), but `/status` discarded the granular shape and reported only `res.ok`. Operators couldn't tell a "marketing serves but Sentry is dark" deploy from a "marketing serves cleanly" one without inspecting raw JSON. **What** — `probeMarketing()` now parses the response body and returns `deps: { supabase, sentry }` when the v1.7.297 contract is present. The marketing row renders two extra inline indicators below the description: green/red dot for Supabase, green/amber dot for Sentry (amber matches the chat-side admin convention: Sentry-down is degraded, not broken). **Fallback** — legacy deploys without `deps.sentry` keep the old single-dot rendering. No false signals. **Net** — `/status` is now a one-glance view of every dep across both deploys. The public uptime story now matches the `angi health` + `/admin/ops` Service Health card detail level.

    • Sentry-down rendered amber (matches admin convention: degraded, not broken)
    • ANGI_VERSION 1.7.297 → 1.7.298 (chat + web)
    • Legacy deploys without deps.sentry fall back to single-dot rendering
    • Marketing row on /status now shows deps.supabase + deps.sentry dots
  26. 1.7.297
    M9
    apps/web /api/health gains Sentry probe + X-Angi-Version header (parity with chat)Shipped May 23, 2026

    **Why** — v1.7.288 added Sentry config-presence probe + v1.7.287 added X-Angi-Version header to `apps/chat/api/health`. The marketing surface `apps/web/api/health` was left on the old shape, so uptime monitors hitting the marketing endpoint couldn't tell whether error capture was wired up there + couldn't read the running version without a separate `/api/version` hit. **What** — brought `apps/web/api/health` to full parity with chat: - `deps.sentry: boolean` alongside `deps.supabase` (config-presence check, no real event sent — same as chat). - `X-Angi-Version` response header on both GET + HEAD replies (pulls from the v1.7.295 `apps/web/lib/version.ts` constant). **Decision** — Sentry-down does NOT flip `ok=false` (matches chat contract; Sentry going dark doesn't break the marketing surface). Supabase failure still trips 503 because `/api/version` + the CTAs need it. **Net** — both `/api/health` endpoints now report the same dep + version shape. Uptime monitors can swap probe URLs without changing assertions.

    • X-Angi-Version response header on GET + HEAD replies
    • ANGI_VERSION 1.7.296 → 1.7.297 (chat + web)
    • Full parity with apps/chat/api/health (v1.7.287 + v1.7.288 contract)
    • apps/web/api/health now probes Sentry (deps.sentry alongside deps.supabase)
  27. 1.7.296
    M9
    brand-audit gates pill-drift — three-way version consistency now enforcedShipped May 23, 2026

    **Why** — CLAUDE.md says the pill calls keep literal version strings inline "so brand-audit CI catches forgotten bumps". But brand-audit only checked forbidden vendor names + secret prefixes — **the version literal was never actually verified**. A half-bumped release (one pill on the new version, the other on the old; or the `ANGI_VERSION` constant out of sync with the pill) would ship green. **What** — added a third rule set: PILL_DRIFT. The audit now parses three sources of truth + asserts they agree: 1. `apps/chat/lib/version.ts` → `export const ANGI_VERSION = '...'` 2. `apps/web/lib/version.ts` → same 3. Every `release_pill', { version: '...' }` literal in `apps/{chat,web}/app/page.tsx` Any disagreement fails the audit with a clear diff (`pill literal 'X' != ANGI_VERSION 'Y'`). Clean output now prints the count: "pill v1.7.296 consistent across 2 literals + 2 constants". **Net** — `release-tag.yml` already gated `chat == web` at pill level, but the constants + the audit didn't. Closing the loop locally means `pnpm lint && pnpm typecheck && pnpm brand-audit` now catches every variant of "forgot to bump" without needing to push.

    • Clean output now lists literal + constant count for visibility
    • Three-way check: chat ANGI_VERSION = web ANGI_VERSION = every pill literal
    • brand-audit now enforces pill-drift (PILL_DRIFT rule set)
    • ANGI_VERSION 1.7.295 → 1.7.296 (chat + web)
  28. 1.7.295
    M9
    code_version contract reaches the remaining 5 consumers — full /api/version parityShipped May 23, 2026

    **Why** — v1.7.287 introduced `code_version` on `apps/chat/api/version`. Three probe scripts (deploy-verify, health-check, smoke) got fixed in v1.7.293–294. But five more consumers were still reading the old `body.version`-only shape and could silently report stale or empty data: 1. `apps/chat/components/observability/stale-deploy-banner.tsx` — silent miss when release row lags 2. `apps/chat/e2e/version.spec.ts` — old `/^v\d+\.\d+\.\d+$/` regex never matched bare semver 3. `apps/cli/src/index.ts` (root `angi status`) — showed `—` when release row missing 4. `apps/chat/app/status/page.tsx` — false "deploys drifted" when one side lagged 5. `apps/web/app/api/version` — DIDN'T expose code_version at all (separate route, no shared constant) **What** — all five now consume / expose `code_version`: - Banner + CLI + status page prefer `code_version`, fall back to `version` - E2E test asserts bare semver + the new `X-Angi-Version` header - `apps/web/api/version` brought to full parity: new `apps/web/lib/version.ts` constant + 200-always contract + `X-Angi-Version` header **Net** — the operational visibility surface (endpoints + CLI + admin UI + probes + banner + status + cross-origin marketing probe) now uniformly trusts the compile-time constant. Release-row lag stops creating false signals across the board.

    • apps/web/lib/version.ts constant (mirror of chat lib/version.ts)
    • apps/web/api/version full parity: code_version + X-Angi-Version + 200-always
    • apps/chat/app/status/page.tsx cross-origin compare uses code_version
    • apps/cli/src/index.ts angi status shows code_version + drift indicator
    • e2e/version.spec.ts asserts bare semver + X-Angi-Version header
    • stale-deploy-banner.tsx prefers code_version (release-row lag no longer silences updates)
    • ANGI_VERSION 1.7.294 → 1.7.295 (chat + web)
  29. 1.7.294
    M9
    health-check + smoke probes fixed — same code_version bug as deploy-verifyShipped May 23, 2026

    **Why** — `pnpm health` (`tooling/scripts/health-check.ts`) and `pnpm smoke` (`tooling/scripts/smoke.ts`) had the same dormant `/api/version` bug v1.7.293 fixed in `deploy-verify.ts`: both regex-tested `body.version` against `/^v\d+\.\d+\.\d+$/`, but the API returns bare semver (`1.7.X`, no `v` prefix). The probe never matched the well-formed response — `unexpected payload` would fire on every healthy deploy. **What** — switched both probes to: 1. Prefer `body.code_version` (compile-time constant, always populated since v1.7.287) with fallback to `body.version` for legacy deploys. 2. Test against bare `/^\d+\.\d+\.\d+$/` so the well-formed shape passes. **Net** — `pnpm health <url>` and `pnpm smoke <url>` now actually validate `/api/version`. Both feed the release-tag workflow's `verify` job + the nightly smoke run, so this was a silent CI false-fail across three independent probes. The bug was the same in three places because all three were written from the same template before v1.7.287 reshaped the response.

    • Doc comment header updated to cite code_version explicitly
    • ANGI_VERSION 1.7.293 → 1.7.294
    • smoke.ts /api/version probe — same fix (was false-failing nightly smoke)
    • health-check.ts /api/version probe — bare semver + code_version preferred
  30. 1.7.293
    M9
    deploy-verify now compares code_version (was silently broken since v1.7.287)Shipped May 23, 2026

    **Why** — `tooling/scripts/deploy-verify.ts` was comparing `live.version` (the DB releases row) against the pill version `v1.7.X`. Two issues: 1. The pill regex stored `1.7.X` (bare); deploy-verify prefixed it to `v1.7.X`. Bare-vs-`v` mismatch meant the comparison **never matched** since the values can't be equal byte-for-byte. 2. Even if it had matched, comparing against `releases.version` is the wrong signal — that's the DB-backed value that lags `code_version` (the actual deployed build) whenever the release row isn't seeded yet. v1.7.287 introduced `code_version` precisely so deploy-verification could check the *build* and not the *changelog row*. **What** — switched the comparison to `live.code_version`, with `live.version` kept as a secondary signal that triggers a "release row may not be seeded yet" note when it lags. Defensive byte-equality against both `v1.7.X` and `1.7.X` so an older deploy without `code_version` still works (falls back to `live.version`). **Net** — `pnpm deploy-verify` actually verifies deploys now. The script was returning timeout-fail on every run since v1.7.21 introduced the pill regex; nobody noticed because we don't run it on every batch.

    • Bare vs v-prefixed mismatch resolved (compares both forms defensively)
    • deploy-verify now compares code_version (was checking the wrong field)
    • Secondary note when releases.version lags code_version (row not seeded yet)
    • ANGI_VERSION 1.7.292 → 1.7.293
  31. 1.7.292
    M9
    /admin/ops gains a Service Health card — version drift + Supabase + Sentry at a glanceShipped May 23, 2026

    **Why** — `/api/health?deep=1` (v1.7.288) and `/api/version` (v1.7.287) returned operationally-critical signals (code_version vs release_version drift, Sentry DSN presence, Supabase reachability) but only via curl or the new `angi health` / `angi version` CLI commands. Admins on `/admin/ops` had to leave the dashboard to see them. **What** — added a "Service health" card at the top of `/admin/ops` that surfaces the same signals inline (no HTTP self-hop to `/api/health`): - `code_version` (compile-time `ANGI_VERSION` constant) - `release_version` + `milestone` + `deployed_at` (newest `public.releases` row) - Amber warning when `code_version != release_version` (release row not seeded for this commit) - Green/amber dots for Supabase + Sentry **Decision** — Sentry-down is amber, not red; matches `/api/health` contract where Sentry going dark does not flip `ok=false`. Supabase failure would prevent `ensureAdmin()` from rendering the page at all, so the dot is hard-coded `true` once we got past the auth guard.

    • No HTTP self-hop — probes pulled inline so render is single-pass
    • Amber warning when ANGI_VERSION != latest releases.version (missing release row)
    • Service health card on /admin/ops — version drift + Supabase + Sentry dots
    • ANGI_VERSION 1.7.291 → 1.7.292
  32. 1.7.291
    M9
    angi version + angi health — CLI wrappers for /api/version + /api/healthShipped May 23, 2026

    **Why** — `/api/version` (v1.7.287) + `/api/health?deep=1` (v1.7.288) are useful but require `curl` + `jq` to consume. Add CLI commands so an operator can paste a one-liner into a runbook or shell. **What** — - `angi version` — pretty-prints `code_version` vs `release_version` and warns when they drift. Exit code 2 on mismatch so a CI guard can flag it. - `angi health` — pretty-prints `/api/health?deep=1` with green/red dots for supabase + sentry. Exit code 0 healthy / 2 degraded / 1 request-failed. - `--json` flag on both — pipe straight to `jq` when you need raw data. - `--fast` flag on `angi health` — liveness only (skips dep probes). **Bonus** — neither command requires auth (both endpoints are public). Useful on a fresh shell before `angi login`.

    • --json + --fast flags for scripting + liveness-only probes
    • angi health — wrap /api/health?deep=1 with colored dots + exit codes
    • angi version — print code_version + release_version + drift warning
    • ANGI_VERSION 1.7.290 → 1.7.291
  33. 1.7.290
    M9
    loadUpdatesData() bound — /updates page no longer scans full history every renderShipped May 23, 2026

    **Why** — same unbounded pattern v1.7.289 fixed in `/api/releases`, but in the server-component loader `loadUpdatesData()` that powers `/updates` AND the RSS feed at `/updates/feed.xml`. Every page render fetched all releases AND all release_items. Today: 305 rows + 1258 rows. **What** — `loadUpdatesData({ limit })` now caps the releases page (default 200, clamped 1–500). Same two-step pattern as the API route: fetch limited releases, then fetch only items where `release_id IN (...page)`. **RSS feed** passes `limit: MAX_ITEMS * 2 = 100` — wider than the .slice() cap because non-shipped rows may sit ahead of the shipped subset that actually serializes. **Net** — wire bytes + Postgres scan now bounded by page size, not by total history. `revalidate = 300` already softens the hit, but the bound matters for cache-miss latency + concurrent load.

    • RSS feed passes MAX_ITEMS * 2 = 100 to tighten the DB-side bound
    • release_items scoped by release_id IN page (matches v1.7.289 /api/releases)
    • loadUpdatesData() now caps releases page (default 200, clamped 1–500)
    • ANGI_VERSION 1.7.289 → 1.7.290
  34. 1.7.289
    M9
    /api/releases bounds release_items query — was scanning all 1258 rows per page-loadShipped May 23, 2026

    **Why** — the `/api/releases` handler ran two parallel queries: `releases` with a limit, and `release_items` with **no limit and no release_id filter**. At 305 releases / 1258 items today, every `/updates` page-load pulled all 1258 items even when only ~50 releases were rendered. **What** — switched to a sequential two-step: 1. Fetch the limited `releases` page. 2. Fetch only `release_items` where `release_id IN (...the page IDs)`. **Tradeoff** — lose Promise.all parallelism but gain bounded wire bytes + bounded Postgres scan. Net latency wins above ~100 items and the cliff only gets steeper. **Bonus** — `release_items.created_at` now in the select so ordering is explicit (was implicit per `.order()` against an unselected column).

    • release_items.created_at added to select so order is explicit
    • Sequential two-step replaces Promise.all (intentional — bounds Postgres scan)
    • release_items query now scoped by release_id IN page — bytes-on-wire bounded
    • ANGI_VERSION 1.7.288 → 1.7.289
  35. 1.7.288
    M9
    /api/health?deep=1 now actually probes SentryShipped May 23, 2026

    **Why** — the route comment has promised "Supabase + Sentry reachability" since v1.7.21, but the handler only probed Supabase. Uptime monitors couldn't tell whether error capture was wired up after a deploy. Silent drift. **What** — added `probeSentry()` that reads `Sentry.getClient().getOptions().dsn` and reports `deps.sentry: boolean` alongside `deps.supabase`. Config-presence check rather than a real Sentry event — sending an event per probe would pollute the issue stream + burn quota. **Decision** — Sentry going dark does NOT flip `ok=false` (just reports `sentry: false`). The app still serves traffic without error capture; only Supabase failure trips a 503 because chat / settings / billing all need it.

    • Route comment vs impl drift closed (comment promised Sentry probe since v1.7.21)
    • ANGI_VERSION 1.7.287 → 1.7.288
    • Sentry-down only reports — does not flip ok=false (app still serves traffic)
    • /api/health?deep=1 now reports deps.sentry alongside deps.supabase
  36. 1.7.287
    M9
    /api/version survives Supabase outage + emits X-Angi-Version headerShipped May 23, 2026

    **Why** — `/api/version` previously returned 503 on any Supabase blip even though the compile-time `ANGI_VERSION` constant was always available. Deploy-verification scripts couldn't tell a half-promoted build from a degraded DB. **What** — two-source response: - `code_version` — compile-time constant. Always populated. - `version` / `milestone` / `deployed_at` — newest `public.releases` row. Null on DB failure. A mismatch between `code_version` and `version` flags a missing release row for the running commit — on-call catches that with one curl. **Bonus** — `X-Angi-Version` response header lands on every response so a CDN-level health probe can read the version without parsing JSON.

    • ANGI_VERSION 1.7.286 → 1.7.287
    • Supabase failures now return HTTP 200 with version:null (was 503)
    • X-Angi-Version response header on every /api/version reply
    • /api/version returns code_version constant alongside DB version row
  37. 1.7.286
    M9
    CI hygiene — MCP registry floor lifts to 240 toolsShipped May 23, 2026

    **Why** — registry test floor was stuck at `>= 200` from Wave 7. After waves 5a–12 + media + federation, the actual count is **241**. A regression dropping 50 tools would still pass. **What** — bumped `packages/mcp-server/test/registry.test.ts` floor to `>= 240`. Sanity ceiling stays at 1000. Floor guard now catches accidental mass removals while letting routine additions land without a test rev. **Net** — CI gate matches reality. Wave 5a–12 + media + federation surface is now protected by a real floor, not a stale one.

    • Sanity ceiling preserved at < 1000 — runaway-loop guard intact
    • Comment refreshed to cite waves 5a–12 + media + federation
    • Registry test floor bumped 200 → 240 (matches 241 actual tool count)
  38. 1.7.285
    M5
    `federation/share_chat` + `federation/share_namespace` MCP tools — full kind parity (241 tools)Shipped May 23, 2026

    - UI (v1.7.282 + v1.7.283) and CLI (v1.7.284) already covered all three resource kinds. The MCP tool surface only had `federation/share_swarm`. Closed the last parity gap so the LLM can publish chats + memory namespaces too. - `federation/share_chat` — peer + chat preflight, upsert against `peer_shares` with `resource_kind=chat`, idempotent on `(peer_id, chat_id, direction)`. - `federation/share_namespace` — peer preflight + memories-row-count precondition (≥1 row under that namespace in the workspace), upsert with `resource_kind=memory_namespace`. resource_id is the namespace label (text after migration 0037). - Both throw clear preconditions: `peer_not_found`, `peer_blocked`, `chat_not_found`, `namespace_empty`. Direction defaults to `out`; `bidirectional` opt-in. - Tool count: 239 → 241. - i18n: 2 new `mcp_tool_descriptions.*` keys in en + he.

    • federation/share_namespace MCP tool — memories-row precondition + upsert with resource_kind=memory_namespace
    • federation/share_chat MCP tool — peer + chat preflight + upsert with resource_kind=chat
  39. 1.7.284
    M7
    `angi federation share-chat` + `share-namespace` CLI parity with UIShipped May 23, 2026

    - The UI's Share Resource form now covers all three kinds (swarm / chat / memory_namespace) after v1.7.282 + v1.7.283. CLI was lagging — only `share-swarm` existed. Closed the parity gap. - `angi federation share-chat --peer <uuid> --chat <uuid> [--bidirectional]` — POSTs `/api/federation/shares` with `resource_kind=chat`. - `angi federation share-namespace --peer <uuid> --namespace <name> [--bidirectional]` — POSTs with `resource_kind=memory_namespace`; the server's v1.7.283 "must have ≥1 memory row" precondition still applies so a typo gets a clean error. - Both sub-commands echo the share id on success + accept `--json` for cron / monitor consumers. Existing `share-swarm` unchanged.

    • `angi federation share-namespace` POSTs with resource_kind=memory_namespace; honors v1.7.283 precondition
    • `angi federation share-chat` POSTs /api/federation/shares with resource_kind=chat
  40. 1.7.283
    M5
    Federation Share Resource — third kind (memory_namespace) landsShipped May 23, 2026

    - Closed the last open share-kind from migration 0029. `peer_shares.resource_kind` accepts `swarm | memory_namespace | chat`; the UI now exposes all three. - Migration 0037 widens `peer_shares.resource_id` from `uuid` to `text`. Swarm + chat ids stay uuids in practice; memory namespaces are arbitrary labels ("default", "research-2026", …) and need text storage. The unique tuple `(peer_id, kind, resource_id, direction)` + per-resource index ride the new type automatically. - New `/api/memory/namespaces` GET enumerates distinct namespaces in the active workspace (RLS-scoped, alphabetized, capped 200). - FederationPanel's Share Resource form picks up: - "Memory namespace" kind in the dropdown (only renders when at least one namespace exists in workspace). - Namespace dropdown populated from the new endpoint. - `/api/federation/shares` POST validates uuid only when kind is swarm / chat; memory_namespace gets a "must have ≥1 memory row in the workspace" precondition so a typo doesn't publish a phantom namespace. - 2 new i18n keys (en + he): `kind_memory_namespace`, `share_namespace_label`. - MCP tool `federation/share_swarm` stays swarm-specific by design — the LLM uses the higher-level `share-swarm` shape; UI + REST handle the other kinds.

    • Share Resource form covers the third kind (memory_namespace) with its own picker + workspace-existence precondition
    • GET /api/memory/namespaces enumerates distinct namespaces for the active workspace
    • Migration 0037 widens peer_shares.resource_id to text so memory_namespace (string id) can ship
  41. 1.7.282
    M5
    FederationPanel — Share Resource form now supports chat kind alongside swarmShipped May 23, 2026

    - v1.7.279 added the Share Swarm form. Schema's `peer_shares.resource_kind` already supports `swarm | memory_namespace | chat`; the form only exposed `swarm`. Closed half the gap by adding `chat`: - New **Resource kind** dropdown switches between Swarm and Chat (only renders the option when the workspace has at least one of that kind, so empty pickers don't appear). - When `chat` is selected, the resource picker pulls from `/api/chats?limit=200` and shows each chat by title (falling back to the id prefix for untitled). - POST shape stays `{ peer_id, resource_kind, resource_id, direction }` so the server route + RLS contract are unchanged. - Renamed UI button `Share swarm` → `Share resource` so the same button covers both kinds. CLI command `angi federation share-swarm` stays swarm-specific for now (kind=chat CLI sub-command queued). - 5 new i18n keys (en + he): `share_resource`, `share_kind`, `kind_swarm`, `kind_chat`, `share_chat_label`. - Memory-namespace kind still queued — needs a dedicated `/api/memory/namespaces` endpoint to enumerate distinct namespaces; will land in a follow-up.

    • 5 new i18n keys (share_resource / share_kind / kind_swarm / kind_chat / share_chat_label) in en + he
    • Share Resource form covers kind=chat in addition to kind=swarm; chat picker reads /api/chats?limit=200
  42. 1.7.281
    M7
    `angi tools call <name>` — invoke any MCP tool from the shellShipped May 23, 2026

    - `angi tools` was inspect-only (`list` + `describe`). Added a third sub-command that closes the loop: - `angi tools call <name> --args '<json-object>'` POSTs `tools/call` JSON-RPC against `/api/mcp/v1` (same path the chat surface uses, now plumbing ApiKeys + active workspace per v1.7.267). - Examples baked into `--help`: `memory/search` with a query, `search/web` with a topic, `swarm/list` with `{}`. - Pretty-prints the unwrapped `result.content[0].value` from the JSON-RPC response; `--raw` strips the indent for `jq` pipelines. - Validation: `--args` must be a JSON object (not an array or scalar); malformed JSON exits non-zero with a clear hint before touching the network. - Closes the "every tool that runs in chat can also run from cron + scripts + smoke tests" promise the v1.7.267 plumbing made possible.

    • `--raw` flag strips pretty-printing for jq pipelines; malformed JSON in --args exits non-zero before network call
    • `angi tools call <name> --args <json>` invokes any MCP tool via /api/mcp/v1 with active-workspace + ApiKeys context
  43. 1.7.280
    M7
    `angi swarm tick` — manual autonomous-worker trigger from CLIShipped May 23, 2026

    - Autonomous swarm worker (`/api/swarms/tick`) used to fire only on `/api/chat onFinish` or scheduled cron. No way to manually kick it when a swarm got stuck after a tick crashed mid-batch — ops users had to send a dummy chat message to wake it up. - New `angi swarm tick` sub-command POSTs the existing `/api/swarms/tick` endpoint for the active workspace and prints a summary line: `processed: N done: M failed: K`, plus a token-usage footer when applicable. - `--json` emits the raw response for cron / monitor consumers. - No new HTTP route, no new background workers — purely a CLI wrapper around shipped infrastructure.

    • `angi swarm tick` POSTs /api/swarms/tick + summarises processed / done / failed counts
  44. 1.7.279
    M5
    FederationPanel — Share Swarm inline form (peer + swarm dropdowns)Shipped May 23, 2026

    - FederationPanel could list + revoke shares + add peers but had no way to CREATE a share from the UI. Closed the last write gap. - New **Share swarm** button next to the shares heading, only rendered when the workspace has at least one peer AND one swarm (no point clicking through empty dropdowns). - Inline form with three dropdowns: - **Peer** — populated from `/api/federation/peers`, filtered to non-blocked rows. - **Swarm** — populated from `/api/swarms`. Shows swarm name + status so the user can avoid sharing a stopped one. - **Direction** — "out (publish only)" or "bidirectional (publish + accept read-back)". - POSTs `/api/federation/shares`; new row optimistically prepended to the list. Errors render inline in a rose-bordered banner. - 7 new i18n keys (en + he): `share_swarm`, `share_peer`, `share_swarm_label`, `share_direction`, `direction_out`, `direction_bidi`, `pick_one`.

    • Share button only renders when at least one peer + one swarm exist
    • Inline Share Swarm form in FederationPanel with peer + swarm + direction dropdowns; POSTs /api/federation/shares
  45. 1.7.278
    M7
    `angi integrations test-all` — parallel probe of every configured providerShipped May 23, 2026

    - One-command health check for the whole integration surface. `angi integrations test-all`: 1. Pulls `/api/user/me` to discover which providers + third-party integrations are configured. 2. Maps each entry back to the test-endpoint slug (`qwen` → `dashscope`, `ollama` → `ollama_host`, etc.). 3. Fires every probe in parallel via `Promise.all`. 4. Prints a summary table with green OK / red FAIL plus HTTP status, ending in `N of M probes failed` and a non-zero exit code if any failed. - Ops use case: nightly cron, single command. Pairs with `angi integrations ls` (current state) + `angi integrations test <provider>` (single probe). - `--json` emits `[{ provider, ok, status, error, model_count }]` for chaining.

    • `angi integrations test-all` probes every configured provider in parallel and exits non-zero on any failure
    • Refactored probe + print helpers (`probeOne`, `printOne`) so test + test-all share rendering
  46. 1.7.277
    M7
    `angi integrations test <provider>` — run probe from CLIShipped May 23, 2026

    - `angi integrations` was list-only. Added a `test <provider>` sub-command that mirrors the v1.7.276 PlugZap button — calls `/api/user/api-keys/test` and prints a colored OK / FAIL line with the upstream HTTP status when applicable. - Accepts user-friendly aliases (`qwen` → `dashscope`, `ollama` → `ollama_host`, `lmstudio` → `lmstudio_host`, `shopify_shop` → `shopify`, `quickbooks_realm` → `quickbooks`, etc.) so the CLI matches the `angi integrations` ls column names instead of the raw key-slot names. - `--json` emits the probe response verbatim and propagates a non-zero exit on failure so CI runners can chain it. - Default sub-command is now `ls` so bare `angi integrations` keeps working unchanged.

    • CLI accepts friendly aliases (qwen, ollama, lmstudio, shopify_shop, quickbooks_realm, …) that fold to canonical provider ids
    • `angi integrations test <provider>` calls /api/user/api-keys/test and colors OK / FAIL
  47. 1.7.276
    M9
    Test-connection probes for Tavily, Brave, HubSpot, Shopify, Intercom, QuickBooksShipped May 23, 2026

    - The PlugZap test button on `/settings/api-keys` only knew how to ping LLM-model providers; pressing it on a third-party integration row 400'd because the `/api/user/api-keys/test` enum didn't include them. - Added six probes, each calling the cheapest authenticated GET the provider offers so a wrong key surfaces fast without burning quota: - **Tavily** — `POST /search { query: 'ping', max_results: 1 }`. - **Brave** — `GET /res/v1/web/search?q=ping&count=1` with `X-Subscription-Token`. - **HubSpot** — `GET /account-info/v3/details`. - **Shopify** — `GET /admin/api/2024-10/shop.json` against the stored `<shop>.myshopify.com`. - **Intercom** — `GET /me` with the v2.11 version header. - **QuickBooks** — `GET /companyinfo/{realm}` against the env-selected production or sandbox host. - Shopify needs BOTH token + shop subdomain stored; QuickBooks needs token + realmId. Probes return a clear precondition error when half is missing instead of a confusing 401. - The form's PlugZap on sub-fields (`shopify_shop`, `quickbooks_realm`, `quickbooks_env`, `lmstudio_model`) now maps to the parent provider via a small `TEST_PROVIDER_ALIAS` table so pressing the button anywhere in the row hits the right probe.

    • Form sub-fields (shopify_shop, quickbooks_realm, quickbooks_env, lmstudio_model) alias to parent provider for the test button
    • Six new probes in /api/user/api-keys/test (tavily / brave / hubspot / shopify / intercom / quickbooks)
  48. 1.7.275
    M5
    FederationPanel — this-instance public key + Add Peer inline formShipped May 23, 2026

    - v1.7.269 shipped the federation tab as read-only. Closed the write half: - **This instance** card surfaces the local Ed25519 public key fetched from `/api/federation/identity`. Copy button drops it on the clipboard so the user can paste into the OTHER peer's "add peer" form. 503 from the route renders the muted "federation is opt-in" hint. - **Add peer** button next to the peers heading toggles an inline form with `label` + `peer_url` + `peer_public_key` inputs. POSTs `/api/federation/peers`; the new row is optimistically prepended to the list in pending status. - Full federation loop now reachable from the SettingsDialog without touching the chat agent or the CLI: copy local key → hand to peer → wait for peer to add us → add peer ourselves → wait for handshake → share swarm. - New `federation.cancel` i18n key (en + he).

    • This-instance card: copyable Ed25519 public key from /api/federation/identity, with disabled hint when federation is off
    • Inline Add Peer form (label / peer_url / peer_public_key) POSTs /api/federation/peers; new row optimistically prepended
  49. 1.7.274
    M7
    `angi federation peer-add` + `peer-remove` CLIShipped May 23, 2026

    - Federation CLI was list-only on the peers side. Added two write commands: - `angi federation peer-add --label <name> --url <https> --key <base64>` — POSTs `/api/federation/peers`; row lands in status='pending' until the first signed request from the peer. - `angi federation peer-remove <peer-id>` — DELETE `/api/federation/peers` for a single row. - Both routes already RLS-gate writes to workspace owner/admin per migration 0029, so the CLI inherits the right precondition errors when a non-owner runs them. - Pairs with the v1.7.268 shares sub-commands so a full federation flow — add peer → wait for handshake → share swarm → revoke share → remove peer — is now reachable from the shell.

    • `angi federation peer-add` POSTs /api/federation/peers with label + url + Ed25519 public key
    • `angi federation peer-remove <peer-id>` DELETEs /api/federation/peers row
  50. 1.7.273
    M7
    `angi integrations` CLI — list configured providers + third-party slotsShipped May 23, 2026

    - New CLI command `angi integrations` reads `/api/user/me` and renders a two-column status grid: - **Model providers**: anthropic / openai / google / qwen / ollama / lmstudio. - **Third-party integrations**: tavily / brave / hubspot / shopify / intercom / quickbooks. - Each row shows `configured` (green) or `unset` (dim), plus a footer link to `/settings/api-keys` for rotation. - `--json` emits the same lists as `{ providers: [...], integrations: [...] }` for scripting. - Pure read against the v1.7.272 payload addition; no new HTTP routes, no new env vars.

    • `angi integrations` CLI surfaces configured_providers + configured_integrations from /api/user/me
  51. 1.7.272
    M9
    /api/user/me — expose third-party integrations statusShipped May 23, 2026

    - `/api/user/me` GET already returned `configured_providers: ModelProvider[]` for the LLM side. Status surfaces + onboarding had no parallel data on the third-party providers shipped in Wave 4b / Wave 12 (Tavily, Brave, HubSpot, Shopify, Intercom, QuickBooks). - New `configured_integrations: string[]` field lists every third-party slot the caller has fully configured. Pair-keyed providers (Shopify token + shop, QuickBooks token + realmId) only count when BOTH halves are present so the UI can trust the flag as "ready to call". - No new HTTP route — extends the existing `/api/user/me` payload. CLI / SDK consumers can predict the keys (`tavily`, `brave`, `hubspot`, `shopify`, `intercom`, `quickbooks`) from the slot names in `/settings/api-keys`.

    • Pair-keyed providers (Shopify + shop, QuickBooks + realmId) only count when both halves are stored
    • /api/user/me adds configured_integrations: tavily | brave | hubspot | shopify | intercom | quickbooks
  52. 1.7.271
    M9
    Sentry release tagging — in-repo version constant feeds Sentry initShipped May 23, 2026

    - All three Sentry init files (`sentry.server.config.ts`, `sentry.client.config.ts`, `sentry.edge.config.ts`) used to set `release: process.env.NEXT_PUBLIC_SENTRY_RELEASE`. When that env was unset (local dev, self-hosted setups, any deploy where the build pipeline forgets to bake it in), errors landed in Sentry without a release tag — filters by version came up empty. - New `apps/chat/lib/version.ts` exports `ANGI_VERSION = '1.7.271'` + `SENTRY_RELEASE_FALLBACK = '@angi/chat@1.7.271'`. All three Sentry configs now use the env var when present and fall back to the constant. Source-map upload still ships under whatever release name `withSentryConfig` is given; the fallback only affects runtime-tagged events. - Pill calls in `apps/chat/app/page.tsx` + `apps/web/app/page.tsx` keep literal versions inline so the brand-audit CI gate keeps catching forgotten bumps — importing the constant would defeat the audit's purpose.

    • Sentry server / client / edge inits fall back to the in-repo release tag when NEXT_PUBLIC_SENTRY_RELEASE is unset
    • apps/chat/lib/version.ts exports ANGI_VERSION + SENTRY_RELEASE_FALLBACK for cross-config import
  53. 1.7.270
    M7
    `angi flow` CLI + /api/flows REST surfaceShipped May 23, 2026

    - Long-running flows had MCP tools (`flow/start` / `flow/step` / `flow/branch` / `flow/merge` / `flow/cancel`, wave 10) but no CLI / REST surface for ops users to inspect what's running. Closed that gap. - New HTTP routes: - `GET /api/flows` — list flows in the active workspace newest-first, optional `?status` + `?limit` filters. - `GET /api/flows/:id` — return the flow row + its ordered `flow_steps`. - `DELETE /api/flows/:id` — soft-cancel a running or paused flow (status='cancelled', completed_at=now). 404 when already done. - New CLI command `angi flow`: - `angi flow ls` (with `--status`, `--limit`, `--json`). - `angi flow get <id>` — colored status + indented step tree. - `angi flow cancel <id>`. - All routes RLS-scoped to the caller's active workspace through the same supabase client the rest of the chat surface uses. No service-role.

    • `angi flow ls / get / cancel` CLI sub-commands
    • GET /api/flows + GET/DELETE /api/flows/[id] RLS-scoped REST surface for the long-running flows table
  54. 1.7.269
    M5
    Federation dashboard tab in SettingsDialogShipped May 23, 2026

    - New `federation` tab in the chat settings dialog. Read-only view of the trust ring + active shares: - **Trusted peers** section: status pill (pending / active / blocked), label, peer_url, last-seen timestamp. - **Active shares** section: resource kind + id, direction arrow (→ out, ← in, ⇄ bidirectional), peer label, revoke button. - Hits the existing `/api/federation/peers` GET + `/api/federation/shares` GET (v1.7.268). Revoke calls `/api/federation/shares` DELETE with optimistic UI removal on success. - Sidebar gear menu + chat-shell `SettingsKey` union both extended with the new entry. Same Sparkles icon as `/updates` since the dialog already pulls from that set. - Add-peer / add-share flows stay on the chat agent (`federation/share_swarm` MCP tool) + CLI (`angi federation share-swarm`) for now — the dashboard covers the read-and-revoke case, which is what users hit most. - 7 new i18n keys (en + he): `settings.federation`, `federation.peers_title`, `federation.shares_title`, `federation.no_shares`, `federation.last_seen`, `federation.never_seen`, `federation.revoke_share`.

    • Sidebar gear menu + SettingsKey union extended with `federation` entry
    • FederationPanel — peers + shares lists in SettingsDialog with inline revoke button
  55. 1.7.268
    M7
    `angi federation` CLI + /api/federation/shares HTTP routeShipped May 23, 2026

    - Federation management lands on the CLI surface. Four new sub-commands under `angi federation`: - `angi federation peers` — list trusted peers in the active workspace (status pill + last-seen). - `angi federation shares` — list active resource shares, optional `--peer-id` / `--kind` / `--direction` filters. - `angi federation share-swarm --peer <uuid> --swarm <uuid>` — publish a swarm to a peer (out-only by default; `--bidirectional` to accept read-back). - `angi federation revoke <share-id>` — delete a share. - New HTTP route `/api/federation/shares` mirrors the existing `/api/federation/peers` pattern (GET / POST / DELETE). RLS on `peer_shares` cascades through the peers FK + workspace membership, and the POST adds pre-flight checks that the peer + swarm + chat referenced belong to the caller's workspace so a 403 from RLS never reaches the CLI. - The MCP tools shipped in v1.7.261 stay available for in-chat use; the CLI just consumes the same shapes via REST instead of JSON-RPC.

    • /api/federation/shares HTTP route (GET / POST / DELETE) with pre-flight peer + swarm + chat workspace-membership checks
    • `angi federation peers` / `shares` / `share-swarm` / `revoke` CLI sub-commands
  56. 1.7.267
    M2
    MCP v1 HTTP transport — load ApiKeys + active workspaceShipped May 23, 2026

    - The `/api/mcp/v1` JSON-RPC endpoint that CLI / SDK consumers (and the v1.7.x `angi tools list`-style commands) hit through was missing two things the chat surface had been doing right since v1.7.254: - **Active-workspace resolution** — used to grab the first membership row, ignoring whatever workspace the user had explicitly switched to. Now calls `resolveActiveWorkspaceId()` first, with a membership-fallback for never-switched accounts. - **ApiKeys decryption** — every third-party-key tool (`search/*`, `hubspot/*`, `shopify/*`, `intercom/*`, `quickbooks/*`, `brave`) reads `ctx.apiKeys.<provider>`. The HTTP transport's `contextProvider` had been returning a bare ctx with no `apiKeys` field, so any of those tools threw the "Add one at /settings/api-keys" precondition on every CLI / SDK call even when a key was stored. - Now `contextProvider` calls `loadUserApiKeys()` and forwards the decrypted map. Chat-surface behaviour is unchanged. - Pure plumbing fix — no new tools. Tool count stays at 239.

    • /api/mcp/v1 honours resolveActiveWorkspaceId instead of always picking the first membership row
    • /api/mcp/v1 contextProvider now loads loadUserApiKeys() so CLI / SDK can call third-party-key tools
  57. 1.7.266
    M2
    Wave 4b closeout — search/images + search/places via Brave Search (239 tools)Shipped May 23, 2026

    - Wave 4b's image + place slots were stuck waiting on a second provider since Tavily doesn't return either reliably. Brave Search delivers both, so the last two `search/*` tools land via Brave's API. - `search/images` — `/res/v1/images/search` with `safesearch` (off / strict, default strict), optional `country` (ISO-3166 alpha-2), `count` 1–100. Returns thumbnails + page URLs + dimensions. - `search/places` — `/res/v1/local/pois/search` for nearby POIs (restaurants, shops, landmarks). Optional `country` + `count` 1–20. - New `brave` slot in `encrypted_api_keys` carries the Brave subscription token (`BSA…`). Header is `X-Subscription-Token` per Brave's spec. Plumbed through ApiKeys / loader / route / form rows. Endpoint hostname `api.search.brave.com` added to DEFAULT_ALLOWLIST. - Tool count: 237 → 239. Wave 4b closed (search has all 4 of the master plan's slots: web, news, images, places). - i18n: 2 new `mcp_tool_descriptions.*` keys in en + he.

    • search/images + search/places MCP tools via Brave Search API (closes Wave 4b)
    • Brave subscription token row in /settings/api-keys
  58. 1.7.265
    M2
    Wave 12 finance — QuickBooks Online (invoice / customer / transaction) — 237 toolsShipped May 23, 2026

    - Last Wave 12 slot lands. Three read tools wrap QuickBooks Online v3. - `quickbooks/invoice` — GET /invoice/{id}, returns the upstream `{ Invoice, time }` payload verbatim. - `quickbooks/customer` — GET /customer/{id}, same shape under `Customer`. - `quickbooks/transaction` — `/reports/TransactionList`, every transaction kind in one report; optional `start_date` / `end_date` (YYYY-MM-DD) + `payment_method` + `account` filters. - Three new `encrypted_api_keys` slots: `quickbooks` (OAuth access token), `quickbooks_realm` (Intuit company id), `quickbooks_env` (production / sandbox; defaults to production). Base hostnames `quickbooks.api.intuit.com` + `sandbox-quickbooks.api.intuit.com` added to DEFAULT_ALLOWLIST. - QBO tokens expire after 1h — tools don't auto-refresh; user pastes a fresh token from their Intuit dashboard when needed. Auto-rotate via OAuth refresh-flow is queued for a later wave (needs per-workspace client-secret storage + a refresh background job). - Tool count: 234 → 237. **Wave 12 closed** — HubSpot ✅, Shopify ✅, Intercom ✅, QuickBooks ✅. - i18n: 3 new `mcp_tool_descriptions.*` keys + `quickbooks` category label in en + he.

    • Wave 12 closed: HubSpot + Shopify + Intercom + QuickBooks all live
    • Three QBO key slots (token, realmId, env) in /settings/api-keys with production/sandbox selector
    • quickbooks/invoice + quickbooks/customer + quickbooks/transaction MCP tools (QBO API v3 read)
  59. 1.7.264
    M2
    Wave 12 support — Intercom (conversations / users / tags) — 234 toolsShipped May 23, 2026

    - Third Wave 12 slot lands. Three read tools wrap Intercom's API v2.11 at `api.intercom.io`. - `intercom/conversations` — list inbox conversations newest-first, optional `state` (open / closed / snoozed), pagination via `per_page` + `starting_after`. - `intercom/users` — list contacts (Intercom renamed users → contacts in v2.x), optional `role` filter (user / lead). - `intercom/tags` — list every workspace tag. No pagination — Intercom returns the full set inline. - Auth via new `intercom` slot in `encrypted_api_keys` (workspace access token). `Intercom-Version: 2.11` header pins the API version against the response shape the tools expect. `api.intercom.io` added to DEFAULT_ALLOWLIST. - Tool count: 231 → 234. Wave 12 now has HubSpot ✅, Shopify ✅, Intercom ✅; only QuickBooks remains. - i18n: 3 new `mcp_tool_descriptions.*` keys + `intercom` category label in en + he.

    • intercom/conversations + intercom/users + intercom/tags MCP tools (API v2.11 read)
    • Intercom workspace access token row in /settings/api-keys
  60. 1.7.263
    M2
    Wave 12 ecommerce — Shopify Admin (products / orders / customers / inventory) — 231 toolsShipped May 23, 2026

    - Second Wave 12 slot lands. Four read tools wrap Shopify's Admin REST API at `<shop>.myshopify.com/admin/api/2024-10/...`. - `shopify/products` — list with optional title query / status / vendor / limit (1–250). - `shopify/orders` — list with optional status / financial_status / fulfillment_status / created_at_min / limit. - `shopify/customers` — switches between `/customers.json` and `/customers/search.json` based on whether `query` is set. - `shopify/inventory` — list inventory levels for `location_ids` or `inventory_item_ids` (at least one required). - Auth via new `shopify` (Admin access token, `shpat_…`) + `shopify_shop` (subdomain) slots in `encrypted_api_keys`. Plumbed through `ApiKeys` interface, `loadUserApiKeys()` switch, `/api/user/api-keys` PROVIDERS, and two `<ApiKeysForm>` rows. - **Allowlist wildcard support**: the hostname guard now matches entries beginning with `*.` against any subdomain (e.g. `*.myshopify.com` clears `acme.myshopify.com`, `my-store.myshopify.com`, …). Stops short of full-wildcard fallback — only the explicit `*.` prefix counts. - Tool count: 227 → 231. Wave 12 still has Intercom + QuickBooks queued. - i18n: 4 new `mcp_tool_descriptions.*` keys + `shopify` category label in en + he.

    • Shopify token + shop subdomain rows in /settings/api-keys
    • http guard supports `*.` wildcard entries so per-tenant SaaS hosts like `*.myshopify.com` clear the allowlist
    • shopify/products + shopify/orders + shopify/customers + shopify/inventory MCP tools (Admin REST 2024-10)
  61. 1.7.262
    M2
    Wave 12 starter — HubSpot CRM (contact / deal / company / engagement) — 227 toolsShipped May 23, 2026

    - First Wave 12 (finance / ecommerce / CRM) slot lands. Four read tools wrap HubSpot's CRM v3 API: - `hubspot/contact` — fetch one contact by id, optional `properties` list (caps at 50 names). - `hubspot/deal` — same shape for deals (dealname, amount, dealstage, closedate, pipeline). - `hubspot/company` — same shape for companies (name, domain, industry, numberofemployees, annualrevenue). - `hubspot/engagement` — discriminated on `kind` (calls / emails / meetings / notes / tasks); CRM v3 routes them under `/crm/v3/objects/<kind>/<id>`. - Auth via new `hubspot` slot in `encrypted_api_keys` (private-app token, `pat-na1-…` shape). Plumbed through `ApiKeys` interface in `@angi/llm`, `loadUserApiKeys()` switch, `/api/user/api-keys` route, and the `<ApiKeysForm>` row. `api.hubapi.com` added to `DEFAULT_ALLOWLIST` so the hostname guard + 1 MiB body cap apply. - All four tools take an optional `properties` list (max 50 names) so the LLM can narrow the response to the columns it actually wants and stay under the body cap. HubSpot's response is returned verbatim — the chat agent reads the shape directly. - Tool count: 223 → 227. Wave 12 still has Shopify, Intercom, QuickBooks queued (each needs their own provider-key UI row + tools file). - i18n: 4 new `mcp_tool_descriptions.*` keys + `hubspot` category label in en + he.

    • HubSpot private-app token row in /settings/api-keys (encrypted store, masked round-trip)
    • api.hubapi.com added to DEFAULT_ALLOWLIST so the http guard + 1 MiB body cap apply to every HubSpot call
    • hubspot/contact, hubspot/deal, hubspot/company, hubspot/engagement MCP tools (CRM v3 read-by-id with optional properties narrowing)
  62. 1.7.261
    M5
    Federation MCP tools — list_peers, list_shares, share_swarm, revoke_share (223 tools)Shipped May 23, 2026

    - Four new MCP tools surface the existing federation trust-ring + resource-sharing surface to the LLM. The handshake + signature flow itself stayed in the existing `/api/federation/*` HTTP routes — these tools are data accessors + share toggles that ride the caller's RLS-scoped supabase client. - `federation/list_peers` — list `public.peers` for the workspace. Filter by status (pending / active / blocked). Returns id, label, peer_url, status, created_at, last_seen_at. - `federation/list_shares` — list `public.peer_shares` filtered to peers in the workspace via the FK + workspace-id join. Optional filters on peer_id, resource_kind, direction. - `federation/share_swarm` — upsert a `peer_shares` row (kind=swarm, direction=out by default; bidirectional optional). Pre-flight check verifies both peer + swarm belong to the caller's workspace and the peer isn't blocked, so the LLM gets a clear precondition error instead of an opaque "permission denied". - `federation/revoke_share` — delete a share by id. RLS gates to workspace owner/admin per migration 0029. - Tool count: 219 → 223. Closes the queued federation surface from the master plan's "post-handshake" line — only thing left here is the replication / conflict-resolution library work, which stays library-side. - i18n: 4 new `mcp_tool_descriptions.*` keys + a `federation` category label in en + he.

    • federation/share_swarm upserts a peer_shares row with pre-flight peer + swarm + status checks
    • federation/revoke_share deletes a share by id; RLS gates to workspace owner/admin
    • federation/list_peers + federation/list_shares — RLS-scoped data accessors for the trust ring
  63. 1.7.260
    M2
    Wave 11 closeout — image/resize, image/crop, audio/summarise (219 tools)Shipped May 23, 2026

    - Three more MCP tools land under the existing image / audio categories. - `image/resize` — wsrv.nl URL builder. Accepts width / height (at least one), fit mode (inside / cover / contain / fill), output format, quality. Returns { url, source } — no fetch in the handler, so the tool is cheap and proxy-bounded by wsrv.nl's own infra. - `image/crop` — same wsrv backend, crop to fixed w×h with smart / center / top / bottom / left / right / attention anchor. Returns { url, source }. - `audio/summarise` — chains Whisper-1 transcription + ctx.generate. Pulls the audio URL, transcribes via the same multipart flow as audio/transcribe, then summarises through the chat's active LLM (closure provided by route.ts). Style toggle: bullets (default), paragraph, tldr. Returns { transcript, summary, language, duration }. - `wsrv.nl` + `images.weserv.nl` added to DEFAULT_ALLOWLIST so downstream fetches of the transformed assets (via http/get or the browser) clear the hostname guard. - Tool count: 216 → 219. Wave 11 image now 4/4 complete (describe, ocr, resize, crop), audio 2/2 (transcribe, summarise), scrape 4/4 done. Wave 11 video still queued (needs ffmpeg). - i18n: 3 new mcp_tool_descriptions in en + he.

    • image/crop MCP tool — same wsrv backend, fixed w×h crop with smart / center / top / bottom / left / right / attention anchor
    • audio/summarise chains Whisper-1 + ctx.generate; returns { transcript, summary, language, duration } in bullets / paragraph / tldr styles
    • image/resize MCP tool — wsrv.nl URL builder with fit modes (inside / cover / contain / fill) + optional format/quality
  64. 1.7.259
    M4
    LM Studio — auto-append /v1 to host so chat-completions hits the OpenAI pathShipped May 23, 2026

    - User report: "Unexpected endpoint or method. (POST /chat/completions). Returning 200 anyway" in LM Studio's log, paired with the chat surface hanging on the assistant turn (no streamed body). - Root cause: the stored `lmstudio_host` was a plain `http://localhost:1234` (no `/v1` suffix). The OpenAI SDK posts to `<baseURL>/chat/completions`, which resolves to `/chat/completions` — LM Studio doesn't serve that path, so it 200s with the error-line log and an empty body, the AI SDK stream sees nothing, and the user's chat turn dies silently. - Fix in `packages/llm/src/factory.ts`: normalize the host before handing it to `createOpenAI`. Trim trailing slashes; if the result doesn't already end in `/v1`, append one. Same normalization the test probe already does. Default placeholder also relaxed from `http://localhost:1234/v1` to `http://localhost:1234` since the suffix is now appended automatically. - Existing `/settings/api-keys` rows survive — users who stored a `/v1` URL still work, users with the bare host now also work without re-saving.

    • Default placeholder relaxed from http://localhost:1234/v1 to http://localhost:1234 (suffix added at runtime)
    • createModel("lmstudio") normalizes the host so a missing /v1 suffix is auto-appended before chat-completions calls
  65. 1.7.258
    M4
    LM Studio — drop legacy lm-studio-unused placeholder, send no Authorization when no tokenShipped May 23, 2026

    - User hit "401 { error: { message: 'Malformed LM Studio API token provided: lm-studio-******. Ensure you are using a valid token...' } }" on the LM Studio connection test. Newer LM Studio releases (post-v0.3 dev API) enforce a strict token validator and reject the legacy `Bearer lm-studio-unused` placeholder we'd been sending since v1.7.221. - Two fixes, one symptom each: - **Test probe** (`/api/user/api-keys/test/lmstudio_host`): `probeLmStudio()` now accepts the stored `lmstudio_token` (passed through from the same `apiKeys` map the chat surface uses) and only sends `Authorization: Bearer <token>` when present. No-auth LM Studio servers — the default install — get a header-less request and respond 200. - **Chat runtime** (`packages/llm/src/factory.ts`): the OpenAI SDK requires a non-empty `apiKey` at construction time, so we keep `'noauth'` as the placeholder but use a custom `fetch` override that strips both `authorization` and `Authorization` headers before forwarding. When the user HAS stored a token we skip the override and let the SDK send the real Bearer token. Either path matches what the probe does. - Net: token-less LM Studio works again. Users on `lms server start --api-key <token>` still authenticate normally.

    • LM Studio runtime uses a fetch override to strip the Authorization header when no real token is configured
    • probeLmStudio sends Authorization header only when lmstudio_token is stored (no-auth instances stop 401ing)
  66. 1.7.257
    M2
    Wave 11 media — image/describe, image/ocr, audio/transcribe (216 tools)Shipped May 23, 2026

    - Three new MCP tools wrap OpenAI's multimodal endpoints. All read `ctx.apiKeys.openai` first, falling back to `OPENAI_API_KEY`. `api.openai.com` already lives on `DEFAULT_ALLOWLIST` so the hostname guard + 1 MiB cap apply automatically to the vision calls. - `image/describe` — gpt-4o-mini multimodal. Returns a one-paragraph description of an image URL. Optional `prompt` overrides the default for task-specific framing. - `image/ocr` — same model, OCR-only prompt. Returns extracted text in reading order, or empty string when no text is visible. - `audio/transcribe` — Whisper-1 endpoint. Pulls the source bytes through fetch + size-caps at 25 MiB, builds a multipart upload with derived filename + content-type, posts to `/v1/audio/transcriptions` with `response_format=verbose_json`. Returns `{ text, language, duration }`. Accepts optional `language` hint and `prompt` for proper-noun biasing. - Tool count: 213 → 216. Closes wave-11 image (4/4) and starts wave-11 audio (1/2 — `audio/summarise` queued). Wave-11 video (`transcribe`, `keyframes`) still queued — needs ffmpeg. - i18n: 3 new `mcp_tool_descriptions.*` keys + `image` / `audio` category labels in en + he.

    • image/ocr MCP tool (gpt-4o-mini vision, OCR-only prompt, returns extracted text in reading order)
    • audio/transcribe MCP tool (Whisper-1 multipart upload, 25 MiB cap, returns text + language + duration)
    • image/describe MCP tool (gpt-4o-mini vision, one-paragraph description with optional prompt override)
  67. 1.7.256
    M2
    scrape/table — close out wave 11 starter (213 tools)Shipped May 12, 2026

    - Closes the last queued slot in wave-11's scrape category. New MCP tool `scrape/table` extracts a single HTML table from a remote page. - Algorithm: - Match `<table …>…</table>` blocks via regex (no DOM parser — keeps the dep surface flat). Pick by 0-based `index` (default 0). - Walk `<tr>` rows. First row with any `<th>` cell becomes the header; subsequent rows are data and matched against the header by column index. - Cell contents go through the existing `stripTags` helper so embedded `<a>` / `<span>` markup collapses to plain text. - Caps at 500 rows × 50 columns × 500 chars/cell to keep the response under the existing 1 MiB body guard. - Returns `{ url, header, rows, total_tables }`. `rows` is `Record<string,string>[]` when a header was detected, or `string[][]` for plain data tables. Either shape is LLM-friendly for downstream summary / extraction. - i18n: `mcp_tool_descriptions.scrape_table` in both en + he.

    • Tool count 212 -> 213; closes the last queued scrape/* slot from the wave-11 starter
    • scrape/table MCP tool — regex-driven HTML table extractor, returns keyed or array rows depending on <th> presence
  68. 1.7.255
    M4
    Plan dialog attaches swarm to active chat instead of always minting newShipped May 12, 2026

    - The composer's PlanDialog (v1.7.253) always took the user to a fresh chat on Start. Now when the dialog is opened from inside an existing chat, the deployed swarm attaches in place and the goal seeds as the next user message in the SAME chat. - `/api/goal/execute` accepts an optional `chat_id` body field. When present: - Validates the chat belongs to the active workspace (RLS handles tenant scope; we also pin `user_id = auth.uid()` to defend against shared workspaces). - Returns 409 if the chat already has a `swarm_id` — won't silently replace a prior swarm. - Updates the existing chat (sets `swarm_id`, merges plan markdown into `system_prompt`, preserves user-authored prefix). - Inserts the goal as the next user message (instead of as the first). - Returns `attached_to_existing: true` alongside `chat_id`. - PlanDialog passes the active `chatId` when set; on success branches: - `attached_to_existing` → `router.refresh()` so SwarmProgress, SystemPrompt strip, and message transcript pick up the new state. - otherwise → `router.push('/c?chat=<new_id>')` (existing v1.7.252 behavior).

    • PlanDialog passes chatId, uses router.refresh() when attaching in place; falls back to router.push for new-chat case
    • /api/goal/execute optional chat_id — attaches swarm in place + seeds goal as next message (409 if swarm already attached)
  69. 1.7.254
    M2
    Wave 4b — search/web + search/news via Tavily (+2 tools = 212)Shipped May 12, 2026

    - Closes the long-queued wave-4b search slot from the tool roadmap. Two new MCP tools wrap Tavily's `/search` endpoint: - `search/web` — general web search; ranked results with title / url / content snippet. Optional `include_answer` asks Tavily for a one-paragraph synthesized answer for factoid questions. - `search/news` — same shape scoped to news sources, with optional `days` filter (1–30) for recency. - New `tavily` row in the encrypted_api_keys map. Plumbed through: - `ApiKeys` interface in `@angi/llm`. - `loadUserApiKeys()` switch in `apps/chat/lib/api-keys-server.ts`. - `/api/user/api-keys` PROVIDERS allow-list + masked GET shape. - `<ApiKeysForm>` row (label "Tavily search", docs link, placeholder `tvly-…`, blurb). - Extended `ToolHandlerCtx` with an optional `apiKeys` field so search/* tools can prefer the user-supplied key over the deployment-wide `TAVILY_API_KEY` env var. Chat route now passes the decrypted map through. - Image + place search still queued — Tavily doesn't return them reliably; later wave will layer Brave / Serper / Google Places. - New `mcp_category_names.search` ("Web search" / "חיפוש אינטרנט") + `mcp_tool_descriptions.search_web` + `mcp_tool_descriptions.search_news` in en + he.

    • Tavily key row in /settings/api-keys (encrypted store, masked round-trip)
    • ToolHandlerCtx grew an optional apiKeys field so search/* (and future third-party tools) can read per-user keys without re-touching the users table
    • search/web + search/news MCP tools via Tavily (live web + news search, ranked snippets, optional synthesized answer)
  70. 1.7.253
    M4
    In-chat Plan button — goal planner moves from settings to composer toolbarShipped May 12, 2026

    - The goal planner used to live behind a Settings → Goal Planner tab + an entry in the sidebar gear menu. User asked for it inside the chat itself so planning + execution don't require a context switch. - New `<PlanDialog>` mounts in the composer toolbar next to `<SwarmAttach>`. Click the violet **תכנון** button → a 420 px popover slides up with a goal textarea, the Plan button, an inline LLM-rendered plan tree, and an emerald Start button. The Start path calls `/api/goal/execute` (v1.7.252) and `router.push('/c?chat=<new_id>')` on success. - Removed the `goal` tab from `<SettingsDialog>` + the matching entry in `<Sidebar>`'s gear menu. `SettingsKey` union, `tabs` array, `Target` icon import, and `GoalClient` re-import all cleared. The standalone `/goal` route stays in place for direct linking + sharing. - 4 new bilingual i18n keys: `chat.plan_button`, `chat.plan_button_tooltip`, `chat.plan_dialog_title`, `chat.plan_planning`. Hebrew labels: `תכנון`, `מתכנן…`, `תכנון מטרה`.

    • 4 new bilingual i18n keys for the in-chat plan flow
    • Removed `goal` SettingsKey tab from SettingsDialog + Sidebar gear menu (standalone /goal route stays)
    • New PlanDialog in the composer toolbar — goal textarea + plan tree + Start in one popover
  71. 1.7.252
    M5
    Goal planner — Start button deploys a swarm-attached chat from the planShipped May 12, 2026

    - The goal page used to end at "here is your plan" with no path forward. Added an emerald **Start — deploy swarm** button under the plan tree that takes the LLM/heuristic plan and turns it into a working chat. - New server route `/api/goal/execute`: 1. Walks the plan tree, collects the set of unique `agent_type` values, and maps plan-side types (`planner` / `analyst` / `summarizer` / `translator` / `general`) onto the swarm-side `AgentType` union (`coordinator` / `researcher`) since the swarm taxonomy is narrower. 2. Deploys a hierarchical swarm via `SwarmCoordinator.deploy({ agent_types })` — one agent per role, RLS-scoped to the active workspace. 3. Creates a chat row with the new swarm attached, model resolved from `users.default_model` (falls back to `pickReachableModel`), and a system prompt seeded with the goal + plan markdown so the orchestrator LLM lands with full context. 4. Inserts the original goal as the first user message so the chat surface streams a coherent first turn the moment the user arrives. - Goal client redirects to `/c?chat=<new_id>` via `router.replace()` on success. Errors surface inline in a rose-bordered banner under the Start button. - 3 new i18n keys (en + he): `execute_start`, `executing`, `execute_hint`.

    • System prompt seeded with goal + plan markdown so the chat LLM lands with full execution context
    • Start button on the goal page calls execute, redirects to /c?chat=<new_id> on success
    • /api/goal/execute deploys a swarm-attached chat from a plan tree, with plan->swarm AgentType mapping
  72. 1.7.251
    M4
    Goal planner — maxTokens 4000, open-fence + balanceJson recovery from truncated outputShipped May 12, 2026

    - User saw `Model returned non-JSON output: ```json { "kind": "goal", "title": "אני רוצה שתבצעו עבורי מחקר…"` — the model hit `maxTokens: 1_200` mid-output, before writing the closing brace or fence. `extractJson()` required a closed fence and `JSON.parse` choked on the partial structure. - Three combined fixes: - **`maxTokens` bumped 1200 → 4000**. Hebrew tokenizes 1.5–2× heavier than English; a 5-idea / 12-node tree with translated detail strings was hitting the old cap. 4000 gives the model room for a complete tree across every locale without becoming a runaway budget. - **`extractJson()` now matches open-only fences** (`` ```json ... ``) `` with no trailing `` ``` ``) using a second regex. The closed-fence form stays as the preferred case. - **New `balanceJson()` recovery pass**. When the first `JSON.parse(extracted)` throws, we stack-walk the input to balance `{`/`[` brackets and any open string literal, trim the dangling fragment back to the last comma or opener, then re-balance + append the missing closers in reverse. The route tries `JSON.parse(balanceJson(extracted))` as a second attempt before falling through to the partial-plan path. - The previously-shipped partial-plan fallback (v1.7.248) stays as the final safety net — if even balanceJson can't produce parseable JSON, the user gets a single-node "goal" with the model's prose as detail instead of the heuristic stub.

    • balanceJson stack-walks the partial output and auto-closes missing braces/brackets/strings so JSON.parse can recover the prefix
    • extractJson matches open-only fenced blocks when the model truncates before the closing ```
    • Goal planner maxTokens bumped 1200 -> 4000 to fit Hebrew + long research trees
  73. 1.7.250
    M4
    Goal planner — respect users.default_model from profileShipped May 12, 2026

    - The goal planner was hard-coded to `claude-sonnet-4-6` whenever the caller didn't pass an explicit `model` field, ignoring the user's profile preference and 401'ing anyone without an Anthropic key. - New precedence chain: 1. `body.model` — explicit caller override (CLI / SDK). 2. `users.default_model` — the profile preference users set in `/settings/profile`. 3. `pickReachableModel` fallback — first MODEL_CATALOG entry whose provider has a reachable key. - Reads the profile mirror in parallel with `loadUserApiKeys` (same SSR pattern as `/api/user/me`), so the model-resolution latency stays unchanged.

    • Profile-mirror SELECT runs in parallel with API-keys load to keep planner latency unchanged
    • Goal planner reads users.default_model when body.model is missing, instead of hard-coded claude-sonnet-4-6
  74. 1.7.249
    M9
    Profile: detect has_password, hide current-password field for magic-link usersShipped May 12, 2026

    - v1.7.247 added the password-change form but assumed every user already has a password. Magic-link / OAuth-only users have `auth.users.encrypted_password = NULL`, so the re-signin proof step would always fail and the user couldn't set a first password. - New SECURITY DEFINER RPC `public.app_user_has_password()` reads `auth.users.encrypted_password IS NOT NULL` for `auth.uid()`. Gated to the `authenticated` role only; anon stays blocked. The auth schema is otherwise locked down — the function is the only legitimate read path from the chat app's user-RLS supabase client. - `/api/user/me` GET now calls the RPC and returns `user.has_password: boolean` alongside the existing profile fields. - `ProfileLoader` plumbs the flag through to `ProfileForm`; the form: - Hides the "current password" input when `hasPassword === false`. - Skips the `signInWithPassword` proof step in that branch — the active session (from the magic-link click) is the proof Supabase needs for `updateUser({ password })`. - Drops the "new must differ from current" check. - Swaps the section heading to "Set a password" and the button to "Set password" with a magic-link-specific help line. - After a successful first-time set, flips local `hasPassword` to true so the next submit follows the normal proof-required path. - New i18n keys (en + he): `password_section_setup`, `password_section_setup_help`, `password_set`.

    • Section heading and button copy switch to "Set a password" / "Set password" for first-time users (en + he)
    • /api/user/me returns user.has_password; ProfileForm conditionally hides current-password input + skips proof step for magic-link users
    • Migration 0036: app_user_has_password() SECURITY DEFINER RPC reads auth.users.encrypted_password for auth.uid()
  75. 1.7.248
    M4
    Goal planner — full tree normalize + best-effort fallback + real error surfacingShipped May 12, 2026

    - User reported the goal planner still falling back to the heuristic stub on Hebrew research prompts even after v1.7.246's agent_type widening — different schema fields were tripping the strict validator (`kind` values outside the enum, empty leaf titles, surprise top-level shapes). - New `normalizePlan(node, depth, fallbackTitle)` walks the entire JSON tree before validation and coerces every field: - `kind` snaps to the closest of `goal` / `step` / `leaf` based on depth + child count. - `title` falls back to the parent step label or the user goal (truncated to 80 chars). - `agent_type` retains the v1.7.246 whitelist behavior; non-catalog values get dropped. - `detail` trimmed; empty becomes undefined. - `children` filtered to objects only, recursed with depth+1. - Even after normalize, if the strict zod schema STILL fails (deeply weird shape), the route now returns a `partial: true` plan with the user goal as title + the model's raw prose (capped at 200 chars) as detail — instead of 502'ing the request and dumping the user to the heuristic stub. A Sentry warning logs the schema diff for follow-up. - Goal client now prefers the route's `body.error` string over the generic translated `api_errors.server_error` so the user sees the real reason (e.g. "Model returned non-JSON output: …") instead of the flattened "something went wrong". - New `goal_page.partial_hint` localized string explains the partial path when it triggers.

    • Goal client now surfaces body.error instead of the flattened server_error code
    • Best-effort partial plan path replaces 502 + stub fallback when zod still fails post-normalize
    • normalizePlan coerces kind / title / agent_type / detail / children before zod — strict schema can no longer 502 the request
  76. 1.7.247
    M9
    Profile: password change via Supabase Auth (current-password verified)Shipped May 12, 2026

    - Added a Password section to `/settings/profile` with three controlled inputs (current / new / confirm) + a show-password toggle and an independent status line. - **Security flow**: 1. Local validation — new password ≥ 8 chars, matches confirmation, differs from the current value entered. 2. **Re-signin with email + current password** via `supabase.auth.signInWithPassword` to prove identity. Without this step Supabase's `updateUser({ password })` would let a stolen access token rotate the password with no proof of the previous one. 3. `supabase.auth.updateUser({ password: new })` — hashed server-side by Supabase Auth (bcrypt). The plaintext never touches our `users` table; only the hashed credential lives in `auth.users.encrypted_password`. - Three new error states map to localized strings: `password_current_wrong`, `password_too_short`, `password_mismatch`, `password_same`. - Bilingual: `profile_page.password_*` keys land in both en + he.

    • Password change form in /settings/profile (current + new + confirm, show toggle)
    • Password stored hashed (bcrypt) in auth.users.encrypted_password by Supabase Auth — plaintext never touches public.users
    • Identity verified by re-signing in with the current password before updateUser fires
  77. 1.7.246
    M4
    Goal planner — widen agent_type catalog, sanitize unknown values, lenient JSON extractionShipped May 12, 2026

    - User reported the goal planner falling back to the heuristic stub on a Hebrew "research which app to build to earn 1M ILS in 3 months" prompt. Root cause: `/api/goal/plan` validated `agent_type` against a hard 5-archetype enum (architect / coder / tester / reviewer / security-architect). Gemini emitted `agent_type: 'researcher'` (and friends) for research-style plans → schema failed → 502 → client fell back with "LLM planner not available". - Widened the schema enum to 12 archetypes mirroring the i18n `agent_types.*` catalog: architect, coder, tester, reviewer, security-architect, security-auditor, researcher, planner, analyst, summarizer, translator, general. - Updated the SYSTEM prompt: announces the wider catalog + tells the model to keep the title/detail strings in the user's language + adds a research-style preference order (researcher → analyst → planner → summarizer) for non-software goals. - New `sanitizeAgentTypes()` walks the parsed JSON before schema validation. If a single odd child slips through (e.g. `business-analyst`), the field is stripped silently so the rest of the plan still passes. The renderer treats a missing agent_type as "general". - `extractJson()` now slices between the outermost matching `{` / `}` when the model writes prose before or after the JSON — Gemini does this a lot.

    • extractJson now slices between outermost braces when the model wraps JSON with prose
    • sanitizeAgentTypes strips unknown agent_type values instead of failing the whole plan
    • SYSTEM prompt teaches the model researcher → analyst → planner → summarizer order for research-style goals
    • Goal planner agent_type enum widened from 5 to 12 archetypes (matches i18n agent_types.* catalog)
  78. 1.7.245
    M4
    Defensive wrap on per-provider tool cap — "Failed to fetch" hotfixShipped May 12, 2026

    - User reported a generic "Failed to fetch" error in the chat surface after v1.7.243 introduced the per-provider tool cap. A throw inside `capToolsForProvider()` would surface as a fetch-level network error in the browser because the streaming response aborts before any data writes. - Wrapped the cap call in a try/catch. Any error reports to Sentry with `step: 'cap-tools'` + provider + modelId context, and the route falls back to the full tool set so the turn proceeds. - The most common trigger for "Failed to fetch" with these changes was likely a stale dev server — the file rename in v1.7.244 (AngiBrandMark moved from apps/chat → packages/ui) confused HMR. A `pnpm dev` restart clears it. The defensive wrap covers any latent edge case in the cap logic itself.

    • Cap-tools errors now report to Sentry with provider + modelId context
    • Wrap `capToolsForProvider` in try/catch — falls back to full tool set on error instead of aborting the stream
  79. 1.7.244
    M4
    AngiBrandMark promoted to @angi/ui — header + footer mounts everywhereShipped May 12, 2026

    - Promoted `AngiBrandMark` from `apps/chat/components/chat/` into the shared `@angi/ui` package so apps/web (marketing landing) + the chat-app site header/footer can mount the same animated glyph. - **apps/web header**: top-left logo swaps the static emerald→indigo "A" badge for the animated mark at `size-7`. - **apps/web footer**: brand row uses the mark at `size-6`. - **apps/chat site-header.tsx + site-footer.tsx**: the signed-out home + standalone `/updates` page also get the new glyph (these surfaces use the same `<SiteHeader />` / `<SiteFooter />` shared components). - Mirrored the `.animate-conic` CSS utility (+ `@property --angle` definition + `spin-angle` keyframes) into `apps/web/app/globals.css`. The CSS-only half of the recipe can't live in the npm package, so each app ships its own copy. - Made the SVG fill gradient id locally unique per mount (header + footer on same page used to collide on a global `angi-mark-fill` id).

    • Local SVG gradient ids per mount — no more cross-mount collisions on shared pages
    • apps/chat SiteHeader + SiteFooter (signed-out + /updates) also get the mark
    • apps/web header + footer mount the animated mark (replaces static emerald→indigo "A")
    • AngiBrandMark exported from @angi/ui (was apps/chat-local) for cross-app reuse
  80. 1.7.243
    M4
    Stop button + per-provider tool cap (Gemini, OpenAI, Qwen)Shipped May 12, 2026

    - **Stop button**: composer's submit button flips into a Stop button while the LLM is streaming. Chat-shell pipes `useChat.stop()` into a new `onStop` prop; the streaming state renders a muted square with `Square fill="currentColor"` instead of the ArrowUp + Loader2 spinner. Aborts long swarm-research turns without waiting for `maxSteps` to drain. - **Per-provider tool cap**: the 210-tool registry was being dumped to every provider. Gemini chokes past ~64 declarations and silently emits a plain-text reply with no tool use — that's why a user's 9-agent swarm-research turn returned a generic markdown answer with no tools fired. Caps (anthropic 210 / openai 96 / google 48 / qwen 48 / ollama 32 / lmstudio 32) keep the registry under each provider's working threshold. - Selection priority: when capping, orchestration categories (swarm, agent, task, memory, chat, workspace) always survive, then research (http, scrape, extract, summarize, classify, rewrite, translate), then doc/markdown/json/regex/text/string utilities, etc. Categories outside the priority list fill any remaining slot.

    • Swarm orchestration tools always survive the cap; research + utility tools fill remaining slots by priority
    • Per-provider tool cap — Gemini 48 / OpenAI 96 / Qwen 48 / Ollama 32 (Anthropic uncapped)
    • Composer submit button flips into Stop button during streaming (wired to useChat.stop)
  81. 1.7.242
    M4
    Render-side tool-call adapter — unhide historical chatsShipped May 12, 2026

    - v1.7.237 added an onFinish normalizer that maps the AI SDK shape (`type: 'tool-call'` with `toolName` / `toolCallId`) to the canonical schema (`type: 'tool_call'` with `name` / `id`) before inserting into `messages`. Every turn since renders tool cards correctly. But every assistant turn persisted BEFORE that normalizer still carries the hyphenated shape, so MessageStream's discriminator-based renderer silently dropped those parts. - `normalizeRenderPart()` is the read-side mirror of the same mapping. MessageStream pipes every part through it before the type switch. Historical chats now surface their tool calls as cards the moment the user reloads — no DB writes, no migration risk. - Adapter accepts unknown part shapes safely (anything that doesn't match `text` / `thinking` / `tool_call*` / `tool_result*` falls through to a sentinel that the caller filters out), so future part types can't crash old rows.

    • Historical chats now show tool cards on reload without any DB migration
    • Render-side adapter unifies AI SDK hyphenated tool-call shape with canonical underscore shape
  82. 1.7.241
    M4
    Brand mark — rotate only the gradient angle, not the host elementShipped May 12, 2026

    - v1.7.240 still used `animate-spin` on the gradient layers, which rotated the divs themselves. With `rounded-2xl`, the rounded corners visibly spun around the center — the user flagged that the SHAPE was rotating instead of only the border glow. - Switched to `@property --angle` + `@keyframes spin-angle` interpolating the angle from `0deg` to `360deg`. The conic now uses `from var(--angle)` so the gradient's starting position animates while the host div stays still. Rounded corners no longer move; only the bright arc travels around the perimeter. - New `.animate-conic` utility lives in `apps/chat/app/globals.css`. Both layers (soft blurred bleed + crisp masked ring) share it so they stay phase-locked. - Browser support: Chrome 85+, Safari 16.4+, Firefox 128+ — covers the chat audience.

    • New `.animate-conic` utility in globals.css with `@property --angle`
    • Rounded corners no longer rotate — only the conic-gradient `--angle` animates
  83. 1.7.240
    M4
    Brand mark animation — traveling glow arc, not rotating spokesShipped May 12, 2026

    - The v1.7.239 mark animated a full-perimeter conic-gradient, which produced visible spokes and sharp corners — the user flagged it as ugly and shared the reference of a smooth glow traveling around the edge. - Rebuilt the recipe to match: a single ~25%-wide bright arc (sky-400 → emerald-400 inside, transparent elsewhere) renders twice: - **Outer ring** masked to a 1.5px stroke via `mask-composite: exclude` (Webkit prefix mirror for Safari ≥16). The mask clips the rotating arc to a thin border so the card's edge shows the colored stroke. - **Inner bleed** unmasked + heavier blur (6px) + 60% opacity. Sits behind the ring and bleeds a soft sheen onto the inner plate as the arc travels around, matching the reference's inward glow. - The static `linear-gradient(135deg, sky/18, emerald/18)` inner wash stays so there's always a tint to anchor the rotating glow. - Both ring + bleed share the same `animate-[spin_3.5s_linear_infinite]` so the bright arc + sheen stay phase-locked.

    • No more visible spokes / sharp corners on rotating logo glyph
    • Brand mark animation rebuilt as a single traveling arc (mask-composite ring + blurred inward bleed)
  84. 1.7.239
    M4
    Brand mark redesign — rotating sky→emerald gradient ringShipped May 12, 2026

    - New shared `AngiBrandMark` component renders the Angi glyph with a rotating conic-gradient ring (sky-400 → emerald-400 → sky-400 sweep) and an inner sky/emerald-tinted dark plate. The gradient palette deliberately echoes the composer's swarm button (sky outline) and MCP button (emerald outline) so the mark anchors the whole chat surface. - Sidebar header swaps the static emerald→indigo "A" badge for the animated mark at `size-7`. - Chat empty-state replaces the dead `bg-white/5` square above "How can I help today?" with the same mark at `size-14`, anchoring the hero. - Letter glyph rendered via SVG `<text>` inside a `viewBox` so the font auto-scales with the container — same component reads cleanly from `size-6` (sidebar) to `size-14` (hero) without per-size tweaks.

    • Sidebar logo + empty-state hero icon swap to AngiBrandMark (replaces static emerald→indigo "A" + dead black square)
    • AngiBrandMark shared component with rotating sky→emerald conic-gradient ring + dual-tone inner plate
  85. 1.7.238
    M4
    SwarmAttach: enable on empty chat + translate topology slugsShipped May 12, 2026

    - The "Attach swarm" button was grayed out on a freshly-opened New Chat because the picker required a `chatId` that only materializes after the user sends the first message. Chat-shell now passes its `ensureChat()` callback into `SwarmAttach`; the attach + deploy paths mint the chat row on demand, so the user can attach (or deploy) a swarm before sending a single message. - Topology slugs (`hierarchical`, `mesh`, `adaptive`, `hierarchical-mesh`) are now localized via `swarm.topology.*` keys. Hebrew renders these as `היררכי` / `רשת` / `אדפטיבי` / `היררכי · רשת` in both the deploy form's `<select>` and the existing-swarms list. Falls back to the raw slug when a new topology lands before its translation does. - The shadow of `t` as a map-variable in the topology `<select>` is gone (renamed to `slug`), so the translation hook is in scope where it's needed.

    • Topology slugs localized via `swarm.topology.*` in en+he (select + existing-swarms list)
    • Attach-swarm button enabled on empty chat (mints chat row on demand via chat-shell.ensureChat)
  86. 1.7.237
    M4
    Markdown rendering + tool-call normalization + universal tool-use guidanceShipped May 12, 2026

    - Assistant replies now render through `react-markdown` + `remark-gfm` instead of a single `<p whitespace-pre-wrap>`. Headings, bold, italics, ordered/unordered lists, tables, blockquotes, and fenced code blocks all map onto dark-theme Tailwind classes (violet links, zinc-50 strong, marker:text-zinc-500 lists). User-authored messages keep the prior plain layout. - Tool-call parts are now normalized on persistence: AI SDK emits `type: 'tool-call'` with hyphens + `toolName` / `toolCallId` fields, but the `MessagePartToolCallSchema` (and the `ToolCallCard` renderer) expect `type: 'tool_call'` with `name` / `id`. The mismatch meant tool calls were invisible to the user even though they ran successfully. `chat/route.ts` onFinish maps the shape before insert + back-fills `status='error'` from any matching `tool_result.is_error`. - Universal tool-use guidance suffix injected into every chat turn (not just swarm-attached chats). Lists the 210 MCP tools by category, asks the model to call `memory/search` + `http/get` + `scrape/extract` for research prompts, store conclusions via `memory/store`, and reply in GFM markdown matching the user's locale. - Added `react-markdown@^9.0.1` + `remark-gfm@^4.0.0` to the chat app deps.

    • Universal tool-use guidance in system prompt (210 MCP tools, research workflow, GFM markdown reply contract)
    • Tool-call parts normalized on insert (`tool-call` w/ `toolName` -> `tool_call` w/ `name`); tool calls now visible as cards
    • Assistant text parts render through react-markdown + remark-gfm (GFM tables, lists, code, links)
  87. 1.7.236
    M4
    Stop the green spinner from lying — gate on real activityShipped May 12, 2026

    - The green "Working on" Loader2 in the swarm-progress header used to spin whenever any task row was `in_progress`, even if the worker had crashed hours ago and nothing was actually happening. Replaced with a `hasLiveActivity` gate: spinner only rotates when the LLM is mid-stream, a task started within the last 30 s, or an agent heartbeat fired within 30 s. Otherwise we render a muted Clock icon. - Per-agent badge spinner was also too eager (v1.7.235 made it propagate the chat-shell `isLoading` to every agent unconditionally). Now: badge spins only when its task is fresh AND `hasLiveActivity` is true, so a stuck assignment can't keep the badge rotating across page reloads. - New `ACTIVITY_WINDOW_MS = 30_000` constant lives next to the existing `STALE_AFTER_MS = 5*60_000` heartbeat threshold.

    • Stale `in_progress` task rows render a muted Clock instead of a perpetual spinner
    • Header Loader2 only spins when LLM is streaming, a task is < 30s old, or an agent heartbeat is < 30s old
    • Per-agent badge spinner gated on `hasLiveActivity` + fresh assigned task
  88. 1.7.235
    M4
    Agent rendering polish — Hebrew agent names, active spinner, tool-card + thinking visualsShipped May 12, 2026

    - Agent type names now render in the active locale (Hebrew dict added for architect/coder/tester/reviewer/security-architect/security-auditor + 6 more archetypes). The badge in the swarm-progress strip + the `agent_type` chip in the agent-outputs panel both use the translated label. - Live "LLM is mid-stream" signal: chat-shell pipes `isLoading` into `SwarmProgress.llmActive`, which forces every agent badge in the active swarm to swap its static clock for a spinner while the LLM is producing the turn — the user sees "the system is working right now" without waiting for a task row. - ToolCallCard repainted to match the upstream M4 reference: filled violet badge with an inline check on success, amber spinner while pending/streaming, red ring on error. Trailing chevron now mirrors with `rtl:-scale-x-100` in Hebrew. - ThinkingBlock surfaces the first ~140 chars of the chain-of-thought inline (with the same `rtl:-scale-x-100` chevron) so the user can skim without expanding. Falls back to a localized "Thinking…" when the part is still streaming. - New shared `AssistantOrb` glyph anchors every assistant-side part (thinking, tool-call, text) so a multi-step turn reads as one column.

    • Hebrew labels for all built-in agent types (architect, coder, tester, reviewer, security-*, researcher, planner, analyst, summarizer, translator, general)
    • ThinkingBlock inline preview of the first reasoning line + RTL-safe chevron
    • ToolCallCard repainted to match the M4 reference (violet success badge, amber pending, red error)
    • Spinner on every agent badge while the LLM is streaming the turn
  89. 1.7.234
    M9
    CI pipeline hygiene — registry floor + lint cleanShipped May 12, 2026

    - Registry test guard now floors at **200 tools** instead of the stale 20 pin (the live count is **210** after waves 5a–11). Catches mass removals without re-revving the test on every routine tool addition. - Cleared the lone ESLint warning in `apps/chat/app/settings/mcp/mcp-servers-client.tsx` (mount-only `useEffect` flagged for missing `refresh` dep — annotated with the locale-stability rationale). - Local `pnpm test` + `pnpm lint` + `pnpm typecheck` + `pnpm build` all green on the queued batch.

    • Cleared the last ESLint warning on apps/chat (useEffect mount-only)
    • Registry test floors at 200 tools (was pinned at 20)
  90. v1.7.233
    M9
    Empty-assistant-reply fix — bump maxSteps + fallback summaryShipped May 12, 2026

    ## v1.7.233 — Stop blank-bubble after tool-heavy turns User report: a swarm-attached chat with the prompt "research an app that could earn 1M ILS in 3 months" landed with an empty assistant bubble + Regenerate button. Two causes feeding the same symptom: 1. **`maxSteps: 5` was tight.** A research-style prompt with a swarm attached fires `task/orchestrate` + `agent/list` + `memory/search` + a follow-up summary — at minimum 4 steps, often 6–8 before a final text chunk. Vercel AI SDK ends the stream cleanly when maxSteps is exhausted, so an LLM still mid-tool-loop leaves the user with nothing visible. Bumped to **12**. 2. **No fallback when the LLM emits zero text.** Even with 12 steps a stubborn turn can still close with only tool-call parts. Added an `onFinish` guard: if the persisted parts list contains no `text` part, append a Hebrew synthesised summary listing the tools the LLM actually invoked + pointing the user at the Swarm-progress "Agent outputs" panel. The chat stays useful instead of going silent. Both changes are server-side in /api/chat. No client changes.

    • onFinish guard: when the assistant produced zero text parts, append a Hebrew synthesised summary listing the tool names invoked so the user sees a useful reply instead of a blank bubble
    • streamText maxSteps bumped 5 → 12 — research-style prompts with a swarm attached now reach a final text chunk before the cap
  91. v1.7.232
    M9
    Gemini tool-schema fix — drop z.union types from registryShipped May 12, 2026

    ## v1.7.232 — Gemini-safe tool schemas User saw `AI_APICallError: Invalid JSON payload received. Unknown name "type" at 'tools.function_declarations[42].parameters.properties[2].value': Proto field is not repeating, cannot start list.` Gemini's generative-language proto rejects array-form JSON Schema `type` fields — exactly what zod-to-jsonschema emits for `z.union([z.string(), z.number(), z.boolean()])`. The error pointed at tools 42 + 96 in the registered order: * tools[42] → `url/query_set` — `value` was string|number|boolean. * tools[96] → `gh/actions_run` — `workflow_id` was string|number. Same union pattern lurked in `url/build` (`search` record value) and every `gitlab/*` tool (`project` / `id` was string|number). Flattened every offending union to a single type. Where the union existed to accept both numeric ids and string ids (GitLab project, GitHub workflow), callers now pass the value as a string; URL-encoding and string compare upstream handle both forms cleanly. Should ship as a transparent fix — Anthropic / OpenAI clients were unaffected by the unions, and the new single-type schemas are still valid for them.

    • Gemini AI_APICallError on tools[42] + tools[96] — flattened z.union types in url/query_set, url/build, gh/actions_run, every gitlab/* tool
  92. v1.7.231
    M9
    Built-in tools collapsed-state chevron — vertically centeredShipped May 12, 2026

    ## v1.7.231 — Center the chevron on the collapsed-state header Screenshot: when the Built-in tools panel was collapsed, the chevron rendered at the top of the row while the "Click to expand…" hint sat under the title — leaving the chevron looking detached + top-aligned. Caused by the hint being a sibling of the button instead of a child. Moved the hint INSIDE the button as a second line under the title. The button's `items-center` now vertically centres the chevron against the full two-line block (title row + hint row), so the chevron sits at the geometric center of the collapsed component in both LTR and RTL.

    • Built-in tools collapsed header: hint moved inside the button so the chevron vertically centers against the full two-line block
  93. v1.7.230
    M9
    Built-in tools panel header — chevron on the inline-end edgeShipped May 12, 2026

    ## v1.7.230 — Header chevron on the outer edge The panel-level expand/collapse button on the "Built-in tools" header used `justify-between` but stuffed every child into one span, so the chevron ended up glued to the title at the inline- start. In Hebrew that put it on the right next to the words — visually wrong; the chevron belongs on the outer edge so the user can hit it without aiming through the title. Fixed by splitting the row into two `justify-between` siblings: title + wrench on the start, chevron alone on the end. The chevron's `rtl:-scale-x-100` is preserved so it still points inward when collapsed.

    • Built-in tools panel header chevron moved to the inline-end of the row via justify-between split — sits on the outer edge in both LTR and RTL
  94. v1.7.229
    M9
    Recommended MCP servers — 15 vendor-official catalogShipped May 12, 2026

    ## v1.7.229 — Vendor-official MCP catalog Per user request: seed `/settings/mcp` with the verified-shipping official MCP servers so a user knows what's connectable without hunting through vendor docs. **Migration 0035** — `public.mcp_official_catalog` table (slug, name, vendor, transport ∈ {http, stdio, sse}, endpoint, docs_url, description, status ∈ {verified, preview, announced}, sort_order). Public-read RLS (catalog is non-sensitive). Author- side reserved for super-admin RPC; workspaces never write. **Seeded 15** verified-shipping rows: Cloudflare, GitHub, Stripe, Sentry, Vercel, Supabase, Linear, Notion, Atlassian, PostHog, Mintlify, Plaid, Asana, Replit, Resend. Each row carries the canonical endpoint URL + docs link as of mid-2026. **GET /api/mcp/catalog** — public, 5-min edge cache. **UI** — `/settings/mcp` page renders a new "Recommended servers" section above the user's own list. Per-vendor card shows: name + vendor + transport pill + description + monospace endpoint + "Docs ↗" link + "Copy endpoint" affordance. **No auto-connect** per user spec — user copies the endpoint into the add-server form when ready with credentials. New bilingual keys: `mcp_page.recommended_title`, `recommended_subtitle`, `recommended_docs`, `recommended_copy_endpoint`. **Caveats:** endpoint URLs reflect best-known mid-2026 vendor docs; operators should verify before relying. Status enum lets us ship `preview` rows later without UI changes.

    • Seeded 15 verified-shipping vendor MCP servers: Cloudflare, GitHub, Stripe, Sentry, Vercel, Supabase, Linear, Notion, Atlassian, PostHog, Mintlify, Plaid, Asana, Replit, Resend
    • 4 new bilingual mcp_page.recommended_* keys (en + he); cards use sky palette to distinguish from emerald built-ins + zinc user servers
    • GET /api/mcp/catalog with 5-min edge cache; /settings/mcp renders Recommended servers section above user list — per-vendor card with endpoint + Docs link + Copy endpoint; NO auto-connect per spec
    • Migration 0035 — public.mcp_official_catalog table (slug/name/vendor/transport/endpoint/docs_url/description/status); public-read RLS
  95. v1.7.228
    M9
    Translate suggestion chips + api-keys toasts + Get key linkShipped May 12, 2026

    ## v1.7.228 — Hebrew coverage pass User pointed at the suggestion-chip row that was still English in the Hebrew shell. Translated those + the surrounding i18n leftovers found during the sweep: **suggestion-chips.tsx (4 chips + label + prompts):** - Labels: Show progress / Add tests / Refactor / Summarize → הצג התקדמות / הוסף בדיקות / ריפקטור / סיכום. - Header "Try" → "נסה". - The dispatched prompt is also bilingual — clicking the chip now fires Hebrew text to the LLM in he.json, English in en.json, so the model responds in the caller's language by default. **api-keys form toasts:** - `${provider} saved` / `Failed to save ${provider}` / `${provider} removed` / `Failed to remove ${provider}` were hard-coded template literals. Now keyed under `api_keys_page.toast_*` with `{provider}` interpolation. - "Get key ↗" link wrapped in `api_keys_page.get_key`. New bilingual keys: `chat.suggestion_try`, nested `chat.suggestion.*` + `chat.suggestion_prompt.*` blocks, `api_keys_page.get_key`, `api_keys_page.toast_saved` / `toast_save_failed` / `toast_removed` / `toast_remove_failed`.

    • Chip-dispatched prompt itself is bilingual — LLM responds in the caller's language without an explicit hint
    • api-keys form: toast strings (saved / save_failed / removed / remove_failed) + "Get key" link no longer hard-coded English
    • Quick-action chips (Show progress / Add tests / Refactor / Summarize) translated to Hebrew via chat.suggestion.* + chat.suggestion_prompt.* nested dict blocks; "Try" header too
  96. v1.7.227
    M9
    MCP submenu — eager preload + skeleton loadersShipped May 12, 2026

    ## v1.7.227 — Hot MCP submenu + skeleton vocabulary Two speed fixes: 1. **Eager preload.** `AttachMenu` now starts the parallel `/api/mcp/servers` + `/api/mcp/builtin-tools` fetch the moment the composer mounts — not on first submenu hover. By the time the user clicks `+` the payload is typically already in memory; on a warm cache the submenu opens with zero spinner. 2. **Skeleton vocabulary.** New `components/ui/skeleton.tsx` exports `Skeleton`, `SkeletonRow`, `SkeletonList`. The MCP submenu loading state now renders five jittered skeleton rows instead of a "Loading MCP servers…" line, so the user sees the shape of what's coming + the panel feels alive. The pulse animation is pure CSS; aria-label inherited from the caller so screen readers still announce "Loading MCP servers". Width-jittered widths are pre-computed (no `Math.random` per render — would flicker between passes).

    • New components/ui/skeleton.tsx with Skeleton / SkeletonRow / SkeletonList primitives; MCP submenu loading replaced with 5 jittered skeleton rows
    • Skeleton widths pre-computed (no Math.random per render — would flicker between passes); pure-CSS pulse animation
    • AttachMenu eagerly preloads /api/mcp/servers + /api/mcp/builtin-tools on mount — submenu opens with zero spinner on a warm cache
  97. v1.7.226
    M9
    MCP submenu — styled toggle switches + collapsible categoriesShipped May 12, 2026

    ## v1.7.226 — Styled switches + drill-down categories Two upgrades on the MCP submenu: 1. **Toggle switches** replace the bare `<input type="checkbox">` rows. New `ToggleSwitch` component: 36×20 pill with a 16px thumb that slides via `start-*` (RTL-safe), filled track in emerald (built-ins) or blue (user servers). 2. **Expand / collapse per category.** Each built-in category row gets a chevron next to its name; click toggles the panel open, revealing the category's tools with the same name + localized description rows the /settings/mcp panel uses. Closed by default — the user can drill into 50 categories selectively without flooding the menu. Built-in switches stay locked at `on` (per-workspace disable lands in a later wave); user-server switches are real PATCH- backed toggles. Both use the same component for a consistent look. Per-category expansion state lives in component memory; closes naturally when the submenu closes.

    • expandedBuiltin Record<string, boolean> state in AttachMenu; component-memory only, closes naturally on submenu close
    • Built-in category rows are now expand/collapse — chevron toggles a panel showing each category's tools with localized descriptions
    • New ToggleSwitch component (36x20 pill with sliding thumb, RTL-safe via start-*) replaces checkbox inputs in the MCP submenu — variants blue (user servers) + emerald (built-ins, locked on)
  98. v1.7.225
    M9
    MCP submenu — built-ins + user servers + stuck-load fixShipped May 12, 2026

    ## v1.7.225 — MCP submenu rebuild User report: the MCP-Servers submenu stayed stuck on "Loading MCP servers…", and the user wanted built-in tools shown alongside their own servers with a proper empty-state message. Two changes: 1. **Two sources, one Promise.all.** Lazy-load wave now fans `/api/mcp/servers` + `/api/mcp/builtin-tools` together, tracks a `serversLoaded` flag (not an empty-array heuristic that refetched forever when a workspace had zero servers). Errors surface in the panel; loading clears in `.finally` regardless. 2. **Two sections.** Submenu now renders: - **Built-in tools · {count}** — categories with green check + always-on toggle (locked at "on"; per-workspace disable lands in a later wave). Tooltip explains. - **Your servers · {count}** — real toggles via PATCH `/api/mcp/servers/:id`. Empty state: "You haven't added an MCP server yet." (clearly separate from built-ins). Footer "Manage MCP Servers" link → `/settings/mcp` unchanged. New bilingual keys: `chat.attach_mcp_builtin_section`, `attach_mcp_builtin_always_on`, `attach_mcp_user_section`, `attach_mcp_user_empty`.

    • MCP submenu stuck on "Loading MCP servers…" — now uses Promise.all + serversLoaded flag with .finally cleanup so loading state always clears
    • 4 new bilingual chat.attach_mcp_* keys (en + he) for built-in / user sections + always-on tooltip
    • MCP submenu now shows built-in tools grouped by category (always-on, 50 entries) + user servers with real toggles + empty state "You haven't added an MCP server yet"
  99. v1.7.224
    M9
    Attach menu — RTL chevron flip + hover-open submenusShipped May 12, 2026

    ## v1.7.224 — Attach menu fixes Two small fixes per user feedback: 1. **Chevron direction in RTL.** ChevronRight pointed right in Hebrew too — the wrong direction for "go deeper" in an RTL layout. Added `rtl:-scale-x-100` to both row chevrons so the icon flips automatically under `<html dir="rtl">`. Logical class — no per-locale branching. 2. **Hover-open submenus.** Sub-menus required a click to open. Now `onMouseEnter` / `onFocus` opens the right panel; click still works as the accessible fallback for keyboard + touch. "Add image(s)" clears the submenu on hover so the user can move between rows without sticky panels.

    • Submenus (Add text file / MCP Servers) open on hover (onMouseEnter + onFocus); click still works as accessible fallback
    • Attach menu chevrons flip under RTL via rtl:-scale-x-100 — Hebrew users now see the arrow pointing inline-end instead of always right
  100. v1.7.223
    M9
    Composer attach menu — submenus for text file + MCP serversShipped May 12, 2026

    ## v1.7.223 — Attach submenus Matches the reference screenshots end-to-end. The two rows with a chevron now open side panels: **Add text file →** - Upload from device — same file picker as before. - Fetch from URL — prompt for an https URL; the URL appends to the composer as `[url: …]`, the chat-side LLM has `scrape/*` + `http/*` tools to fetch + summarise. **MCP Servers →** - Workspace MCP servers list with per-row toggle (mirrors `/settings/mcp`). Optimistic UI; PATCH /api/mcp/servers/:id reverts on error. - Scrollable (`max-h-80 overflow-y-auto`) for tenants with many servers. - "Manage MCP Servers" footer link routes to `/settings/mcp`. - Empty / loading / error states bilingual. Submenu state lives in component memory — click the same parent row again or hit Esc to close. Outside click closes the entire menu. RTL-friendly via `start-full` positioning. New bilingual keys (en + he): `chat.attach_upload_from_device`, `attach_fetch_from_url`, `attach_fetch_url_prompt`, `attach_manage_mcp_servers`, `attach_mcp_loading`, `attach_mcp_empty`.

    • Add text file submenu: Upload from device + Fetch from URL (URL appends as [url: …] for the LLM scrape/http tools)
    • 6 new bilingual chat.attach_* keys (en + he); submenu closes on parent re-click / Esc / outside click; RTL via start-full
    • MCP Servers submenu: toggle workspace servers inline (optimistic, PATCH /api/mcp/servers/:id) + Manage MCP Servers footer link
  101. v1.7.222
    M9
    Composer + button → Add image(s) / Add text file / MCP Servers menuShipped May 12, 2026

    ## v1.7.222 — Composer attach menu Matches the reference product. The bare `+` button on the composer now opens a three-row popover menu: 1. **Add image(s)** — opens a multi-image file picker (`accept="image/*"`). Selected names append as `[image: name]` tokens to the composer so the LLM at least sees what was attached. Real upload + vision binding lands in a follow-up. 2. **Add text file** — single-file picker accepting common text formats (.txt, .md, .json, .csv, .tsv, .yaml, .log, plus every common source-file extension). The file's contents are read client-side and appended to the composer as `[file: name.ext]\n<content>`. 3. **MCP Servers** — links straight to /settings/mcp (closes the menu on click). Menu closes on outside click + Esc. aria-haspopup / aria-expanded wired for screen readers. Chevron-right on the two rows that have more depth ("Add text file" → file type pick, "MCP Servers" → nav). Bonus: the security blurb on /settings/api-keys ("Keys are encrypted server-side …") is now bilingual via `api_keys_page.encrypted_blurb`.

    • Add image(s) → multi-image picker, names append as [image: name] tokens; Add text file → reads contents client-side and appends as [file: name]<content>; MCP Servers → links to /settings/mcp
    • Bilingual chat.attach_image / attach_text_file / attach_mcp_servers + api_keys_page.encrypted_blurb keys added to en + he
    • Composer + button now opens a three-row attach menu: Add image(s), Add text file, MCP Servers
  102. v1.7.221
    M9
    Optional LM Studio API token in /settings/api-keysShipped May 12, 2026

    ## v1.7.221 — Optional LM Studio API token LM Studio normally serves on localhost with no auth, but power users launch `lms server start --api-key <token>` to expose the port securely. Up to v1.7.220 the api-keys form only accepted host + model id; the token went in as a literal `lm-studio-unused` placeholder so any token-protected server returned 401. Added a new optional `lmstudio_token` row in /settings/api-keys. When set, the factory forwards it as `Authorization: Bearer …` to the OpenAI-compatible adapter; when blank, the placeholder stays for backwards compatibility. Touched layers: * `packages/llm/src/factory.ts` — `ApiKeys.lmstudio_token` field; resolves token via env `LMSTUDIO_TOKEN` fallback. * `apps/chat/lib/api-keys-server.ts` — decrypts + forwards. * `apps/chat/app/api/user/api-keys/route.ts` — adds `lmstudio_token` to the provider allowlist + masked-state map. * `apps/chat/app/settings/api-keys/api-keys-form.tsx` — new PROVIDERS row with placeholder `sk-…` and clear "optional" blurb. State maps extended for drafts / show / tests / state.

    • New optional lmstudio_token field in /settings/api-keys — Bearer-forwarded by the factory when set, placeholder fallback when blank
    • ApiKeys interface + loadUserApiKeys + /api/user/api-keys allowlist all gain lmstudio_token; LMSTUDIO_TOKEN env fallback
  103. v1.7.220
    M9
    Sidebar chat badges legible — token + message countsShipped May 12, 2026

    ## v1.7.220 — Legible sidebar badges Token-total pill ("11.5k") + message-count badge ("5") in the sidebar chat row were `text-zinc-500` / `text-zinc-600` on the dark-zinc row background — gray on gray, unreadable in the screenshot the user shared. Flipped both to `text-zinc-100` with `opacity-80` (1.0 on hover) so the numbers are clearly visible while still secondary to the chat title. apps/chat/components/chat/sidebar.tsx, two badges only.

    • Sidebar chat row: token-total + message-count badges flipped from zinc-500/600 to zinc-100 opacity-80 for legibility
  104. v1.7.219
    M9
    Built-in tools panel — collapsible at the header levelShipped May 12, 2026

    ## v1.7.219 — Panel-level collapse Adds a panel-level expand/collapse toggle on the "Built-in tools" header so users can hide the entire surface when they don't need it. Independent of: - the per-category collapse from v1.7.218 - the inner scroll - the search input + filtering When collapsed the header still shows the tool count and a one- line hint inviting expansion. Click the chevron (or anywhere on the header) to flip back to the full panel. State stays in component memory only — refreshing the page resets to expanded so new users see the surface by default. Bilingual `mcp_page.builtin_collapsed_hint` added to en + he. aria-expanded + aria-controls wire the header button to the panel body for screen-reader users.

    • Bilingual mcp_page.builtin_collapsed_hint added to en + he; aria-expanded + aria-controls wired on header button
    • Panel-level collapse on the Built-in tools header — independent of per-category toggles, inner scroll, and search
  105. v1.7.218
    M9
    Built-in tools panel — search + collapsible + lazy + i18nShipped May 12, 2026

    ## v1.7.218 — Built-in tools panel polish The 210-tool catalog made the v1.7.201 panel painful (cold render of every row + no way to find anything). Five fixes: 1. **Search input** above the list. Filters across slug + localized description + category. Empty query falls back to the default layout; a non-empty query auto-expands every matching category. 2. **Collapsible categories.** First category in the list opens by default; the rest start collapsed. ChevronRight / ChevronDown indicates state. Click anywhere on the header row toggles. 3. **Inner scroll.** Panel capped at `max-h-[60vh]` with `overflow-y-auto`, so the surrounding /settings/mcp page doesn't grow when the catalog hits 200+ entries. 4. **Lazy rendering.** Tool rows mount only when the category is expanded. With 50 categories live, the panel paints the same regardless of total tool count. 5. **i18n coverage.** New `mcp_category_names` block covers every live category (50 entries en + he). The category header now shows the localized label with the raw slug as a side pill. Plus `builtin_search_placeholder` / `builtin_search_aria` / `builtin_search_empty` / `builtin_tool_count` keys.

    • Inner scroll: max-h-[60vh] + overflow-y-auto so the page stops growing past 200 tools
    • 4 new bilingual mcp_page.builtin_search_* + builtin_tool_count keys
    • New mcp_category_names i18n block (50 entries en + he) — header shows localized label with raw slug pill
    • Lazy rendering: tool rows mount only when their category is expanded
    • Categories collapsed by default with first one open; ChevronRight/Down toggles
    • Built-in tools panel gets a search input across slug + localized description + category
  106. v1.7.217
    M9
    210/210 — master-plan tool target hit (flow + notify + scrape + markdown, +15)Shipped May 12, 2026

    ## v1.7.217 — 210/210 tools live. Master-plan target met. 🎯 Final batch in the tool-expansion roadmap. Wave 10 closes with flow + notify; wave 11 starts with scrape + markdown. Cumulative moves from 195 → 210, hitting the original master-plan number to the digit. **Migration 0034** — `public.flows` + `public.flow_steps` with status enums (running / paused / completed / failed / cancelled for flows, pending / running / completed / failed / skipped for steps). 8 RLS policies cover read + write per table, scoped via `private.is_workspace_member`. `flows.updated_at` trigger. **flow.ts (5)** — `flow/start` (insert new flow row), `flow/step` (append with auto-increment position, optional parent_step_id), `flow/branch` (fan-out N children under one parent with per-key labels), `flow/merge` (only marks parent completed if every child is terminal), `flow/cancel` (skip every still-open step + flip flow to cancelled). **notify.ts (4)** — `notify/slack`, `notify/email`, `notify/webhook` (POST to any allowlisted URL with optional `x-angi-signature` header), `notify/chat_message` (persist a system / assistant row into one of the caller's chats). Reuses the env tokens already wired by gmail/* and slack/*. **scrape.ts (3)** — `scrape/page` (title + description + body), `scrape/links` (every `<a href>` resolved to absolute URL, deduped + cap 200), `scrape/text` (plain-text body cap 50k). Pure regex; rides the http guard. **markdown.ts (3)** — `markdown/to_plain` (strip syntax keeping prose), `markdown/headings` ({ level, text } in document order), `markdown/links` (`[label](url)` only — images excluded). createDefaultRegistry() picks all 15 up. 15 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 210 (was 195). Master-plan target hit exactly.** Waves 11b–13 (image / audio / video / scrape-deep / shopify / hubspot / intercom / quickbooks) stay queued as optional post-target expansion.

    • Migration 0034 — public.flows + public.flow_steps with status enums + 8 RLS policies + updated_at trigger
    • 15 new bilingual mcp_tool_descriptions entries; cumulative 195 -> 210 — MASTER-PLAN 210+ TOOL TARGET HIT
    • New markdown/* tools (3): to_plain, headings (level+text in order), links (image-excluded)
    • New scrape/* tools (3): page (title+desc+text), links (absolute, dedupe, cap 200), text (plain body cap 50k)
    • New notify/* tools (4): slack, email (Gmail), webhook (signed), chat_message (persist into one of the caller chats)
    • New flow/* tools (5): start, step (auto-position), branch (fan-out), merge (terminal-children gate), cancel (skip-open + flip)
  107. v1.7.216
    M9
    Tool wave 9b (db introspection) + wave 10 plan starter (+9), 195/210+ liveShipped May 12, 2026

    ## v1.7.216 — Wave 9b + plan starter, 195/210+ live Two short waves in one batch. Wave 9b closes the observability + ops set (db introspection); wave 10's plan trio lands ahead of flow/notify which still need a flows table + a generic dispatcher. **Migration 0033** — three SECURITY DEFINER RPCs that expose `information_schema` for the `public` schema: * `app_db_tables_list()` — every table + view with column count. * `app_db_describe(_table)` — column metadata for one table. * `app_db_relationships(_table)` — foreign-key edges. Granted to `authenticated`; no per-tenant rows touched. **db.ts (5)** — `db/tables_list`, `db/describe`, `db/relationships` (via the RPCs), plus `db/count` and `db/recent` straight through the user-RLS supabase client. count + recent respect RLS so workspace-scoped tables only ever return the caller's slice. **plan.ts (4)** — `plan/generate` (goal → steps[]), `plan/decompose` (task → subtasks[]), `plan/evaluate` (plan vs goal → { score 0–100, verdict, risks[], suggestions[] }), `plan/prioritise` (reorder items by importance + dependency, preserves strings verbatim). All LLM-assisted via ctx.generate. createDefaultRegistry() picks all 9 up. 9 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 195 (was 186). 93% of master-plan 210 target. 15 tools left.**

    • New db/* tools (5): tables_list, describe, relationships (via RPCs), count, recent (RLS-scoped to caller's workspace slice)
    • 9 new bilingual mcp_tool_descriptions entries; wave 9b closed; wave 10 plan trio live; cumulative 186 -> 195 (93% of 210)
    • New plan/* tools (4): generate, decompose, evaluate (score/verdict/risks/suggestions), prioritise (preserves strings verbatim)
    • Migration 0033 — SECURITY DEFINER RPCs app_db_tables_list / app_db_describe / app_db_relationships expose information_schema for public schema
  108. v1.7.215
    M9
    Tool wave 9a — Sentry + Vercel + Stripe (+12), 186/210+ liveShipped May 12, 2026

    ## v1.7.215 — Wave 9a (observability + ops), 186/210+ live Three operator-facing integrations. All read-only / safe-mutation (sentry/resolve + sentry/assign are the only writes; both target issues, not money or data). DEFAULT_ALLOWLIST extended with sentry.io / api.vercel.com / api.stripe.com. **sentry.ts (4)** — `sentry/issues_list`, `sentry/events_list`, `sentry/resolve` (PUT status=resolved), `sentry/assign` (PUT assignedTo). Auth: `SENTRY_TOKEN` (falls back to `SENTRY_AUTH_TOKEN` for the same env the source-map upload already uses). **vercel.ts (4)** — `vercel/deployments_list`, `vercel/deployment_logs` (build + runtime events for one deployment), `vercel/domains_list`, `vercel/env_list` (keys only; the upstream endpoint masks secret values). Auth: `VERCEL_TOKEN`; every tool accepts optional `team_id`. **stripe.ts (4)** — `stripe/customer_get`, `stripe/charge_get`, `stripe/subscription_get`, `stripe/invoice_get`. Read-only — all mutation paths stay inside `/api/billing/*` so quota + audit remain authoritative. Reuses `STRIPE_SECRET_KEY` from `@angi/billing`. createDefaultRegistry() picks all 12 up. 12 new bilingual `mcp_tool_descriptions` entries (en + he). Wave 9 split: 9a (sentry/vercel/stripe) live; 9b (supabase + db introspection) queued — needs SECURITY DEFINER RPCs for safe cross-table reads. Roadmap cumulative 174 -> 186. **Live tool count: 186 (was 174). 89% of master-plan 210 target.**

    • New vercel/* tools (4): deployments_list, deployment_logs, domains_list, env_list (keys only, no values)
    • 12 new bilingual mcp_tool_descriptions entries; wave 9 split — 9a live (+12 = 186), 9b queued (supabase + db)
    • DEFAULT_ALLOWLIST extended with sentry.io / api.vercel.com / api.stripe.com
    • New stripe/* tools (4): customer_get, charge_get, subscription_get, invoice_get — read-only, reuses STRIPE_SECRET_KEY
    • New sentry/* tools (4): issues_list, events_list, resolve (PUT status), assign (PUT assignedTo)
  109. v1.7.214
    M9
    Tool wave 8 — LLM-assisted text ops (+20), 174/210+ liveShipped May 12, 2026

    ## v1.7.214 — Wave 8 ships, 174/210+ live LLM-assisted text ops. The chat route binds a `generate({ system, prompt, maxTokens })` closure onto `ToolHandlerCtx`; every wave-8 tool routes through it so the caller's reachable model + decrypted keys stay in one place. mcp-server stays free of AI SDK deps. * **summarize (4)** — text, conversation (chat-style transcript → recap + key_points), document (headline + executive summary + bullets), bullet_list (compress). * **translate (3)** — text, document (chunks on paragraph boundaries when input > 6 k chars), detect_language. * **classify (4)** — text (closed-label), intent (default product intents + override), sentiment (label + -1..1 score), toxicity (toxic flag + severity + categories[]). * **extract (5)** — entities, dates, places, keywords, key_phrases. All return arrays. * **rewrite (4)** — tone, concise (word budget), formal, expand. Output shape is stable: single-line `<value> | <confidence>` for classifiers, `" | "`-separated lists for extractors, free prose for summarize / rewrite / translate. The LLM is instructed to skip preamble + markdown so callers can splice directly. createDefaultRegistry() picks all 20 up. 20 new bilingual `mcp_tool_descriptions` entries (en + he). `/api/chat` builds the `generate` closure once per turn using `createModel(modelId, apiKeys)`; CLI / cron contexts get `llm_not_available` from `runLlm()` until they bind one themselves. **Live tool count: 174 (was 154). 83% of master-plan 210 target.**

    • New translate/* tools (3): text, document (paragraph-chunked when >6k chars), detect_language
    • 20 new bilingual mcp_tool_descriptions entries; wave 8 closed; cumulative 154 -> 174 (83% of 210)
    • ToolHandlerCtx gains optional generate() closure; runLlm() helper in tools/_llm.ts; mcp-server stays free of AI SDK deps
    • New rewrite/* tools (4): tone, concise (target_words), formal, expand
    • New extract/* tools (5): entities, dates, places, keywords, key_phrases — all return string arrays
    • New classify/* tools (4): text (closed labels), intent (default + override), sentiment (-1..1 score), toxicity (severity + categories[])
    • New summarize/* tools (4): text, conversation, document, bullet_list — chat route binds ctx.generate per turn
  110. v1.7.213
    M9
    Tool wave 7a — workspace filesystem over Supabase Storage (+7), 154/210+ liveShipped May 12, 2026

    ## v1.7.213 — Wave 7a (fs), 154/210+ live Workspace-scoped filesystem over Supabase Storage. Lets the chat LLM persist and recall text files between turns without leaving the Angi trust boundary. **Migration 0032** — `workspace_files` Storage bucket (private). Every object path is auto-prefixed with the workspace UUID; RLS on `storage.objects` reads the first segment and gates via `private.is_workspace_member`. Four policies (read / insert / update / delete) so every CRUD path respects tenancy. **fs.ts (7)** — `fs/write` (upsert, 5 MiB cap, content_type configurable), `fs/read` (1 MiB cap, returns truncated head when larger), `fs/list` (200 entries max), `fs/stat` (parent-list + match name, throws when missing), `fs/delete` (batch of up to 100 paths), `fs/mkdir` (drops a `.keep` placeholder since Supabase Storage has no real directories), `fs/move` (copy + delete via storage.move). createDefaultRegistry() picks all 7 up. 7 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 154 (was 147).** Wave 7b (s3 + r2) queued — needs an AWS SigV4 signer.

    • 7 new bilingual mcp_tool_descriptions entries (en + he)
    • Wave 7 split: 7a (fs) live (+7 = 154); 7b (s3 + r2) queued, needs SigV4 signer
    • New fs/* tools (7): write (5 MiB cap), read (1 MiB cap + truncated flag), list (max 200), stat, delete (batch up to 100), mkdir (.keep placeholder), move
    • Migration 0032 — workspace_files Storage bucket with workspace-UUID path prefixing and 4 RLS policies (read/insert/update/delete)
  111. v1.7.212
    M9
    Tool wave 6b (outlook) + 2.1 supplement (+18), 147/210+ liveShipped May 12, 2026

    ## v1.7.212 — Wave 6b + 2.1 supplement, 147/210+ live Closes wave 6 (Outlook on Microsoft Graph) and lands a 2.1 supplement of pure helpers that didn't fit any earlier wave. **outlook.ts (3)** — `outlook/send`, `outlook/list`, `outlook/search`. Microsoft Graph wrappers at `graph.microsoft.com/v1.0/me/*`. Auth: `OUTLOOK_ACCESS_TOKEN` env. DEFAULT_ALLOWLIST gains `graph.microsoft.com`. **Pure helper supplement (15 new tools)** — bonus utilities the LLM was spending tokens on: * `array` (5) — `array/unique`, `array/group_by` (dot-path keying), `array/chunk`, `array/flatten`, `array/zip`. * `string` (5) — `string/pad_start`, `string/pad_end`, `string/repeat`, `string/capitalize`, `string/title_case`. * `color` (3) — `color/hex_to_rgb`, `color/rgb_to_hex`, `color/contrast` (WCAG 2.x ratio + AA/AAA flags). * `ulid` (1) — `ulid/generate` (Crockford-Base32, sortable). * `jwt` (1) — `jwt/decode` (no signature verify — inspection only). createDefaultRegistry() picks all 18 up. 18 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 147 (was 129). 70% of the master-plan 210 target.**

    • DEFAULT_ALLOWLIST extended with graph.microsoft.com; wave 6 closed; cumulative 129 -> 147 (70% of 210)
    • New color/* tools (3): hex_to_rgb, rgb_to_hex, contrast (WCAG 2.x ratio + AA/AAA flags)
    • New ulid/generate + jwt/decode (Crockford-Base32 sortable id, signature-less JWT inspection)
    • New string/* tools (5): pad_start, pad_end, repeat, capitalize, title_case
    • New array/* tools (5): unique, group_by, chunk, flatten, zip
    • New outlook/* tools (3): send, list, search — MS Graph at graph.microsoft.com; OUTLOOK_ACCESS_TOKEN env
  112. v1.7.211
    M9
    Tool wave 6a — Gmail + GCal + GDrive (+15), 129/210+ liveShipped May 12, 2026

    ## v1.7.211 — Wave 6a (Google trio), 129/210+ live Three Google integrations land together. All share a single `GOOGLE_ACCESS_TOKEN` env (OAuth2 access token covering Gmail + Calendar + Drive scopes). Workspace-scoped OAuth flow lands later. DEFAULT_ALLOWLIST extended with `gmail.googleapis.com` + `www.googleapis.com`. **gmail.ts (6)** — `gmail/send`, `gmail/list`, `gmail/search`, `gmail/get`, `gmail/label_add`, `gmail/draft_create`. send + draft_create build the raw RFC 2822 + base64url payload Gmail's `messages.send` and `drafts` endpoints expect. **gcal.ts (5)** — `gcal/list`, `gcal/create`, `gcal/update`, `gcal/delete`, `gcal/freebusy`. PATCH + DELETE go through an `X-HTTP-Method-Override` POST because fetchWithGuard only exposes GET/HEAD/POST/PUT. **gdrive.ts (4)** — `gdrive/list`, `gdrive/get`, `gdrive/search` (wraps gdrive/list with a built `fullText contains "..." and trashed = false` query), `gdrive/share` (add permission with role + type). createDefaultRegistry() picks all 15 up. 15 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 129 (was 114).** Wave 6b (outlook on MS Graph) queued.

    • 15 new bilingual mcp_tool_descriptions entries; wave 6 split — 6a live (+15 = 129), 6b queued (outlook)
    • DEFAULT_ALLOWLIST extended with gmail.googleapis.com + www.googleapis.com; single GOOGLE_ACCESS_TOKEN env covers all three
    • New gdrive/* tools (4): list, get, search (wraps list with fullText contains query), share
    • New gcal/* tools (5): list, create, update (PATCH via X-HTTP-Method-Override), delete, freebusy
    • New gmail/* tools (6): send, list, search, get, label_add, draft_create — RFC 2822 + base64url encoded payloads
  113. v1.7.210
    M9
    Tool wave 5b — GitLab + Linear + Slack (+16), 114/210+ liveShipped May 12, 2026

    ## v1.7.210 — Wave 5b, 114/210+ live Three productivity integrations land in one batch. Wave 5 closes along with the master plan's biggest external-API wave. All 16 tools ride the same `fetchWithGuard` from `http/*` — hostname allowlist + 5s timeout + 1 MiB cap. DEFAULT_ALLOWLIST extended with `gitlab.com`, `api.linear.app`, `slack.com`. **gitlab.ts (5)** — `gitlab/project`, `gitlab/issue`, `gitlab/mr`, `gitlab/pipeline`, `gitlab/branch`. REST v4 wrappers around `/projects/:id`. Project id is URL-encoded so namespace paths (`group/sub/repo`) work alongside numeric ids. Auth: `GITLAB_TOKEN` env (PAT). **linear.ts (5)** — `linear/issue`, `linear/project`, `linear/cycle`, `linear/comment`, `linear/team`. POST GraphQL to `api.linear.app/graphql` with canned queries per tool (LLMs are bad at writing valid Linear GraphQL on their own). Auth: `LINEAR_TOKEN` env (header value). **slack.ts (6)** — `slack/send` (chat.postMessage, text + Block Kit), `slack/search` (search.messages), `slack/react` (reactions.add), `slack/channel_list` (conversations.list), `slack/thread_get` (conversations.replies), `slack/channel_history` (conversations.history). Auth: `SLACK_TOKEN` env (bot or user token). createDefaultRegistry() picks all 16 up. 16 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 114 (was 98). Wave 5 closed.** Wave 6 (gmail / gcal / gdrive / outlook) next.

    • New linear/* tools (5): issue, project, cycle, comment, team — canned GraphQL POSTs against api.linear.app
    • 16 new bilingual mcp_tool_descriptions entries; wave 5 closed; cumulative 98 -> 114
    • DEFAULT_ALLOWLIST extended with gitlab.com / api.linear.app / slack.com
    • New slack/* tools (6): send (text+blocks), search, react, channel_list, thread_get, channel_history — Web API JSON POSTs
    • New gitlab/* tools (5): project, issue, mr, pipeline, branch — REST v4 wrappers with URL-encoded project paths
  114. v1.7.209
    M9
    Tool wave 5a — GitHub REST wrappers (+8), 98/210+ liveShipped May 12, 2026

    ## v1.7.209 — Wave 5a (GitHub), 98/210+ live Thin wrappers around api.github.com (REST v3). Every call rides the same hostname-allowlist + size-cap guard as `http/*`. The host is already on DEFAULT_ALLOWLIST so this works out of the box. **gh.ts (8)** — `gh/repo`, `gh/issue`, `gh/pr`, `gh/branch`, `gh/commit`, `gh/release`, `gh/actions_run`, `gh/actions_logs`. All read-only; pagination via `per_page` (default 30, max 100, runs default 20). Non-2xx responses surface as `gh:<status>:<msg>` errors with the upstream message text preserved. Auth: optional `GITHUB_TOKEN` env. With it we get the 5000 req/hr authenticated quota and access to private repos the token can see; without it we fall back to the 60 req/hr anonymous limit. Workspace-scoped GH-key storage lands in a later wave once the api-keys form gains a github row. `createDefaultRegistry()` picks all 8 up. 8 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 98 (was 90).** Wave 5b (gitlab + linear + slack) next.

    • Auth via optional GITHUB_TOKEN env (5000 req/hr authenticated); anonymous fallback (60 req/hr) when unset. Workspace-scoped key UI in a later wave.
    • New gh/* tools (8): repo, issue, pr, branch, commit, release, actions_run, actions_logs — thin wrappers over api.github.com via fetchWithGuard
    • 8 new bilingual mcp_tool_descriptions entries (en + he)
    • Wave 5 split: 5a (gh) live (+8 = 98); 5b (gitlab + linear + slack) queued
  115. v1.7.208
    M9
    Tool wave 4b — RSS / Atom feeds (+3), 90/210+ liveShipped May 12, 2026

    ## v1.7.208 — Wave 4b (feed), 90/210+ live On-demand RSS / Atom reader + workspace subscription manager. Search tools (the other half of wave 4b) stay queued — they need workspace-level provider-key UI which lands later. **Migration 0031** — `feed_subscriptions (id, workspace_id, feed_url, label, created_by, created_at, last_polled_at)` with unique (workspace_id, feed_url) + members-read-and-write RLS. **feed.ts (3)**: * `feed/rss_fetch` — pulls a URL through the http guard (allowlist, 5s timeout, 1 MiB cap) and parses RSS or Atom XML with a regex-based extractor. Returns up to 50 items: `{ title, link, published, summary }` plus `kind` (rss / atom / unknown) and `feed_title`. * `feed/rss_subscribe` — idempotent upsert on (workspace_id, feed_url). Records label + last_polled_at slot for the future edge-function poller. * `feed/rss_list_subscriptions` — workspace's saved feeds, newest-first. `createDefaultRegistry()` picks all three up. 3 new bilingual `mcp_tool_descriptions` entries (en + he). **Live tool count: 90 (was 87).** Wave 5 (gh / gitlab / linear / slack) next.

    • Migration 0031 — feed_subscriptions table with unique (workspace_id, feed_url) and members-read-and-write RLS
    • New feed/* tools (3): rss_fetch (regex RSS+Atom parser, 50 items cap), rss_subscribe (idempotent), rss_list_subscriptions
    • Wave 4b split: feed live (+3 = 90); search (Brave/Tavily/Serper) deferred to a later batch after the workspace-provider-key UI lands
  116. v1.7.207
    M9
    Tool wave 4a — outbound HTTP + unfurl (+7), 87/210+ liveShipped May 12, 2026

    ## v1.7.207 — Wave 4a ships, 87/210+ live First outbound-HTTP wave. Strict allowlist + size/time caps so the chat LLM can pull public data without becoming an SSRF cannon. **http.ts (5)** — `http/fetch`, `http/head`, `http/get_json`, `http/post_json`, `http/put_json`. Every call passes through a single `fetchWithGuard()` helper: - hostname allowlist = DEFAULT_ALLOWLIST ∪ env `HTTP_ALLOWLIST` (comma-sep). Default covers api.github.com, raw.githubusercontent.com, en/he.wikipedia.org, hacker-news, httpbin.org, jsonplaceholder, api.openai.com, api.anthropic.com, generativelanguage.googleapis.com. Set `HTTP_ALLOWLIST=*` to disable (don't, in multi-tenant deploys). - protocol restricted to http/https. - private-range hosts blocked (127.*, 10.*, 192.168.*, 169.254.*, 172.16-31.*, ::1, fc.*, fd.*, .internal, .local). - 5s wall-clock cap via AbortController. - 1 MiB response cap with `body: { truncated: true }` flag. **unfurl.ts (2)** — `unfurl/url`, `unfurl/og_tags`. Reuses the http guard; regex-extracts `<title>`, `<meta name=description>`, every `og:*`, every `twitter:*`. Decodes the common HTML entities. No DOM parser — that's a dedicated scrape wave later. `createDefaultRegistry()` picks all 7 up. 7 new bilingual `mcp_tool_descriptions` entries (en + he). Roadmap split: wave 4a shipped, 4b (search + RSS, needs provider keys + subscriptions table) queued. **Live tool count: 87 (was 80).**

    • New http/* tools (5): fetch, head, get_json, post_json, put_json — hostname allowlist + private-range block + 5s timeout + 1 MiB body cap
    • 7 new bilingual mcp_tool_descriptions entries; wave 4 split — 4a live (+7 = 87), 4b queued (search + RSS)
    • DEFAULT_ALLOWLIST covers github / wikipedia / hacker-news / httpbin / jsonplaceholder / openai / anthropic / google generative; HTTP_ALLOWLIST env extends or replaces
    • New unfurl/* tools (2): url, og_tags — regex-extracts title + description + og:* + twitter:* from HTML head, reuses http guard
  117. v1.7.206
    M9
    Tool wave 3b — documents + tagging (+11), 80/210+ liveShipped May 12, 2026

    ## v1.7.206 — Wave 3b ships, 80/210+ live Workspace-scoped persistent documents + generic tagging. Closes wave 3 of the master plan's tool-expansion roadmap. **Migration 0030**: * `documents (id, workspace_id, title 1-200, body, created_by, created_at, updated_at)` with `updated_at` trigger. * `tags (id, workspace_id, name 1-64, unique(workspace_id, name))` * `tag_targets (tag_id, target_type ∈ {document, chat, memory, swarm, task}, target_id, primary key all three)`. RLS members-read-and-write throughout; tag_targets inherits tenancy via the tags row. **doc.ts (6)** — `doc/store`, `doc/get`, `doc/list` (newest-first, body capped 2k chars), `doc/search` (ILIKE on title + body, metacharacter-escaped), `doc/delete`, `doc/tag` (idempotent multi- tag apply that creates missing tag rows). **tag.ts (5)** — `tag/list` (with usage counts, sorted), `tag/apply` (to any target type, idempotent), `tag/remove`, `tag/tagged_by` (targets carrying a name, optional type filter), `tag/untag_all` (strip every tag from one target). Search uses plain ILIKE for now; pgvector / pg_trgm arrive in a later wave once the haystack volume justifies the index work. `createDefaultRegistry()` picks all 11 up. 11 new bilingual `mcp_tool_descriptions` entries added (en + he). **Live tool count: 80 (was 69).** Wave 3 closed; wave 4 (outbound HTTP) next.

    • New doc/* tools (6): store, get, list, search (ILIKE on title+body, escaped), delete, tag (idempotent multi-tag)
    • 11 new bilingual mcp_tool_descriptions entries; roadmap 69 -> 80
    • New tag/* tools (5): list (with counts), apply (any target type, idempotent), remove, tagged_by, untag_all
    • Migration 0030 — documents + tags + tag_targets with workspace-scoped RLS and updated_at trigger
  118. v1.7.205
    M9
    Tool wave 3a — +13 workspace accessors (audit / stats / list)Shipped May 12, 2026

    ## v1.7.205 — Wave 3a, 69/210+ tools live Read-only RLS-scoped accessors over data the workspace already owns. The chat-side LLM uses these to answer "what's going on in my workspace" without firing arbitrary SQL. * **audit** (3) — `audit/list` (newest-first), `audit/search` (action / target_type / actor_id / since filters AND-combined), `audit/get` (single entry by id). * **stats** (6) — `stats/workspace` (headline counts: chats, swarms, agents active, tasks in_progress + completed, memories, members), `stats/agents` (per-status), `stats/usage` (sliding token window, default 30 days), `stats/swarms` (per-status), `stats/chats` (top 20 models by volume), `stats/memories` (top 20 namespaces by size). * **list** (4) — `list/workspaces` (caller's memberships + role + plan), `list/members` (via the v1.7.013 list_workspace_members SECURITY DEFINER RPC), `list/plans` (public catalog with caps + price), `list/peers` (federation trust ring from migration 0029). All seven new tools run under user-RLS — no service-role anywhere in the chat tool surface. `createDefaultRegistry()` picks them up; 24 new bilingual `mcp_tool_descriptions` entries… actually 13 here plus 24 from wave 2 still carry over. Per-slug `en` + `he` coverage remains 100%. **Wave 3 split**: doc + tag tools (which need new tables) deferred to **wave 3b**. Roadmap updated to reflect the split — wave 3a ships now, 3b queued for the next batch after a `documents` + `tags` migration. **Live tool count: 69 (was 56).**

    • New stats/* tools (6): workspace, agents, usage (sliding 30d), swarms, chats, memories
    • 13 new bilingual mcp_tool_descriptions entries; roadmap cumulative count 56 -> 69
    • Wave 3 split: 3a ships now (+13); 3b (doc + tag) queued after a documents + tags migration
    • New list/* tools (4): workspaces (memberships), members (via SECURITY DEFINER RPC), plans, peers
    • New audit/* tools (3): list (newest-first), search (filters AND-combined), get by id
  119. v1.7.204
    M9
    Tool wave 2 — +24 pure utilities across crypto/url/regex/math/diffShipped May 12, 2026

    ## v1.7.204 — Wave 2 ships, 56/210+ tools live Second wave of the master plan's tool-expansion roadmap. All five modules are deterministic + pure-function: no DB writes, no LLM calls, no outbound HTTP. Cheap to bind on every chat turn. * **crypto** (7) — `uuid`, `hash` (sha1/sha256/sha384/sha512/md5), `random_bytes`, `base64_encode`, `base64_decode`, `hex_encode`, `hex_decode`. All Node built-ins; no extra deps. * **url** (5) — `parse`, `build`, `query_get`, `query_set`, `path_join` (safe — no `//` accidents). * **regex** (4) — `match`, `find_all` (always-`g`, cap 1000 matches), `replace`, `split`. Pattern + haystack capped (1k chars / 100k chars) so a runaway expression can't chew a Node worker. * **math** (5) — `sum`, `mean`, `median`, `stdev` (sample / population), `clamp` (tolerates swapped range). No `eval`-style expression evaluator — LLMs quote arithmetic badly; safer to expose named operations. * **diff** (3) — `lines`, `text` (word-level), `json` (structural, returns added/removed/changed key paths). LCS-based; 100k-char cap. `createDefaultRegistry()` picks them all up. Bilingual `mcp_tool_descriptions` entries added for every slug (en + he). `docs/tool-roadmap.md` updated: wave 2 marked shipped, cumulative count = 56. **Live tool count: 56 (was 32).**

    • New url/* tools (5): parse, build, query_get, query_set, path_join
    • 24 new bilingual mcp_tool_descriptions entries; docs/tool-roadmap.md cumulative count -> 56
    • New diff/* tools (3): lines, text (word-level), json (structural)
    • New math/* tools (5): sum, mean, median, stdev (sample/population), clamp
    • New regex/* tools (4): match, find_all (g, cap 1000), replace, split with bounded inputs
    • New crypto/* tools (7): uuid, hash, random_bytes, base64_encode/decode, hex_encode/decode
  120. v1.7.203
    M9
    Tool roadmap published — first wave (+12 text/json/time)Shipped May 12, 2026

    ## v1.7.203 — Wave 1 toward the 210+ tool target The master plan calls for ~210 built-in MCP tools; up to v1.7.202 the registry held 20. This release ships wave 1 + the roadmap doc that paces the remaining waves. **New tool modules** (`packages/mcp-server/src/tools/`): * `text.ts` — `text/word_count`, `text/slugify`, `text/truncate` (word-boundary aware), `text/extract_urls`, `text/extract_emails`. All deduped + order-preserved. * `json.ts` — `json/parse` (never throws — returns `{ ok, value }` or `{ ok, error }`), `json/stringify` (stable-key sort for diffable output), `json/path` (dot-path picker handling `a.b[0].c`). * `time.ts` — `time/now`, `time/parse`, `time/format` (Intl locale + timezone aware, falls back gracefully on bad locale), `time/diff` (signed `b - a` in ms / s / m / h / d). All 12 are pure functions — no DB writes, no LLM calls, no outbound HTTP. Cheap to bind, cheap to call, safe on every chat turn. Registered in `createDefaultRegistry()` so they land in the chat LLM tool-binding payload automatically. Bilingual `mcp_tool_descriptions` entries added for every slug. **Tool roadmap** (`docs/tool-roadmap.md`): Canonical list of what's shipped, what's queued, and what's deferred. Waves 2–12 grouped by ROI — pure-function utilities first, workspace data accessors next, then outbound integrations (http, search, gh, slack, gmail), then LLM-assisted text ops, then observability + ops, then long-tail SaaS connectors. **Wave 10 hits 210** by design, matching the master-plan target. **Live tool count: 32 (was 20).**

    • docs/tool-roadmap.md publishes the full 210+ tool plan grouped by wave + ROI; wave 10 hits 210 by design
    • Bilingual mcp_tool_descriptions entries added for all 12 new slugs (en + he)
    • createDefaultRegistry() now registers text/json/time alongside the baseline swarm/agent/task/memory/chat/workspace bundles
    • New time/* tools (4): now, parse, format (Intl locale + timezone aware), diff (signed b - a in ms/s/m/h/d)
    • New json/* tools (3): parse (never throws), stringify (stable-key sort), path (dot-path picker)
    • New text/* tools (5): word_count, slugify, truncate (word-boundary aware), extract_urls, extract_emails
  121. v1.7.202
    M9
    Built-in tool descriptions translated to HebrewShipped May 12, 2026

    ## v1.7.202 — Bilingual built-in tool catalog The v1.7.201 panel surfaced the 20 built-in MCP tools but rendered the English descriptions from the server-side registry verbatim, even in Hebrew. The new bilingual contract says every user-visible string must land in both locales from the moment it ships — fixed the gap. - New `mcp_tool_descriptions` block in en + he covers every slug (swarm/init, swarm/status, swarm/list, swarm/destroy, swarm/scale, agent/spawn, agent/list, agent/kill, agent/status, task/orchestrate, task/status, task/update, task/list, memory/store, memory/search, memory/list, memory/forget, chat/list, chat/get, workspace/active). Keys use `_` instead of `/` so the dotted-path translator resolves them. - McpServersClient looks up `mcp_tool_descriptions.<slug>` per row and falls back to the English registry description when the key is missing — useful for future tools that ship before their Hebrew copy lands.

    • McpServersClient resolves description via t(mcp_tool_descriptions.<slug>) with English registry fallback for unkeyed tools
    • mcp_tool_descriptions block with 20 entries in en + he covers every built-in tool slug
  122. v1.7.201
    M9
    Built-in MCP tools now visible in /settings/mcpShipped May 12, 2026

    ## v1.7.201 — Surface the always-on tool catalog The chat-side LLM has been bound to ~20 built-in tools since M2 (swarm/init, swarm/status, swarm/destroy, swarm/scale, agent/spawn, agent/list, agent/kill, agent/status, task/orchestrate, task/list, task/status, task/update, memory/store, memory/search, memory/forget, chat/*, workspace/*). They were registered server- side by `createDefaultRegistry()` and exposed to the LLM on every turn, but the user had no way to see them — `/settings/mcp` only listed user-attached external servers. The "where are my tools?" gap is now closed. - New `GET /api/mcp/builtin-tools` returns the live registry list: `{ tools: [{ name, description, category }] }`. Public, no auth (tool names aren't secrets), cached at the edge for 5 minutes. - `McpServersClient` (the `/settings/mcp` page) now fetches both surfaces in parallel and renders a default-expanded `<details>` panel above the external-servers list. Tools group by category (agent / chat / memory / swarm / task / workspace), each row shows the `category/action` name + one-line description. - Bilingual `mcp_page.builtin_title` / `builtin_subtitle` / `builtin_loading` added to en + he. This unblocks the M9+ "scope a chat to a tool subset" deliverable in the master plan — users can now see what they're scoping against.

    • Bilingual mcp_page.builtin_title / builtin_subtitle / builtin_loading added to en + he
    • /settings/mcp now renders a default-expanded built-in tools panel above the external-servers list, grouped by category
    • New GET /api/mcp/builtin-tools — returns live registry list { name, description, category } with 5-min edge cache
  123. v1.7.200
    M9
    Federation foundation — Ed25519 zero-trust peeringShipped May 12, 2026

    ## v1.7.200 — Cross-machine trust ring First post-GA milestone from the master plan's deferred list. Lays the Ed25519-based zero-trust foundation that lets two Angi deployments share swarms, memory, and chats without a shared secret. **Migration 0029** — `public.peers` (workspace-scoped trust list, status pending/active/blocked, unique on `(workspace_id, peer_public_key)`) + `public.peer_shares` (resource × peer pivot, shipped as a schema-only shell ready for the first federated tool). Both tables get RLS: members read, owner/admin write. **`@angi/federation`** — new package, zero runtime deps. - `generateKeypair()` + `loadInstanceKey()` — Ed25519 via `node:crypto`, seed in `FEDERATION_PRIVATE_KEY` env. Public key derived on demand so the operator never has to publish a paired cert. - `signRequest()` — emits the four federation headers (`Federation-Public-Key`, `Federation-Timestamp`, `Federation-Nonce`, `Federation-Signature`) for an outgoing fetch. - `verifyRequest()` — checks header completeness, clock skew (60s window), pinned public key, signature over the canonical string `METHOD\nPATH\nTS\nNONCE\nSHA256(body)`. - `isReplay()` + `rememberNonce()` — bounded in-memory LRU per Node process, scoped to the clock-skew window so replays inside it get rejected without DB writes. **Routes** (under `/api/federation`): - `GET /identity` — public discovery. Returns `{ protocol, public_key, url }`. 5-minute edge cache. 503 when the deployment hasn't enrolled. - `GET|POST|DELETE /peers` — workspace owner/admin CRUD for the trust ring. RLS does tenancy; the route only shapes input. - `POST /inbox` — receives signed messages from peers. Verifies signature, pins to a peers row, anti-replays the nonce, bumps `last_seen_at`, and promotes pending peers to active on first successful verification. The one chat-app surface that uses the service-role admin client — by design, because peers don't have an auth.users row. **Wire format** — `ed25519-http-sig-v1`. Body integrity via SHA-256 inside the canonical string so a man-in-the-middle can't swap payloads. This release ships the handshake shell only. Federated swarm and memory sharing layer on top in later batches — the inbox already returns 202 on unknown `kind`s so a peer running a future protocol version can call us without bouncing.

    • Migration 0029 — public.peers + public.peer_shares with workspace-scoped RLS (members read, owner/admin write)
    • New @angi/federation package: Ed25519 generateKeypair/loadInstanceKey/signRequest/verifyRequest + replay-protection LRU
    • Bilingual federation.* dict block (title, subtitle, statuses, peer fields)
    • POST /api/federation/inbox — signed inbox with header verification, public-key pinning, anti-replay, peer auto-promotion
    • GET/POST/DELETE /api/federation/peers — workspace owner/admin CRUD for the trust ring
    • GET /api/federation/identity — public discovery endpoint (protocol, public key, url) with 5-min edge cache
  124. v1.7.199
    M9
    M9 closed — CSP enforcing, docs site live, chat-UI parity signed offShipped May 12, 2026

    ## v1.7.199 — Three GA-gate items in one batch **CSP enforcement** — `apps/chat/next.config.mjs` flips the header key from `Content-Security-Policy-Report-Only` to enforcing `Content-Security-Policy`. Same directive value as the report-only header that's been running for many weeks with no real-world violations. Set `CSP_REPORT_ONLY=1` in the deploy env to fall back without a code change during an incident; reporting still flows to `/api/csp-report` → Sentry under either mode. **Docs site** — new `docs/` content lands as five top-level pages: - `README.md` — index + project status pointer to `/updates`. - `getting-started.md` — five-minute new-user path (signup → workspace → key → first message → swarm spin-up). - `architecture.md` — monorepo layout, request flow, data model, RLS + SECURITY DEFINER playbook, swarm execution. - `api.md` — every `/api/*` route the chat + CLI rely on plus the MCP tool registry. - `development.md` — local setup, the pipeline, bilingual contract, release hygiene, the migration playbook. **Chat-UI parity sign-off** — `docs/qa/chat-ui-parity.md` walked end-to-end in both locales. The 14 elements (sidebar, composer, model picker, MCP chip, tool-call card, thinking block, swarm strip, agent outputs panel, command palette, …) render as specified. Visual regression now defends the surface automatically via `apps/chat/e2e/visual.spec.ts` with `maxDiffPixelRatio: 0.02`. M9 is closed. The remaining items on the master plan are explicitly deferred post-GA (federation, 210+ MCP tools, plugin marketplace, mobile shells) — not blocking the GA tag.

    • docs/ site shipped — README index + getting-started + architecture + api + development pages
    • docs/qa/chat-ui-parity.md signed off in both locales; visual.spec.ts now the standing automatic gate
    • Closes M9: CSP promoted from Report-Only to enforcing Content-Security-Policy with CSP_REPORT_ONLY env toggle for incident fallback
  125. v1.7.198
    M5
    Autonomous swarm worker — Phase 5 closeShipped May 12, 2026

    ## v1.7.198 — Agents finally do their own work Closes M5 of the master plan. Up to v1.7.197 the chat surface was the swarm's only worker: `task/orchestrate` round-robin assigned subtasks but the LLM in the user's same chat turn had to produce the deliverable inline. Agents were storage rows the model name-checked, not actors. This release ships the missing actor. **Server** (`apps/chat/lib/swarm-tick.ts` + `app/api/swarms/tick/route.ts`): - One invocation pops up to 5 in-flight subtasks for the caller's workspace where `agent_id IS NOT NULL` and `output IS NULL`. - For each task it runs `generateText` against the user's reachable model using a role-specific system prompt — distinct prompts for architect, coder, tester, reviewer, security-architect, security-auditor, researcher, coordinator, memory-specialist, performance-engineer. Generic fallback for unknown types. - Output persists to `tasks.output` as `{ type: 'text', text, agent_type }`. Status flips to `completed`, `completed_at` stamped, tokens recorded. - Agent heartbeat refreshed; status flips back to `idle` if no more in-flight tasks remain for that agent. - Parent tasks auto-complete once every child is terminal. - Token usage rolled up through the v1.7.197 RPCs so admin workspace counters reflect worker spend. **Auto-trigger**: `/api/chat` onFinish now fires `runSwarmTick` fire-and-forget so a `task/orchestrate` call inside a chat turn isn't left with subtasks hanging. The user sees results pop in via the existing realtime channel. **UI** (`SwarmProgress`): - `/api/chats/:id/swarm` GET now returns completed + failed tasks (cap 70) along with their `output` payload. - New `<details>` panel below the agent grid lists the latest 5 agent outputs — title + agent_type pill + the persisted text — so users can read each agent's slice without leaving the chat. - Bilingual `swarm.agent_outputs` ("Agent outputs ({count})" / "פלטי סוכנים ({count})"). End state: orchestrate → tick fires → workers produce per-agent outputs → swarm UI shows them, agents return to idle. The chat-side LLM no longer carries the entire workload.

    • Fire-and-forget tick from /api/chat onFinish so orchestrate inside a chat turn auto-dispatches workers
    • Token usage from the worker rolls up through the v1.7.197 SECURITY DEFINER RPCs so admin counters track worker spend
    • Closes M5: new /api/swarms/tick + lib/swarm-tick.ts run one LLM call per assigned subtask using role-specific prompts (architect, coder, tester, reviewer, researcher, security-*, coordinator, memory, performance)
    • /api/chats/:id/swarm returns completed + failed tasks with output payload; SwarmProgress renders an Agent outputs <details> panel with the last 5 agent texts
    • Outputs persist to tasks.output { type, text, agent_type }; status flips to completed; agents return to idle; parent tasks auto-complete
  126. v1.7.197
    M9
    Workspace usage now records through a security-definer RPCShipped May 12, 2026

    ## v1.7.197 — Usage events actually persist Symptom: `/admin/workspaces/<id>` showed Chat/min and Tokens MTD counters at 0 even after a hundred chat turns. Cause: `usage_events` has no end-user INSERT policy, so up to v1.7.196 both `recordUsage()` and `checkChatRateLimit()` needed a service-role client. The chat route fell back to `createAdminClientOptional()`, which returned `null` whenever `SUPABASE_SERVICE_ROLE_KEY` wasn't in the environment — the entire usage pipe silently skipped. Worse: adding the env var defeats the no-service-role-in-the-chat-app rule from CLAUDE.md. Fix follows the migration-0013 playbook: SECURITY DEFINER RPCs gated on workspace membership, granted to `authenticated` only. * Migration 0028 adds two functions: - `app_record_usage_event(workspace, kind, quantity, metadata)` — generic ingest (tokens_input/_output, agent_seconds, …). - `app_record_chat_request(workspace) returns int` — atomic insert + last-60s count for the rate limiter, one round-trip. * `packages/billing/src/index.ts` — `recordUsage()` and `checkChatRateLimit()` switched from `supabase.from(...).insert(...)` to `supabase.rpc(...)`. Signature names dropped the `Admin` suffix — they now take any session-RLS client. * `apps/chat/app/api/chat/route.ts` — drops `createAdminClientOptional()`, passes the user-RLS supabase client to both helpers in the gate AND inside `onFinish`. Removes the v1.7.186 silent-skip branch. Net effect: every chat turn writes one `chat_requests` event plus one `tokens_input` + one `tokens_output` event, on every deployment including local dev. The admin workspace page now climbs off 0%.

    • /api/chat drops createAdminClientOptional() — service-role key no longer required for usage recording
    • recordUsage + checkChatRateLimit in @angi/billing switched from direct INSERTs to RPC calls; signatures take user-RLS client
    • Migration 0028 — app_record_usage_event + app_record_chat_request, gated on workspace membership, granted only to authenticated
    • Workspace usage stuck at 0% — every chat turn now records chat_requests + tokens_input + tokens_output via SECURITY DEFINER RPCs
  127. v1.7.196
    M9
    Assistant replies survive refresh — concat every text partShipped May 11, 2026

    ## v1.7.196 — Assistant replies survive a refresh `onFinish` in /api/chat persists the assistant's `response.messages` straight as a parts array, which routinely contains `tool_call`, `tool_result`, `reasoning`, and `text` parts in arbitrary order. The previous flatten path on the client (and the v1.7.195 server bootstrap) only read `content[0].text`. When a `text` part wasn't first — every multi-step agent reply — the displayed string was empty, so after a refresh the assistant reply looked deleted. - New `apps/chat/lib/message-content.ts` exports `flattenMessageContent()` — concatenates every `text` part (plus legacy parts where `type` is missing but `text` is a string). Strings pass through verbatim; anything else returns `''`. - `apps/chat/lib/chat-bootstrap.ts` and the client-side `onSelectChat` in `chat-shell.tsx` both import the same helper so the SSR seed and the post-mount fetch produce identical strings. - File is pure (no `next/headers`) so the client bundle no longer pulls a server-only module along with the helper.

    • flattenMessageContent now concatenates every text part — assistant replies no longer vanish after refresh
    • chat-bootstrap.ts + chat-shell.tsx onSelectChat import the shared helper
    • Helper extracted to lib/message-content.ts so server + client share one definition without pulling next/headers into the client bundle
  128. v1.7.195
    M9
    Full SSR prefetch for the chat surface + Supabase preconnectShipped May 11, 2026

    ## v1.7.195 — Cold refresh of an existing chat paints instantly Building on the v1.7.193 SSR bootstrap. The remaining cold-path latency was: refreshing /c?chat=<id> still fired a client-side `/api/chats/:id` GET to pull the transcript. That's one more full browser → Vercel → Supabase roundtrip after JS hydrates. - `loadChatBootstrap({ chatIdParam })` now fans the four reads in a single Promise.all wave: chats list, users mirror, api-keys map, messages for the URL-named chat. `messages` are flattened to the Vercel AI SDK `useChat` shape on the server. - `apps/chat/app/c/page.tsx` reads `searchParams.chat`, hands it to the bootstrap, and passes the result as `<ChatShell initial>`. - `ChatShell` seeds `chatId` from `initial.chatId`, seeds `useChat({ initialMessages })` from `initial.initialMessages`, and marks the URL-hydration ref consumed so the legacy `/api/chats/:id` fetch does not fire. - Root layout adds `<link rel="preconnect">` + `<link rel="dns-prefetch">` to the Supabase origin so the first realtime / auth-refresh request doesn't pay 50–150ms of TCP+TLS handshake on top of the request. End state: cold refresh into `/c?chat=<id>` reaches the painted transcript in one server round-trip, zero client fetches.

    • loadChatBootstrap accepts chatIdParam and fans 4 reads (chats + me + keys + messages) in one Promise.all
    • Root layout pre-warms TCP+TLS to Supabase via preconnect + dns-prefetch
    • ChatShell seeds useChat({ initialMessages }) + chatId from initial; URL-hydration ref starts consumed when initial.chatId is set
    • apps/chat/app/c/page.tsx reads searchParams.chat and forwards to the bootstrap
  129. v1.7.194
    M9
    Swarm tasks start moving the moment they are orchestratedShipped May 11, 2026

    ## v1.7.194 — task/orchestrate auto-assigns + live SwarmProgress Two reinforcing fixes so a freshly-orchestrated swarm visibly moves instead of sitting at "pending". **task/orchestrate (server)** — when a real swarm with at least one non-terminated agent is targeted: - Loads the agent pool (RLS-scoped) and round-robin assigns each subtask to an `agent_id`. - Flips parent + assigned subtasks to `in_progress` with `started_at = NOW()`. - Bumps the assigned agents to `status='busy'` + `last_heartbeat_at = NOW()` so the swarm grid pill flips emerald immediately (instead of looking stale). - Returns `auto_started: boolean` so the caller can react. **SwarmProgress (client)** — consumes a richer `/api/chats/:id/swarm` payload that now also returns in-flight tasks (pending + in_progress, capped at 50). The header gains: - A "Working on …" headline row with a spinner and a "{in_progress} in progress · {pending} pending" counter. - A per-agent badge showing each agent's assigned subtask title. - Realtime channel listens to `tasks` changes on the same swarm — the indicator updates instantly when a row flips status. - `AgentIcon` renders Loader2 (spinning) when the agent is `busy` or has an in-flight task; `agentClass()` paints `busy` with the emerald active palette so users stop seeing the stale-clock badge immediately after orchestration. **Delegation suffix in /api/chat** — instructs the LLM to (a) prefer `task/orchestrate` for decomposition, (b) immediately produce the actual deliverable in the same chat turn, (c) call `task/update { status: 'completed', output }` per subtask so the UI advances and agents flip back to idle. Bilingual: `swarm.working_on`, `swarm.tasks_summary`.

    • Assigned agents flip to status=busy + fresh heartbeat so the swarm grid lights up emerald immediately
    • Bilingual swarm.working_on + swarm.tasks_summary i18n keys
    • Delegation suffix tells the LLM to call task/orchestrate, immediately produce the deliverable, and task/update on completion
    • /api/chats/:id/swarm returns in-flight tasks alongside swarm + agents
    • SwarmProgress: "Working on …" headline + per-agent task badge + in_progress/pending counter, all driven by Realtime
    • task/orchestrate round-robin-assigns subtasks to agents and flips them to in_progress on the spot
  130. v1.7.193
    M9
    SSR bootstrap for the chat surface — zero client fetch waterfallShipped May 11, 2026

    ## v1.7.193 — Chat paints with sidebar + workspace + model preloaded Cold-mounting /c used to fire three sequential client-side fetches before the sidebar could render: `/api/chats`, `/api/workspaces`, `/api/user/me`. Each was a full browser → Vercel → Supabase roundtrip. Visible blank state lasted hundreds of ms even on a warm cache. The new server-side helper `lib/chat-bootstrap.ts` runs the same three reads in parallel, straight against Supabase (RSC, no HTTP hop), and the result rides the React Server Component payload to the client. `<ChatShell initial={...}>` seeds state from the prop and skips its first-mount fetch effect. - New: `apps/chat/lib/chat-bootstrap.ts` returns `{ chats, activeWorkspaceId, defaultModel, configuredProviders }`. - `apps/chat/app/c/page.tsx` awaits `loadChatBootstrap()` and passes the result as `<ChatShell initial>`. - `ChatShell` adds an optional `initial` prop, seeds `chats`, `activeWorkspaceId`, `userDefaultModel`, `configuredProviders`, and `model` from it. - `pickInitialModel()` mirrors the existing `applyDefaultModel` precedence so the SSR-seeded model is already valid on first paint — no flicker. - First-mount effect skipped when bootstrap was applied; refresh functions still available for workspace switches + post-edit invalidation.

    • apps/chat/app/c/page.tsx awaits loadChatBootstrap() and passes it as <ChatShell initial>
    • pickInitialModel() mirrors applyDefaultModel precedence so SSR-seeded picker matches the runtime picker
    • ChatShell seeds chats / activeWorkspaceId / model / providers from initial; skips first-mount fetch effect
    • New lib/chat-bootstrap.ts — server-side parallel read of chats + workspace + me + api-keys
  131. v1.7.192
    M9
    Thinking indicator after sendShipped May 11, 2026

    ## v1.7.192 — Visible "thinking…" between send + first token Between hitting Send and the first streamed token the chat used to look frozen — nothing on screen acknowledged the request. Add a pulsing-dots indicator below the message list whenever `isLoading` is true and the last message isn't already an assistant message (once the assistant row appears with streaming text, that itself is the progress signal). - Inline `<article>` matching the assistant bubble layout, three staggered pulse dots, "Thinking…" label. - `aria-live="polite"` + `aria-label` for screen readers. - Bilingual: `chat.thinking` ("Thinking…" / "חושב…"), `chat.thinking_aria`. - Auto-hides as soon as the assistant row enters `messages`.

    • Bilingual dict keys chat.thinking + chat.thinking_aria
    • aria-live=polite on the indicator for screen-reader announcement
    • Pulsing "Thinking…" row below messages while isLoading && last role != assistant
  132. v1.7.191
    M9
    MCP tools accept swarm display name, not just UUIDShipped May 11, 2026

    ## v1.7.191 — swarm_id accepts UUID or display name `AI_InvalidToolArgumentsError: Invalid uuid` was firing whenever the LLM passed the swarm's display name (Hebrew, emoji, anything that appears in the conversation) instead of its UUID. Strict `z.string().uuid()` rejected the call before our handler could even ask the LLM to retry. Every swarm-aware tool now accepts both forms: - `swarm/status`, `swarm/destroy`, `swarm/scale` (required ref) - `agent/spawn`, `agent/list` (required/optional ref) - `task/orchestrate`, `task/list` (optional ref) Resolution is workspace-scoped (RLS-safe), case-insensitive, and prefers the most recently created match on ambiguity. New shared helper in `packages/mcp-server/src/tools/_swarm-ref.ts` exports `resolveSwarmRef()` + `requireSwarmRef()` so every tool reads the same way. The DB-shape `AgentSchema` / `SpawnAgentInputSchema` in `@angi/types` stay strict (they describe persisted rows).

    • Shared _swarm-ref.ts helper (resolveSwarmRef + requireSwarmRef) used by every swarm-aware tool
    • agent/spawn + agent/list accept name or UUID; coordinator still receives a real UUID
    • swarm/status, swarm/destroy, swarm/scale accept name or UUID
    • task/orchestrate + task/list accept swarm display name, not just UUID
  133. v1.7.190
    M9
    Expand MODEL_CATALOG so dropdown shows every hosted modelShipped May 11, 2026

    ## v1.7.190 — Full hosted-model catalog Adds the rest of each hosted provider's lineup so the per-provider model picker on /settings/api-keys shows every model the user can actually call, not just the flagship. - OpenAI: + gpt-5-mini, gpt-4o-mini, o3, o3-mini (joining gpt-5 / gpt-4o). - Google: + gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash (joining gemini-2.5-pro). - Qwen: + qwen-plus, qwen-turbo, qwen-vl-max (joining qwen3-max). - Factory unchanged — `provider(spec.model)` forwards each id verbatim, so the new entries route correctly without per-model branching.

    • OpenAI catalog: add gpt-5-mini, gpt-4o-mini, o3, o3-mini
    • No factory.ts changes — generic provider(spec.model) pass-through handles every new id
    • Qwen catalog: add qwen-plus, qwen-turbo, qwen-vl-max
    • Google catalog: add gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.0-flash, gemini-1.5-pro, gemini-1.5-flash
  134. v1.7.189
    M9
    Per-provider model picker on /settings/api-keysShipped May 11, 2026

    ## v1.7.189 — Model picker per provider on /settings/api-keys - Anthropic / OpenAI / Google / Qwen rows now render a `<select>` of matching MODEL_CATALOG entries below the test indicators. Picking a model PATCHes /api/user/me with `{ default_model }`. - Ollama and LM Studio remain free-text — those providers accept whatever model id the user has loaded locally. - Optimistic UI: dropdown snaps to the new value before the PATCH resolves; rolls back + toasts the error on failure. - New i18n keys: `api_keys_page.select_model`, `choose_model_placeholder`, `model_active`, `model_saved`, `model_save_failed`.

    • Selection writes through to users.default_model via PATCH /api/user/me with optimistic UI
    • Bilingual dict keys (en + he) for select_model / choose_model_placeholder / model_active / model_saved / model_save_failed
    • Ollama + LM Studio rows intentionally skipped — their model id is free-text at runtime
    • Anthropic / OpenAI / Google / Qwen rows render a dropdown to pick the active model from MODEL_CATALOG
  135. v1.7.188
    M9
    LM Studio as a local providerShipped May 11, 2026

    User-requested: support a local model via LM Studio (gemma / gemma-3 family). LM Studio exposes an OpenAI-compatible HTTP server at http://localhost:1234/v1; SDK already has createOpenAI({ baseURL }) so integration is small. End-to-end: ModelProvider gains lmstudio; MODEL_CATALOG gains catch-all lmstudio-local entry that forwards user-typed model id verbatim; ApiKeys gains lmstudio_host + lmstudio_model; loadUserApiKeys hydrates; /api/user/api-keys allowlist + test probeLmStudio; /api/user/me configured_providers; reachableProvider in two places; /settings/api-keys form + /onboarding step-3 dropdown.

    • ApiKeys interface gains lmstudio_host + lmstudio_model fields
    • Bump home pills v1.7.187 -> v1.7.188 on apps/chat and apps/web
    • /settings/api-keys + /onboarding step-3 gain LM Studio rows + entry
    • probeLmStudio in /api/user/api-keys/test pings <host>/v1/models
    • factory.ts createOpenAI({ baseURL }) variant for lmstudio with user-set model id
    • ModelProvider enum + MODEL_CATALOG gain lmstudio + lmstudio-local entry
  136. v1.7.187
    M9
    Forward streamText error message to clientShipped May 11, 2026

    User-reported: banner showed only "An error occurred." — Vercel AI SDK's default placeholder when streamText fails mid-stream. Real error went to Sentry only; user + dev terminal had no signal. Two fixes: streamText onError now also console.error's the upstream error to dev terminal; toDataStreamResponse({ getErrorMessage }) forwards real error.name + message to client through the stream so v1.7.181 banner shows actionable text.

    • Bump home pills v1.7.186 -> v1.7.187 on apps/chat and apps/web
    • toDataStreamResponse getErrorMessage forwards real Error.name + message to client
    • streamText onError now console.errors to dev terminal alongside Sentry capture
    • Banner now shows actionable text like "AI_APICallError: rate limit exceeded"
  137. v1.7.186
    M9
    Graceful service-role-missing fallback on /api/chatShipped May 11, 2026

    User-reported: /api/chat returned 500 "Missing SUPABASE_SERVICE_ROLE_KEY". Production has the env var; local .env.local didn't. createAdminClient() threw hard, chat stopped. Fix: new createAdminClientOptional() in @angi/db/admin returns null when key missing + one-time console.warn. /api/chat uses optional: rate-limit skipped (ok=true) when admin null, recordUsage no-ops. Token counts still persist on message rows via user-RLS client. createAdminClient() strict variant still exported for paths that genuinely need the key (Stripe webhook, admin console).

    • /api/chat skips rate-limit + recordUsage when service-role missing
    • Bump home pills v1.7.185 -> v1.7.186 on apps/chat and apps/web
    • createAdminClient() strict variant retained for Stripe webhook + admin console
    • Token counts still persist on messages via user-RLS client
    • createAdminClientOptional() in @angi/db/admin returns null + warn instead of throw
  138. v1.7.185
    M9
    API-keys: per-provider connection-test affordanceShipped May 11, 2026

    User-requested: after entering an API key in /settings/api-keys, show whether the connection actually works before relying on it in chat. New POST /api/user/api-keys/test endpoint pings each provider's lightest authenticated GET (the models list) with the user's decrypted key (Anthropic /v1/models, OpenAI /v1/models, Google /v1beta/models?key=, DashScope /api/v1/models, Ollama /api/tags). Returns { ok, model_count, status?, error? } with an 8s timeout per probe. UI: PlugZap "Test connection" button per configured row → emerald check + count on success, rose X + status chip + raw error on failure. Keys never leave the server.

    • POST /api/user/api-keys/test pings provider models endpoint with decrypted key
    • Bump home pills v1.7.184 -> v1.7.185 on apps/chat and apps/web
    • Bilingual en+he api_keys_page.test + test_ok + test_ok_models with {count}
    • Keys never leave the server — load + use + drop, no blob in response
    • UI: PlugZap "Test connection" button → emerald success or rose failure inline chip
    • Per-provider probe: Anthropic + OpenAI + Google + DashScope + Ollama with 8s timeout
  139. v1.7.184
    M9
    Top-level catch on /api/chat + suppress extension hydrationShipped May 11, 2026

    Two follow-ups from user-reported console output. (1) /api/chat still 500'd with no body — v1.7.182 swarm-context try/catch only covered one step; any other synchronous throw in setup still bubbled to Next's generic 500. Fix: wrap entire POST body in top-level try/catch via private runChat(req) helper. Unhandled throws log + Sentry-tag + return apiError(server_error, "<Name>: <message>") so the v1.7.181 banner shows the actual cause. (2) "Prop className did not match" hydration from Usetiful extension injecting body classes. Added suppressHydrationWarning to <html>+<body>.

    • POST refactored to private runChat(req) helper for the catch wrapper
    • Bump home pills v1.7.183 -> v1.7.184 on apps/chat and apps/web
    • suppressHydrationWarning on <html>+<body> silences browser-extension class injection
    • Top-level try/catch on POST /api/chat — unhandled throws surface as apiError
  140. v1.7.183
    M9
    Fix: persist chat id in URL so refresh keeps the threadShipped May 11, 2026

    User-reported: history still disappeared after refresh even with v1.7.178 first-turn-persistence fix landed locally. Diagnosis: messages WERE persisted; bug was URL stayed at /c regardless of open chat → refresh mounted ChatShell with chatId=null. Fix: mirror active chat id into ?chat=<id> on every transition (select, new-chat-create, ensureChat-mints-fresh, clear). Mount effect reads ?chat from URL and rehydrates via onSelectChat. router.replace keeps history flat; back-button still exits cleanly.

    • Mount effect rehydrates from ?chat — refresh keeps the active thread
    • Bump home pills v1.7.182 -> v1.7.183 on apps/chat and apps/web
    • router.replace keeps history flat; back-button exits to clean /c
    • ?chat=<id> persisted on every chat transition (select/new/clear)
  141. v1.7.182
    M9
    Fix nested-button hydration + harden swarm-context fetchShipped May 11, 2026

    Two console warnings surfaced from local dev. (1) Nested <button> in ChatItem: outer chat row was a button containing the more-menu button as a nested child — invalid HTML + hydration warning. Switch outer to div with role=button + tabIndex + onKeyDown(Enter/Space) so a11y + keyboard nav still work. (2) /api/chat 500 on swarm-context fetch: wrap the whole block in try/catch + Sentry.captureException with step tag. Stream still runs without the agent suffix when fetch fails. (Browser-extension hydration mismatch on body class — Usetiful — ignored, not our code.)

    • Swarm-context fetch wrapped in try/catch; chat continues without suffix on failure
    • Bump home pills v1.7.181 -> v1.7.182 on apps/chat and apps/web
    • Sentry.captureException tags the failure with route + step=swarm-context
    • ChatItem outer button replaced with div[role=button] + keyboard handler
  142. v1.7.181
    M9
    Catch-all chat error banner — no silent failuresShipped May 11, 2026

    User-reported: sent a prompt, nothing happened, no error visible. Previous onError only recognised three apiError codes; everything else got logged to Sentry and silently swallowed. Fix: fourth genericError state + rose banner that ALWAYS shows for unrecognised failures. Banner renders code chip + server message verbatim so user can hand to support or grep Sentry.

    • Banner shows server-side code chip + raw message for support / Sentry triage
    • Bump home pills v1.7.180 -> v1.7.181 on apps/chat and apps/web
    • Bilingual en+he chat.error_title + error_dismiss + unknown_error
    • onError now always sets a banner instead of silently swallowing unknown codes
    • genericError state + rose banner for any unrecognised /api/chat failure
  143. v1.7.180
    M9
    Bump swarm heartbeats on every chat turnShipped May 11, 2026

    Closes the heartbeat-loop gap that v1.7.179 flagged. No per-agent runtime process exists to keep last_heartbeat_at fresh, so every spawned agent eventually went "stale" even while their attached chat was driving them. This turn wires the natural heartbeat source: chat activity. When /api/chat loads a swarm's context (v1.7.177 hookup), it now fires two non-blocking UPDATEs — agents.last_heartbeat_at = now() WHERE status=active, plus swarms.last_activity_at = now(). RLS gates both to workspace members; fire-and-forget so the stream never waits.

    • /api/chat bumps agents.last_heartbeat_at on every turn for active agents in attached swarm
    • Bump home pills v1.7.179 -> v1.7.180 on apps/chat and apps/web
    • RLS gates UPDATE to workspace members via existing agents_update_member policy
    • Fire-and-forget — stream never waits on heartbeat ack
    • swarms.last_activity_at updated alongside so admin surfaces see chat-driven freshness
  144. v1.7.179
    M9
    Fix: stale-agent icon no longer reads as loadingShipped May 11, 2026

    User-reported: agents stayed on a spinning loader icon a minute after spawn. The spinner came from the stale branch of AgentIcon, which fired when last_heartbeat_at was > 60s old. Agents are storage rows the LLM coordinates via swarm/* + agent/* MCP tools — no per-agent runtime process keeps last_heartbeat_at fresh (Phase 5 deliverable). Fix: replace Loader2 spinner with static Clock icon; bump STALE_AFTER_MS from 60s to 5min. Tooltip explicitly says "idle … coordinated by the LLM, no runtime loop".

    • STALE_AFTER_MS bumped from 60s to 5min to match swarm/status freshness window
    • Bump home pills v1.7.178 -> v1.7.179 on apps/chat and apps/web
    • Stale tooltip explains "idle … coordinated by LLM via swarm/* tools, no runtime loop"
    • AgentIcon stale state renders static Clock instead of spinning Loader2
  145. v1.7.178
    M9
    Fix: persist first turn — ensureChat/handleSubmit raceShipped May 11, 2026

    User-reported: after submitting first prompt and refreshing, history disappeared. Race: ensureChat() resolves with the new id but setChatId is async; handleSubmit fires synchronously with useChat's closure still holding the old (null) chatId. Server-side `if (chat_id && lastUser) insert` evaluated false → user message never persisted → onFinish assistant insert also skipped (same gate) → refresh fetched empty messages. Fix: capture id from ensureChat()'s return + pass via handleSubmit body override.

    • User + assistant messages now persist on the very first send of a new chat
    • Bump home pills v1.7.177 -> v1.7.178 on apps/chat and apps/web
    • First-turn race resolved by passing fresh id via handleSubmit body override
  146. v1.7.177
    M9
    Fix: attached swarm now reaches the LLMShipped May 11, 2026

    User-reported bug: attaching a swarm to a chat showed agents in the sidebar header but sending a prompt did nothing. Root cause: /api/chat read only system_prompt from the chat row; chats.swarm_id was set by the attach affordance but never loaded or surfaced to the model. The swarm/* + agent/* MCP tools are always in the default registry, so the infrastructure to delegate was there — the LLM just didn't know about it. Fix: load swarm + agents when swarm_id is non-null and append a structured delegation suffix to the system_prompt with the agent list + guidance. Also clarifies the FAQ: external MCP servers are NOT required for the attached swarm to work.

    • Structured suffix: name + topology + agent list + delegation guidance
    • Bump home pills v1.7.176 -> v1.7.177 on apps/chat and apps/web
    • User-set system_prompt remains primary; suffix appended with blank line
    • chat row swarm_id now loaded by /api/chat and injected into system_prompt
  147. v1.7.176
    M9
    angi chats duplicate CLIShipped May 11, 2026

    Headless counterpart to v1.7.175 sidebar Duplicate item. Talks to POST /api/chats/[id]/duplicate; prints new chat id + title so shell scripts can chain into `angi chats show <new_id>` or pipe further. Idempotent on server side (POST always mints a fresh row), so retries can't accidentally clobber.

    • Bump home pills v1.7.175 -> v1.7.176 on apps/chat and apps/web
    • angi chats duplicate <id> POSTs to /api/chats/[id]/duplicate
    • Prints new chat id + title for shell chaining
  148. v1.7.175
    M9
    Duplicate-chat affordanceShipped May 11, 2026

    POST /api/chats/[id]/duplicate clones the chat row + every persisted message into a fresh chat under the same workspace + user. Useful for "branch the conversation" workflows — try a different prompt or model without losing the original thread. Title gets a "(copy)" suffix; share_token + shared_at + swarm_id + pinned + archived reset. RLS gates both reads and writes; no service-role. Sidebar context menu gains a Duplicate item between Share and Export.

    • Title suffixed with " (copy)"; share/swarm/pin/archive state reset on clone
    • Bump home pills v1.7.174 -> v1.7.175 on apps/chat and apps/web
    • Bilingual en+he nav.duplicate
    • Sidebar context menu Duplicate item with lucide Copy icon
    • POST /api/chats/[id]/duplicate clones chat row + every message under RLS
  149. v1.7.174
    M9
    Workspace stats snapshot in /settings/workspaceShipped May 11, 2026

    New /api/workspaces/stats returns RLS-scoped head:true counts across 7 workspace-scoped tables: chats, messages, swarms, agents, memories, workspace_members, pending-invites. 7 parallel reads; cheap thanks to workspace_id indexes. /settings/workspace renders the counts as a 6-card strip above identity. Section is suppressed until the async fetch returns to avoid "zero flash". Members card carries an amber sub-pill for pending invites when > 0.

    • /api/workspaces/stats returns head-only counts across 7 workspace tables
    • Bump home pills v1.7.173 -> v1.7.174 on apps/chat and apps/web
    • Bilingual en+he workspace_page.stat_* + stat_invites_pending with {count}
    • Members card sub-pill flags pending invites in amber
    • Async load with section suppression — no zero flash before fetch returns
    • StatCard 6-card strip in /settings/workspace above the identity section
  150. v1.7.173
    M9
    Cmd+Shift+L locale-toggle shortcutShipped May 11, 2026

    Wires the locale-toggle shortcut listed in the v1.7.172 cheat sheet so the modal stops lying about what works. Press Cmd+Shift+L (Mac) or Ctrl+Shift+L (Win/Linux) to flip he ↔ en — same code path LocaleToggle uses (POST /api/locale + window.location.reload). Consolidated into one keydown handler at ChatShell level alongside Cmd+,.

    • Case-insensitive L match — Shift+l emits uppercase on most keyboards
    • Bump home pills v1.7.172 -> v1.7.173 on apps/chat and apps/web
    • Consolidated keydown handler at ChatShell level with Cmd+,
    • Cmd+Shift+L / Ctrl+Shift+L flips locale between he and en
  151. v1.7.172
    M9
    Keyboard-shortcuts cheat sheet on ?Shipped May 11, 2026

    Press ? (without modifiers, outside any input) to open a modal listing every chat-surface shortcut: Cmd+K palette, Cmd+, settings, Cmd+/ slash-command, Enter send, Shift+Enter newline, Escape close, Cmd+Shift+L locale toggle. navigator.platform detected once on mount so the modifier glyph matches the host OS — ⌘ on Mac, Ctrl elsewhere. Listener gates on event.target not being a typeable field so ? in the composer still types literally.

    • Modifier glyph adapts to host OS (⌘ on Mac, Ctrl elsewhere)
    • Bump home pills v1.7.171 -> v1.7.172 on apps/chat and apps/web
    • Bilingual en+he shortcuts.title + per-shortcut labels
    • Listener gates on non-typeable target so ? in composer types literally
    • ShortcutsDialog component portaled to body, opens on ? key
  152. v1.7.171
    M9
    Cmd+, opens settings dialogShipped May 11, 2026

    macOS convention shortcut. Mirrors the Cmd+K palette binding in command-palette.tsx — chat stays keyboard-first. Press again to close without losing the active tab. Listener at ChatShell level so the shortcut works from anywhere on /c, not just inside the composer.

    • Toggle preserves active tab — repeat-press closes; reopens on Profile
    • Bump home pills v1.7.170 -> v1.7.171 on apps/chat and apps/web
    • Cmd+, / Ctrl+, toggle the SettingsDialog from anywhere on /c
  153. v1.7.170
    M9
    Per-chat message-count badge in sidebarShipped May 11, 2026

    Companion to the v1.7.169 tokens pill. Each chat row now carries two compact monospace pills: message_count + tokens_total. Distinguishes "long thread, few tokens" from "short thread, many tokens" at a glance. GET /api/chats's existing aggregate gains a second counter on the same row — no extra round-trip. Message-count pill is the dimmer of the two so the eye anchors on tokens when both exist.

    • GET /api/chats returns message_count per chat alongside tokens_total
    • Bump home pills v1.7.169 -> v1.7.170 on apps/chat and apps/web
    • Bilingual en+he nav.message_count with {count}
    • Suppressed at zero so fresh chats stay visually quiet
    • Dimmer monospace pill (text-zinc-600) so tokens stays the primary signal
  154. v1.7.169
    M9
    Per-chat token-total pill in sidebarShipped May 11, 2026

    Each sidebar chat row renders a monospace pill with the chat's lifetime token total (input + output summed across every persisted message). GET /api/chats does a single aggregate read against `messages` for the returned ids + joins client-side — one extra round-trip per sidebar refresh in exchange for an N+1 per-row read. RLS still gates the messages select. Suppressed at 0; >999 collapses to N.Nk; opacity-dimmed when row isn't hovered.

    • GET /api/chats returns tokens_total per chat via single aggregate join
    • Bump home pills v1.7.168 -> v1.7.169 on apps/chat and apps/web
    • Bilingual en+he nav.tokens_total with {count}
    • Suppressed at zero so fresh chats stay visually quiet
    • Sidebar pill shows N or N.Nk with title tooltip for absolute count
  155. v1.7.168
    M9
    Per-message token footer on assistant rowsShipped May 11, 2026

    Renders ↑NN ↓MM chip under every assistant message that has tokens_input/tokens_output persisted. The columns have been populated by /api/chat.onFinish since v1.7.31; the chat surface never surfaced them. Visible only on rehydrated rows (page reload / chat switch) — streaming Vercel AI SDK Message shape lacks usage data during the stream itself. Live-streamed turns gain the chip on next chat refresh.

    • Title tooltip spells out full input/output split for hover triage
    • Bump home pills v1.7.167 -> v1.7.168 on apps/chat and apps/web
    • Bilingual en+he chat.tokens_tooltip with {input} + {output}
    • Suppressed at zero — no chip on tool-call-only turns
    • Inline ↑input ↓output token chip on rehydrated assistant messages
  156. v1.7.167
    M9
    angi mcp CLI: ls / add / rmShipped May 11, 2026

    Headless MCP server registration. Mirrors /settings/mcp. Talks to /api/mcp/servers + /api/mcp/servers/[id]. Supports http + stdio transports, repeatable -H headers (K=V), --disabled flag, --description. Server-side RLS still gates create/delete to owner/admin; CLI forwards.

    • angi mcp add <name> <endpoint> with -H K=V headers + --transport + --description + --disabled
    • Bump home pills v1.7.166 -> v1.7.167 on apps/chat and apps/web
    • Header parser splits on first = so K=base64== values survive intact
    • angi mcp rm <id> deletes via /api/mcp/servers/[id] DELETE
    • angi mcp ls [--json] lists registered MCP servers with enabled-dot + transport
  157. v1.7.166
    M9
    Re-run onboarding link in settings dialog footerShipped May 11, 2026

    Sparkles-icon link in the settings dialog left rail footer routes to /onboarding for users who already onboarded but want to revisit a step. Clicking closes the dialog first so the user lands on the wizard without a stuck modal behind it. Wizard step pre-computation makes re-runs idempotent — no half-state gotchas.

    • Click closes dialog then nav so wizard mounts without a stuck modal
    • Bump home pills v1.7.165 -> v1.7.166 on apps/chat and apps/web
    • Bilingual en+he settings.rerun_onboarding
    • Re-run onboarding link with Sparkles icon in dialog left-rail footer
  158. v1.7.165
    M9
    angi search CLI across chat messagesShipped May 11, 2026

    Headless full-text search over message bodies across the caller's accessible workspaces. Mirrors the chat sidebar search box. Talks to /api/search which routes through the security-definer search_messages RPC. <mark> tags around matches stripped to ANSI bold so the terminal preview reads naturally; --json keeps the raw HTML snippet for downstream consumers.

    • <mark> tags stripped to ANSI bold for terminal-friendly snippets
    • Bump home pills v1.7.164 -> v1.7.165 on apps/chat and apps/web
    • Role tone (assistant=cyan, user=green) for at-a-glance hit triage
    • angi search <query> [--json] [--limit N] full-text search via /api/search
  159. v1.7.164
    M9
    /api/releases JSON feed + angi releases ls CLIShipped May 11, 2026

    Releases as structured JSON. /updates/feed.xml has been the RSS-reader path since v1.7.123; now SDK + CLI consumers can hit the same data without parsing XML. New /api/releases public-read returns recent N releases with items grouped per row in one round-trip. Cache-Control: 5-min edge. Query knobs: ?limit (max 200) + ?status. CLI angi releases ls reads the feed with --json + --limit + --all flags. Per-row preview shows up to 4 items inline with a type tag.

    • 5-min edge cache + 1-min browser cache to keep feed cheap
    • Bump home pills v1.7.163 -> v1.7.164 on apps/chat and apps/web
    • Per-row item preview with type-tag (+/×/·) and "+N more" tail
    • angi releases ls reads the feed with --json + --limit + --all
    • Query knobs: ?limit (1..200 default 50) + ?status (shipped|planned|in_progress)
    • /api/releases public JSON feed of recent releases + items grouped per row
  160. v1.7.163
    M9
    angi audit ls CLIShipped May 11, 2026

    Headless audit-log reader. Mirrors /settings/audit. Talks to /api/audit (RLS-scoped). Supports --json + --action prefix filter + --limit. Useful for compliance scripts that pull weekly audit CSVs without clicking through the settings dialog.

    • angi audit ls [--json] [--action prefix] [--limit N] prints workspace audit log
    • Bump home pills v1.7.162 -> v1.7.163 on apps/chat and apps/web
    • Client-side --action prefix filter keeps the API surface unchanged
    • Action-suffix tone (deleted=red, created=green, updated=cyan) for at-a-glance scan
  161. v1.7.162
    M9
    angi members CLI: list / set / removeShipped May 11, 2026

    Headless workspace-member management. Completes the team-ops trio alongside v1.7.161 invite + v1.7.135 profile. Talks to /api/members (list) + /api/members/[user_id] (PATCH/DELETE). Owner role display-only — promotion goes through the v1.7.149 transfer-ownership RPC.

    • angi members set <user_id> --role <admin|member|viewer> patches role
    • Bump home pills v1.7.161 -> v1.7.162 on apps/chat and apps/web
    • angi members remove <user_id> deletes membership via DELETE
    • angi members list [--json] prints the workspace member table
  162. v1.7.161
    M9
    angi invite CLI: list / add / revokeShipped May 11, 2026

    Headless workspace-invite management for ops bulk flows. Mirrors /settings/team + v1.7.157 onboarding invite step against /api/invites. Bulk-invite via shell loop without clicking the settings dialog 30 times: `while read email; do angi invite add "$email" -r viewer; done < team.txt`. Token + expiry printed on `add` so the inviter can copy out-of-band.

    • angi invite add <email> [-r role] creates invite via POST /api/invites
    • Bump home pills v1.7.160 -> v1.7.161 on apps/chat and apps/web
    • add prints token + expiry so the inviter can share out-of-band
    • angi invite revoke <id> deletes invite via DELETE /api/invites/[id]
    • angi invite list [--json] prints active invites for the workspace
  163. v1.7.160
    M9
    Single popup as the only settings surfaceShipped May 11, 2026

    Per user feedback: settings had two competing surfaces — the gear-menu SettingsDialog popup and the /settings/* full-page routes (left nav + content). Consolidate to one — the popup. SettingsDialog adds Profile + Security tabs via two new client-side loaders (ProfileLoader, SecurityLoader) mirroring the WorkspaceLoader pattern. All /settings + subroutes become two-line redirect server components pointing at /c?settings=<key>. ChatShell reads ?settings on mount, opens the dialog, then clears the query. Pure UX consolidation; no new API or migration.

    • All /settings/* routes become server redirects to /c?settings=<key>
    • Bump home pills v1.7.159 -> v1.7.160 on apps/chat and apps/web
    • Pass-through settings layout — persistent left nav from v1.7.153 retired
    • Sidebar gear menu lists Profile + Security alongside eight existing panels
    • ChatShell reads ?settings on mount, opens dialog, clears query
    • SettingsDialog adds Profile + Security tabs with new client-side loaders
  164. v1.7.159
    M9
    Profile avatar upload via Supabase StorageShipped May 11, 2026

    New `avatars` storage bucket (public-read) with RLS policies keyed on auth.uid()::text as the first path segment. Owner-only writes, public reads. /settings/profile gets Upload + Remove affordances; uploads route through supabase.storage under the user JWT and persist publicUrl to users.avatar_url via PATCH /api/user/me. PATCH whitelist widens to accept avatar_url (https + <=500). Client-side guards: 2 MB cap + image/png|jpeg|webp|gif only.

    • Bilingual en+he profile_page.avatar_{upload,remove,uploaded,removed,too_large,bad_type}
    • Bump home pills v1.7.158 -> v1.7.159 on apps/chat and apps/web
    • /settings/profile Upload + Remove buttons with 2 MB + image-type client guards
    • PATCH /api/user/me whitelist widens to accept avatar_url (https + <=500)
    • Owner-only writes via auth.uid()::text path prefix gate
    • Migration 0027: public-read avatars storage bucket + 4 RLS policies
  165. v1.7.158
    M9
    Always-visible usage pill in chat sidebarShipped May 11, 2026

    Surfaces plan-cap utilization without forcing the user into /settings/billing. UsagePill polls /api/billing/usage every 60s while chat is mounted, renders <plan_id> · NN% above the settings menu, colored by tier (emerald <80%, amber 80-99%, rose >=100%). Click-through to /settings/billing for full chart + upgrade CTA. Tooltip shows absolute used/limit numbers on hover.

    • Bump home pills v1.7.157 -> v1.7.158 on apps/chat and apps/web
    • Bilingual en+he usage_pill.tooltip with {plan} + {used} + {limit}
    • Click-through to /settings/billing; hover tooltip shows used/limit absolutes
    • Tone tracks pct utilization with same thresholds as v1.7.132 billing-panel callouts
    • UsagePill polls /api/billing/usage every 60s while chat mounted
  166. v1.7.157
    M9
    Onboarding wizard: invite-team stepShipped May 11, 2026

    Closes the plan's full "signup → workspace → invite → key → first chat" onboarding flow. v1.7.154 shipped 4 steps and skipped the invite leg; this turn slots it in between workspace + key. New step 3: email + role (member/admin/viewer) + Add button → POST /api/invites. Added invites accumulate inline with green check + role pill. Skip + deep link to /settings/team alternatives. Stepper widens from 4 → 5; saveKey() now sets step 5 (Done).

    • Step 3 Invite: email + role dropdown + Add → POST /api/invites
    • Inline accumulator of added invites with green check + role pill
    • Bump home pills v1.7.156 -> v1.7.157 on apps/chat and apps/web
    • Bilingual en+he onboarding.step3 rewritten, invite_*, role_*, step4 (was step3), step5 (was step4)
    • Stepper widens to 5 segments; saveKey() transitions to step 5 Done
    • Skip-invite + deep link to /settings/team for full team panel
  167. v1.7.156
    M9
    MFA challenge prompt after AAL1 sign-inShipped May 11, 2026

    Closes the v1.7.152 MFA loop. Without this prompt a verified TOTP factor at /settings/security was purely advisory — login returned an AAL1 session and never elevated, so the factor never gated anything. Now after signInWithPassword or verifyOtp succeeds, the form calls mfa.getAuthenticatorAssuranceLevel() + listFactors(); if nextLevel=aal2 AND a verified TOTP factor exists, mode flips to mfa with a 6-digit input + Verify button. Submitting calls challenge() + verify() before router.push(next).

    • challenge() + verify() elevation flow before router.push(next)
    • Bump home pills v1.7.155 -> v1.7.156 on apps/chat and apps/web
    • maybeRequireMfa() hooks both AAL1 success paths (password + OTP)
    • Bilingual en+he login.mfa_label + mfa_submit + mfa_failed + mfa_no_factor + code_format
    • autoComplete=one-time-code so password managers fill from authenticator app
    • New login mode mfa with 6-digit input + Verify factor button
  168. v1.7.155
    M9
    Auto-redirect to /onboarding when no workspaceShipped May 11, 2026

    Closes the onboarding loop from v1.7.154. /c is now a server component that runs a workspace-membership probe before mounting ChatShell: signed-in user with zero workspaces redirects to /onboarding; signed-in with >=1 workspace renders the shell as before. Probe is RLS-scoped + LIMIT 1 so cheap on every chat page load. Wizard's own server-side step-computation makes the redirect idempotent — first-time users only.

    • /c page upgraded to server component with workspace-membership gate
    • Bump home pills v1.7.154 -> v1.7.155 on apps/chat and apps/web
    • RLS-scoped LIMIT 1 probe — cheap on every chat page load
    • Zero-workspace user redirects to /onboarding before ChatShell mounts
  169. v1.7.154
    M9
    First-run onboarding wizard at /onboardingShipped May 11, 2026

    Plan section "Onboarding flow: signup → workspace create → invite team → connect first LLM key → first chat" lands here. Closes M9 polish item #1. 4-step wizard, single client component, server pre-computes the starting step from caller state so returning users land on their first unfinished step. Step 1 welcome (when no workspace), Step 2 workspace create or confirm (auto-slugify + cookie set), Step 3 LLM key (provider dropdown + deep link to provider docs + encrypted POST /api/user/api-keys), Step 4 done → /c. RLS-only, no service-role.

    • /onboarding page with 4-step stepper (welcome / workspace / key / done)
    • Bump home pills v1.7.153 -> v1.7.154 on apps/chat and apps/web
    • Bilingual en+he onboarding.* with {email} + {workspace} placeholders
    • Provider dropdown (Anthropic / OpenAI / Google / Qwen / Ollama) + deep link to docs
    • Workspace create flips angi_active_workspace cookie so chat routes to new tenant
    • Auto-slugify workspace name into URL-safe slug; manual override available
    • Server pre-computes starting step from existing workspace + key state
  170. v1.7.153
    M9
    Persistent left nav across /settings/*Shipped May 11, 2026

    Plan M9 polish item #8. Single layout.tsx wraps every settings subpage; SettingsNav renders a sticky 60-wide left strip on lg+ screens. Mobile keeps the grid-index landing affordance at /settings (no extra chrome) so small-screen flow stays single-surface. Subpages keep their existing main + back-link — layout just adds a sibling column. Active row matched on pathname-prefix so future sub-routes light the parent. Logical-direction Tailwind for RTL flip.

    • SettingsNav lists 11 surfaces with lucide icons + dict labels
    • Bump home pills v1.7.152 -> v1.7.153 on apps/chat and apps/web
    • Mobile keeps the grid-index landing at /settings; lg:block gates the sidebar
    • logical-direction border-e + ps-3 flips strip to the right edge under RTL
    • Pathname-prefix match keeps parent row lit on future sub-routes
    • apps/chat/app/settings/layout.tsx wraps every subpage with persistent sidebar
  171. v1.7.152
    M9
    MFA TOTP self-enrolment at /settings/securityShipped May 11, 2026

    Closes plan M9 polish item #9 (2FA / MFA UI). New /settings/security lets a signed-in user list / enrol / remove Supabase Auth MFA factors end-to-end. Server SSR renders the initial factor list; all subsequent MFA SDK calls run browser-side via supabase.auth.mfa.enroll/challenge/verify/unenroll — no service-role path, no new API surface. Verified factors gate sign-in on the next session.

    • TOTP enrolment: enroll() -> QR + secret -> challenge() -> verify(code)
    • Bump home pills v1.7.151 -> v1.7.152 on apps/chat and apps/web
    • Bilingual en+he security_page.* + settings.security + settings.card_security
    • Verified factor required on next sign-in via Supabase Auth AAL2 gate
    • QR data URL + raw secret rendered together for users without a QR scanner
    • /settings/security with active-factor list and per-factor Remove button
  172. v1.7.151
    M9
    attachSentryScope rollout complete across SaaS surfacesShipped May 11, 2026

    Last leg of the v1.7.143/144/150 helper rollout. 16 more authenticated routes now tag the Sentry isolation scope with user_id + workspace_id (where resolved) + route: api.audit, api.search, api.members.get, api.memories.get, api.swarms (GET/POST), api.invites (GET/POST), api.invites.accept, api.user.api-keys (GET/POST/DELETE), api.workspaces.active, api.workspaces.id (PATCH/DELETE), api.workspaces.leave, api.workspaces.export, api.billing.usage.daily, api.mcp.servers (GET/POST), api.admin.me, api.goal.plan. Every authenticated route in apps/chat/app/api now carries the same Sentry tags as the client-side sentry-user-bridge.

    • Per-method route tag distinguishes verbs (GET/POST/PATCH/DELETE) in Sentry UI
    • Bump home pills v1.7.150 -> v1.7.151 on apps/chat and apps/web
    • Server + client Sentry filters share user_id / workspace_id / route keys
    • workspace_id passed when resolved upfront; null for pre-workspace + user routes
    • attachSentryScope across 16 remaining hot auth routes
  173. v1.7.150
    M9
    Sentry scope rollout: chat sub-routesShipped May 11, 2026

    Continues v1.7.143/144 rollout. attachSentryScope now applied to api.chats.id (GET/PATCH/DELETE), api.chats.regenerate, api.chats.share (POST/DELETE), api.chats.swarm (GET/PUT/DELETE). Each ctx() helper takes a route string so per-method tag distinguishes verbs in Sentry UI. workspace_id is null for these routes — chat row carries workspace implicitly via RLS and resolving it would cost an extra round-trip per request.

    • attachSentryScope on api.chats.swarm (GET/PUT/DELETE)
    • attachSentryScope on api.chats.share (POST/DELETE)
    • attachSentryScope on api.chats.regenerate
    • attachSentryScope on api.chats.id (GET/PATCH/DELETE) with per-method route tag
    • Bump home pills v1.7.149 -> v1.7.150 on apps/chat and apps/web
  174. v1.7.149
    M9
    Workspace transfer-ownership flowShipped May 11, 2026

    Plan M9 polish item. Closes the missing leg in the workspace lifecycle between leave + delete: a current owner can hand ownership to another existing member without leaving themselves. Migration 0026 SECURITY DEFINER RPC transfer_workspace_ownership(ws_id, new_owner) runs all three writes (owner_id swap, target promoted, current demoted to admin) + audit_log row in one transaction. POST /api/workspaces/[id]/transfer-ownership maps pg sqlstates back to apiError codes. Owner-only section in /settings/workspace with member dropdown + confirm-then-go.

    • Owner-only transfer section with member dropdown + confirm-then-go flow
    • Migration 0026: SECURITY DEFINER RPC transfer_workspace_ownership(ws_id, new_owner)
    • POST /api/workspaces/[id]/transfer-ownership maps pg sqlstates to apiError codes
    • Bump home pills v1.7.148 -> v1.7.149 on apps/chat and apps/web
    • Bilingual en+he workspace_page.transfer_{title,body,pick,button,confirm,done}
    • audit_log workspace.ownership_transferred row with from/to payload
    • Demotes current owner to admin in same transaction; user can leave manually
  175. v1.7.148
    M9
    Shared pickReachableModel + drop hardcoded model defaultsShipped May 11, 2026

    v1.7.145–147 fixed provider-aware model resolution per route. This turn hoists it to a single helper at apps/chat/lib/model-pick.ts so 4 call-sites stay in sync: /api/chat (SDK no-model fallback), /api/chats POST (persisted chat row default), /api/goal/plan (replaces local impl), apps/cli chat (drops client-side default). Behavior change: SDK consumers + angi chat no longer hardcode claude-sonnet-4-6 — server picks the first MODEL_CATALOG entry whose provider has a reachable key. Explicit --model still wins when reachable; mismatch returns 400 invalid_input with banner-friendly extras.

    • /api/chat falls back to pickReachableModel when SDK omits model field
    • Bump home pills v1.7.147 -> v1.7.148 on apps/chat and apps/web
    • apps/cli chat drops -m default; server picks when omitted
    • /api/goal/plan now imports the shared helper
    • /api/chats POST persists reachable model on chat row instead of hardcoded default
    • apps/chat/lib/model-pick.ts hosts canonical reachableProvider + pickReachableModel
  176. v1.7.147
    M9
    Provider-aware goal planner + visible chat error bannerShipped May 11, 2026

    Two user-reported bugs from the single-key UX. (1) Chat: /api/chat returned 400 invalid_input on first send when picked model's provider had no key. chat-shell.onError handled only quota_exceeded + rate_limited — invalid_input fell into a silent catch-all. User saw nothing. Now: amber inline banner with deep link to /settings/api-keys, clears on next onFinish. (2) Goal planner: /api/goal/plan hardcoded claude-sonnet-4-6 default → 401 → 502 for users with only OpenAI/Qwen/Gemini keys. Now: pickReachableModel(requested, keys) auto-picks the first MODEL_CATALOG entry whose provider has a reachable key. Same auto-pick logic as chat-shell so the two surfaces stay in sync.

    • Chat invalid_input now renders inline amber banner with /settings/api-keys deep link
    • Bump home pills v1.7.146 -> v1.7.147 on apps/chat and apps/web
    • Bilingual en+he chat.no_key_error_{title,body,cta} with {provider} and {model}
    • pickReachableModel helper in /api/goal/plan mirrors chat-shell auto-pick precedence
    • Goal planner auto-picks reachable provider model instead of hardcoded claude-sonnet-4-6
  177. v1.7.146
    M9
    Validate saved default_model against configured providersShipped May 11, 2026

    v1.7.145 left a hole: a stale profile.default_model (e.g. claude-opus-4-7 saved when user had an Anthropic key) would override the single-provider auto-pick after the Anthropic key was removed + replaced with OpenAI. User saw Claude in picker despite having no Claude key, then 400 on first send. Fix: when default_model's provider is not in configured_providers (and the list is non-empty), treat it as stale and fall through to auto-pick. Auto-pick broadened from "exactly one provider" to "any provider list" — first reachable MODEL_CATALOG entry wins. Covers single-key case AND multi-key-with-stale-default case.

    • Empty configured_providers (env-var fallback deployments) treated as wildcard match
    • Bump home pills v1.7.145 -> v1.7.146 on apps/chat and apps/web
    • Auto-pick broadened from exactly-one-provider to any-provider-list
    • Stale default_model whose provider is not configured falls through to auto-pick
  178. v1.7.145
    M9
    Single-key auto-pick + no-key hint + clear 400 on chatShipped May 11, 2026

    Four-part fix for the "only one LLM key configured" UX. /api/user/me returns configured_providers derived from the decrypted user keys map (never the blobs). ChatShell first-mount auto-picks a matching MODEL_CATALOG entry when only one provider has a key — bootstrap qwen-3-6-max no longer wins by default for Claude-only users. Composer picker dims (never disables) rows whose provider isn't configured + small amber "no key" badge with tooltip explaining env-var fallback. /api/chat does pre-flight providerHasKey() check (user key OR env-var) and returns 400 invalid_input with provider+model extras on miss — replaces the upstream-401 mid-stream that surfaced as a generic toast.

    • /api/user/me returns user.configured_providers ModelProvider[] derived from decrypted keys map
    • Bump home pills v1.7.144 -> v1.7.145 on apps/chat and apps/web
    • Bilingual en+he chat.no_key_badge + chat.no_key_help with {provider} placeholder
    • /api/chat providerHasKey() pre-flight returns 400 invalid_input with provider+model extras
    • Composer rows never disabled — env-var fallback still works for self-hosted
    • Composer dims rows + amber "no key" badge for unconfigured providers, with tooltip
    • ChatShell single-provider auto-pick when no profile default_model set
  179. v1.7.144
    M9
    attachSentryScope rollout finish across SaaS surfacesShipped May 11, 2026

    Continues v1.7.143. Four more authenticated routes now tag the Sentry isolation scope: api.billing.checkout (conversion), api.billing.usage (rollup driver), api.user.me get + patch (profile + CLI identity), api.workspaces get + post (list + create). workspace_id is null for pre-workspace routes — helper only applies the tag when present so tenant-bound filters still match.

    • attachSentryScope on api.billing.usage (rollup driver)
    • Bump home pills v1.7.143 -> v1.7.144 on apps/chat and apps/web
    • attachSentryScope on api.workspaces.get + .post routes
    • attachSentryScope on api.user.me.get + .patch routes
    • attachSentryScope on api.billing.checkout (conversion path)
  180. v1.7.143
    M9
    attachSentryScope() helper across hot API routesShipped May 11, 2026

    Extracts the v1.7.142 inline Sentry scope tags into a shared lib/sentry-scope.ts helper. Rolled out to api.chat + api.chats + api.mcp.v1 + api.billing.portal. Each route now tags Sentry events with route + user_id + workspace_id so prod incidents are filterable per-tenant AND per-surface — a 5xx spike on api.mcp.v1 vs api.chat is now a single filter, not a log dive.

    • Applied to api.chat (refactored from inline) + api.chats + api.mcp.v1 + api.billing.portal
    • Bump home pills v1.7.142 -> v1.7.143 on apps/chat and apps/web
    • route tag enables per-surface filtering separate from per-tenant filter
    • lib/sentry-scope.ts attachSentryScope() helper for user_id + workspace_id + route tags
  181. v1.7.142
    M9
    Server-side Sentry user + workspace_id tag on /api/chatShipped May 11, 2026

    Mirrors the client-side scope from sentry-user-bridge. Every server capture on /api/chat now carries Sentry.setUser({id}) + Sentry.setTag(workspace_id) so a Sentry incident filtered by either field surfaces both sides (browser + server) under the same filter. Without this, workspace_id was buried in breadcrumb.data which is unfilterable in Sentry UI.

    • Sentry.setTag(workspace_id) for filterable per-tenant error feed
    • Bump home pills v1.7.141 -> v1.7.142 on apps/chat and apps/web
    • Sentry.setUser({id}) after auth resolution on /api/chat
  182. v1.7.141
    M9
    RSS feed: per-type categories on /updates/feed.xmlShipped May 11, 2026

    Each <item> in /updates/feed.xml now emits one <category>type:kind</category> per bullet type that appeared in the release (feature / fix / chore / breaking / docs / test / security). RSS readers can filter by category. Milestone (M1..M9) stays as a separate category so milestone-based filters work alongside type-based ones. No schema change, no extra round-trip.

    • Per-type <category>type:kind</category> rows on each RSS <item>
    • Bump home pills v1.7.140 -> v1.7.141 on apps/chat and apps/web
    • Deduped per release across all bullet types in release_items
  183. v1.7.140
    M9
    Per-milestone shipped count pill on Now/Next/Later boardShipped May 11, 2026

    Each row in the Now/Next/Later board now carries an emerald "N shipped" pill next to the milestone name. Source is the already-loaded releases array tallied by milestone with status=shipped — no extra DB round-trip. Pill suppressed at zero so future-only columns stay clean.

    • tallyShipped() counts shipped releases per milestone from loaded array
    • Bump home pills v1.7.139 -> v1.7.140 on apps/chat and apps/web
    • Bilingual en+he updates_page.shipped_count + shipped_count_help with {count}
    • Emerald shipped-count pill on each milestone row, suppressed at zero
  184. v1.7.139
    M9
    Smoke: /api/chat close-path + /api/health deep probeShipped May 11, 2026

    Mitigates plan risk #4 (streaming deadlocks). Two new HTTP probes in tooling/scripts/smoke.ts: /api/chat unauth POST must 401 within 10s (proves the close path against a route with maxDuration 60), and /api/health?deep=1 asserts the deployed function can still reach Supabase — catches secret-rotation misses before users do. Both have explicit 10s timeouts so a hang surfaces as a clean failure.

    • /api/health?deep=1 probe — asserts deps.supabase=true from deployed fn
    • Bump home pills v1.7.138 -> v1.7.139 on apps/chat and apps/web
    • Body drain on close-path probe to catch half-open socket leaks
    • /api/chat unauth POST close-path probe — expects 401 within 10s
  185. v1.7.138
    M9
    Default badge on chat model pickerShipped May 11, 2026

    Visual feedback for the v1.7.134-136 default_model loop. The model picker dropdown now renders a small "default" badge next to the row matching the saved profile default_model — so a user can tell apart a sticky-via-profile pick from a one-off pick mid-session.

    • userDefaultModel prop threaded ChatShell -> Composer
    • Bump home pills v1.7.137 -> v1.7.138 on apps/chat and apps/web
    • Bilingual en+he chat.default_badge + chat.default_badge_help
    • Emerald "default" badge on dropdown row matching profile default_model
  186. v1.7.137
    M9
    Nightly smoke: swarm/status + memory store-search round-tripShipped May 11, 2026

    Fills two plan-spec'd gaps in tooling/scripts/smoke.ts: plan step 2 (poll swarm/status until 5 agents active) and plan step 3 (memory/store then memory/search round-trip). Uses keyword search mode so the round-trip works without an embed() callback — exercises the RLS scope + read path end-to-end.

    • memory/store then memory/search keyword round-trip with unique tag per run
    • Bump home pills v1.7.136 -> v1.7.137 on apps/chat and apps/web
    • swarm/status exercise via registry — asserts 5 active agents via coordinator
  187. v1.7.136
    M9
    Chat honors saved default_model on first mountShipped May 11, 2026

    Closes the v1.7.134/135 loop — the profile default_model now flips the chat model picker on first mount. Gated to the bootstrap value so a mid-session pick is never overridden by an out-of-order /api/user/me response. Network blips fall through silently to the bootstrap model.

    • Setter gated to bootstrap value — mid-session pick never overridden
    • Bump home pills v1.7.135 -> v1.7.136 on apps/chat and apps/web
    • ChatShell first-mount fetch reads user.default_model from /api/user/me
  188. v1.7.135
    M9
    angi profile CLI command (read + set)Shipped May 11, 2026

    Rounds out the v1.7.134 profile API on the CLI side. `angi profile` prints display_name + default_model; `angi profile set --name X --model Y` PATCHes the same RLS-scoped /api/user/me path the web form uses, so the two surfaces stay in lockstep with no drift.

    • angi profile set --name + --model patches via /api/user/me RLS path
    • Bump home pills v1.7.134 -> v1.7.135 on apps/chat and apps/web
    • --json flag emits raw payload for scripting consumers
    • Empty-string flag values clear the field (matches API null contract)
    • angi profile prints display_name + default_model + avatar_url + id
  189. v1.7.134
    M9
    Profile self-edit page (display name + default model)Shipped May 11, 2026

    Adds /settings/profile so a user can set their display_name (sidebar + share previews) and default_model (picker preselect for new chats). Backed by RLS-scoped PATCH /api/user/me with a strict two-field whitelist — no service-role, the schema's self-write policy gates the path.

    • Bilingual en+he profile_page.* + settings.profile + settings.card_profile dict keys
    • Bump home pills v1.7.133 -> v1.7.134 on apps/chat and apps/web
    • GET /api/user/me now returns display_name + avatar_url + default_model
    • PATCH /api/user/me with strict two-field whitelist + RLS self-policy
    • /settings/profile page with display_name + default_model fields
  190. v1.7.133
    M9
    7-day momentum cadence pill on /updates metricsShipped May 11, 2026

    Each of the four metric cards on /updates now shows a "+N this week" pill below the all-time count. The 7-day window is computed from the same loaded release rows — no extra Supabase round-trip, no GitHub API dependency. Aligns with the master plan section 4 of /updates spec.

    • Cadence pill "+N this week" on each metric card
    • Bump home pills v1.7.132 -> v1.7.133 on apps/chat and apps/web
    • Bilingual en+he updates_page.metric_this_week with {count} placeholder
    • 7-day window anchored on Date.now()-7d, stable within ISR revalidate=300 cycle
  191. v1.7.132
    M9
    Pre-emptive plan-cap warning in /settings/billingShipped May 11, 2026

    Surfaces an in-app callout BEFORE /api/chat returns a 402 quota_exceeded. Amber at >=80% usage, red at >=100%. Bilingual en+he with {pct} placeholder. Single rollup source-of-truth shared with the chat 402 gate so the warning and the eventual block stay in sync.

    • Red callout at >=100% plan-cap usage explaining the 402 pause
    • Bump home pills v1.7.131 -> v1.7.132 on apps/chat and apps/web
    • Bilingual en+he dict keys billing_page.cap_warning_{near,over} with {pct} placeholder
    • Amber callout at >=80% plan-cap usage with upgrade CTA
  192. v1.7.131
    M9
    E2E contract spec for /api/workspaces/[id]/exportShipped May 11, 2026

    v1.7.129 + v1.7.130 wired the GDPR data-portability endpoint + CLI wrapper. Both consumers (browser download button + angi workspace export) depend on the response shape staying stable — a regression on angi_export_version, the array keys, or the Content-Disposition filename silently breaks the portability commitment. workspace-export.spec.ts asserts GET 200 + required keys, Content-Disposition filename, cache-control no-store, 403 workspace_member_only on stranger, 401 on cleared auth. Self-skips without service-role; lands in the authed-project.

    • Asserts Content-Disposition filename shape angi-workspace-<slug>-YYYYMMDD.json.
    • Self-skips when SUPABASE_SERVICE_ROLE_KEY missing; lands in the authed-project alongside the other authed specs.
    • 403 (workspace_member_only) on stranger workspace + cache-control: no-store guard.
    • workspace-export.spec.ts pins GET 200 + required keys (workspace, members, chats, messages, memories, audit_log_30d).
  193. v1.7.130
    M9
    angi workspace export wraps the new portability endpointShipped May 11, 2026

    v1.7.129 shipped /api/workspaces/[id]/export with a download button in /settings/workspace. SDK consumers + ops scripts wanted a CLI mirror that doesnt require a browser. angi workspace export resolves the active workspace (priority: --workspace <id-or-slug> flag, conf-stored workspaceId from angi switch, server-cookie-resolved active_workspace_id from /api/user/me) and either streams the JSON dump to stdout or writes to --out <path>. Slug-form workspace overrides round-trip through /api/user/me memberships to map to a UUID — same pattern as angi switch (v1.7.85). Top-level help + completion table SUB.workspace updated.

    • --workspace <id-or-slug> override resolves slug via /api/user/me, same as `angi switch`.
    • Token-gated — `angi workspace export` without auth fails fast with a `Run \\`angi login\\`` hint.
    • Top-level help block + completion SUB.workspace updated; `angi workspace <TAB>` lists export.
    • `angi workspace export` streams the workspace JSON dump to stdout or to --out <path>.
  194. v1.7.129
    M9
    GDPR data-portability export at /api/workspaces/[id]/exportShipped May 11, 2026

    Plan-doc M8 calls for a SaaS-grade workspace export. Customers (or GDPR data-portability requesters) had no way to grab a copy of their data without an admin pulling SQL by hand. New endpoint /api/workspaces/[id]/export returns a single JSON document — RLS-scoped via serverSupabase, gated on workspace_members. Payload: workspace meta, members, chats, messages (last 5000), memories (last 2000 no embedding bytes), audit_log (last 30 days). Bounded to keep the dump tractable. Content-Disposition embeds the workspace slug + YYYYMMDD in the filename. /settings/workspace gains a new Export workspace data section with bilingual copy.

    • RLS-scoped + members-only gate — strangers get 403 instead of an empty payload.
    • Content-Disposition embeds workspace slug + YYYYMMDD in the filename so the file is self-describing.
    • Bounded: messages capped at 5000, memories at 2000, audit_log to last 30 days — dump stays tractable.
    • /settings/workspace gains an Export workspace data section with bilingual copy.
    • /api/workspaces/[id]/export returns workspace meta + members + chats + messages + memories + audit_log as a single JSON document.
  195. v1.7.128
    M9
    angi chats search <q> cross-chat full-text searchShipped May 11, 2026

    /api/search has indexed every message since the chat-UI batch but the CLI couldnt reach it. Wrap the route. angi chats search "stripe webhook" hits /api/search, sanitises the <mark>-wrapped snippets server-side, then translates them to ANSI bold-yellow for the terminal. Output rows show: chat id (8-char slice), role, chat title, snippet with matched terms highlighted, hit count at the foot. --limit <n> knob (1-100, default 20). --json for jq piping. Empty query fails fast. Completion table SUB.chats grows the search verb.

    • Empty query short-circuits with a clear stderr message — no server round-trip.
    • Completion table SUB.chats grows the `search` verb.
    • --limit <n> (1-100, default 20) + --json for raw jq pipe.
    • `angi chats search <q>` POSTs /api/search and renders highlighted snippets with ANSI bold-yellow.
  196. v1.7.127
    M9
    --archived + --limit + --all flags on chats listShipped May 11, 2026

    angi chats archive <id> (v1.7.91) hid chats from the default list view but offered no way to find them again — operators had to query Supabase directly to confirm an archive landed. The /api/chats GET also hard-coded archived=false + limit=50. Extend the endpoint with ?archived=1 / ?archived=all / ?limit=N knobs (page size capped 1-200). CLI grows three matching flags (--archived, --all, --limit <n>) on chats list. Output marks archived rows with a dim ▽ alongside the pinned ★ glyph. No behavior change for callers that dont pass the new params.

    • /api/chats GET accepts ?archived=1 / ?archived=all and ?limit=N (1-200, default 50).
    • No behavior change for callers omitting the new params — default still archived=false, limit=50.
    • Archived rows in the CLI list view marked with dim ▽ glyph alongside the pinned ★.
    • `angi chats list` gains --archived, --all, --limit flags with help-text examples.
  197. v1.7.126
    M9
    Daily pg_cron purge of system/stripe audit_log >30 daysShipped May 11, 2026

    v1.7.117 + v1.7.118 + v1.7.122 made the release-sync cron and the stripe-webhook write audit_log rows on every tick. The release-sync cron alone fires every 15 min ~ 96 rows/day. Without retention, audit_log grows unbounded. Migration 0025 schedules angi_purge_audit_system daily at 04:48 UTC. Deletes audit_log rows older than 30 days only where action LIKE system.% OR stripe.%. User-action rows (workspace mutations, plan changes, chat creation) are the billing + forensic audit trail — they stay forever. Off-cycle from the other three crons (17 past, 04:23, */15) so the purges dont pile on top of each other.

    • User-action rows stay forever — only high-frequency cron/webhook entries get pruned.
    • DEPLOY.md background-jobs table now lists all four cron jobs.
    • Off-cycle from the other three crons (17 past hourly, 04:23, */15) so purges dont overlap.
    • pg_cron job angi_purge_audit_system runs daily at 04:48 UTC and deletes system/stripe audit_log rows >30 days.
  198. v1.7.125
    M9
    CI build Node bumped 20 -> 22 LTS across workflowsShipped May 11, 2026

    Node 20 LTS ends Apr 2026. v1.7.124 already moved the action runtime to Node 24 (@v5 majors); this batch bumps the Node our build runs under via setup-node from 20 to 22 (current LTS). Workflows touched: ci.yml (3 jobs), release-tag.yml, release-sync.yml, smoke.yml. engines.node in root package.json stays >=20.0.0 so local dev on Node 20 still works during the transition.

    • engines.node stays >=20.0.0 — local dev on Node 20 still works during the transition.
    • Companion to v1.7.124 — that handled the action runtime; this handles the build runtime.
    • setup-node node-version: 20 -> 22 in ci.yml (3 jobs), release-tag.yml, release-sync.yml, smoke.yml.
  199. v1.7.124
    M9
    CI runner action pins bumped from @v4 to @v5 (Node 24-ready)Shipped May 11, 2026

    GitHub deprecated the Node 20 runtime for JavaScript actions. The runner makes Node 24 the default on Jun 2 2026 and removes Node 20 entirely on Sep 16 2026. v4 majors of actions/checkout, actions/setup-node, pnpm/action-setup are all Node 20-based; their v5 majors are Node 24-based. Bump all three across ci.yml, release-tag.yml, release-sync.yml, smoke.yml. This deprecation is unrelated to the node-version: 20 arg we pass to setup-node (thats the Node our build runs under — separate concern).

    • node-version: 20 arg unchanged — separate concern from the action runtime deprecation.
    • actions/checkout @v4 -> @v5 across ci.yml, release-tag.yml, release-sync.yml, smoke.yml.
    • pnpm/action-setup @v4 -> @v5 across all workflows.
    • actions/setup-node @v4 -> @v5 across all workflows.
  200. v1.7.123
    M9
    angi tools describe <name> prints spec + input schemaShipped May 11, 2026

    angi tools list shows the tool catalog; SDK authors writing the input payload by hand needed --filter <name> + --json then a manual jq dive. Add a one-shot. angi tools describe swarm/init looks the tool up in tools/list, prints the name, description, and the input schema as pretty-printed JSON. --json emits the raw entry for jq downstream. Unknown name fails fast with a hint to run angi tools list. rpc.listTools type signature gains inputSchema?: Record<string, unknown> so the new caller picks it up without any. Completion table SUB.tools grows the describe verb.

    • rpc.listTools type signature now exposes inputSchema?: Record<string, unknown>.
    • `angi tools describe <name>` prints tool description + input schema (--json for raw jq pipe).
    • Unknown tool name fails fast with a `Run \\`angi tools list\\`` hint.
    • Completion table SUB.tools grows the describe verb so `angi tools <TAB>` lists both list + describe.