Skip to main content
Synced from the repo — do not edit here

Canonical source: docs/claude/atlas-build-state.md. This page is generated by docs/scripts/sync-handbook.mjs. Edit the source file in the repo; changes appear here on the next build.

Atlas — Build State & Remaining Steps

Snapshot for context-compaction. Last updated 2026-06-23. Companion to docs/claude/copilot.md. Memory: project_atlas_copilot_tier_a.md, project_observation_engine.md.


☠️ TWO DEPLOY LANDMINES (both bit us 2026-07-01, cost real time)

  1. Decorator displacement (AGAIN): inserting a helper def immediately BELOW a decorator block and ABOVE the intended function moves the decorator onto the helper. Refactoring execute_copilot_action (extracting run_copilot_draft above it) put @https_fn.on_call+@error_handler onto the helper → execute_copilot_action lost its decorator → firebase DELETED it from prod (web confirm cards broke). ALWAYS check for a decorator above a function before inserting code above it; after any such edit grep -nE "@https_fn|def <fn>" to confirm alignment.
  2. execute_copilot_action is NOT in main.py's FUNCTION_TARGET dispatch — it only loads via the bottom wildcard from src.functions_ai_agent import *. So firebase deploy --only functions:execute_copilot_action ALONE fails discovery → DELETES it. ALWAYS deploy execute_copilot_action TOGETHER with clinical_agent_sse (which forces functions_ai_agent to load). Same applies to any other functions_ai_agent function not in the FUNCTION_TARGET set.

⚙️ ATLAS-BRAIN DEPLOY GROUP (tool/prompt changes)

Atlas's brain (TOOLS + orchestrate_query + SYSTEM_PROMPT) is shared code bundled into MULTIPLE functions. A tool/prompt change is only live in functions you REDEPLOY. When you change tools/prompt/orchestrator, redeploy ALL of these together or Slack/onboarding lag behind web:

firebase deploy --only functions:clinical_agent_sse,functions:clinical_agent_stream,functions:atlas_slack_worker,functions:heartbeat_worker --force

(Per-clinic = fully automatic; ONE multi-tenant backend serves every clinic, no clinic action ever. This deploy-set is OUR hygiene only. New Slack SCOPES are the only thing that makes clinics re-consent.)

Deployment model (READ FIRST)

  • Backend (Cloud Functions) = LIVE in production. Every firebase deploy --only functions:… this work-stream shipped to prod. Real clinics run on it.
  • Frontend (basisflow-web) = localhost only. All the Atlas UI lives on the dev server + the SSE endpoint. Nothing is on platform hosting yet — that's the pending "publish".
  • The SSE endpoint clinical_agent_sse is an on_request Gen2 fn; the copilot + chat draft-assist call it directly via fetch (Bearer ID token). It must have allUsers invoker (it self-auths inside) — redeploys can drop this; re-grant with gcloud functions add-invoker-policy-binding clinical_agent_sse --gen2 --region=us-central1 --member=allUsers.

What's built & DEPLOYED (backend)

Copilot core (functions_ai_agent.py, functions_orchestrator.py)

  • clinical_agent_sse (SSE streaming, in-request CPU) + orchestrate_query (parallel snapshot+intent classify, model auto-routing, prompt caching, quota gate).
  • Grounding gate: _grounding_judge (Haiku) verifies patient-specific claims vs that turn's DATA tool outputs; emits SSE grounding event → badge + "sources". Excludes action/stage tools (prepare_/save_/create_/set_/update_/send_/execute_) so it doesn't flag operational details. Grounding persisted on the saved message.
  • Context-packet cache: _get_cached_patient_snapshot (150s TTL, process-local) in orchestrator.
  • Draft provenance: every copilot_drafts doc stamped session_id+request_id via _draft_meta() (contextvars set in run_claude_agent; parallel tools use copy_context().run). Frontend pins confirm cards to the originating chat+turn.

Agency tools (all confirm-card gated; writers in _COPILOT_ACTION_WRITERS/_COPILOT_ACTION_PERMS, via execute_copilot_action)

  • book / reschedule / find-slots, assign_protocol (+ catalog reconciliation), update_protocol, save_clinical_note, save_report_to_documents, set_benchmark (+ list_benchmarks), create_heartbeat_task, create/update/enroll sequences, prepare_client_message (editable send-to-client card), prepare_create_task (staff tasks).
  • assign_protocol (in clinic_service) now expands the full supplements[] array + find-or-creates supplements/exercises into the clinic catalog (slug-id dedup) via _reconcile_protocol_catalog.

Proactive observation engine (functions_observations.py, wired into heartbeat_dispatcher)

  • Store: clinicsv2/{clinic}/observations. Deterministic scorer → tier; briefing flag (tier≥2) for the dashboard query; source = template|rule|atlas.
  • Detectors: lab_out_of_range, stale_panel, sleep_trend (templates, toggleable), attention_rule (reads attentionConfig.rules from Settings→Need Attention), Atlas autonomous (run_autonomous_review, Sonnet cross-signal, enqueued to heartbeat_queue → heartbeat_worker, capped 5/scan).
  • Bounded round-robin scan (cursor, 30-min interval). Auto-resolve (_resolve_stale_observations) closes cleared template/rule findings.
  • Settings → Need Attention has a "Default monitoring" toggle section (attentionConfig.defaults.{key}.enabled).

Chat / sectioned-Atlas (functions_chat_option_a.py)

  • Draft-assist: clinical_agent_sse mode:'chat_draft' (_chat_draft_reply).
  • Auto-categorize: hooked into the warm chat_unread_counter trigger (added secrets=['ANTHROPIC_API_KEY']). _classify_chat_message (Haiku) → top-level atlasCategory/atlasUrgency/lastMessageAt/lastMessageFromClient/lastMessageSnippet on chats/{clientId}. Loop-guarded on lastMessageAt + _CAT_VERSION (currently 2). Taxonomy: clinical_question · symptoms · data · fitness · nutrition · refill · scheduling · admin · urgent · general(default). Backfill: tools/backfill_chat_categories.py <clinic> (touches chats → fires trigger; per-clinic only, NOT global).

KB / memory: per-patient clinic_users/{uid}/settings/memory facts (auto-extracted from staff turns now, via Anthropic — Vertex was failing); staff-editable via AtlasMemoryPanel (frontend-direct, rule added). Manual KB items in Settings.

Test tools: tools/trigger_atlas_insight.py <clinic> [uid], tools/backfill_chat_categories.py <clinic>.


2026-06-28 cross-client analytics — slice 1 DEPLOYED. Q: can Atlas answer clinic-wide queries (who cancels most, who hasn't booked in 3mo, cancelled this quarter, adherence<80%, revenue membership vs one-off in April)? Was NO (all tools single-client). VERIFIED data shapes live: cancellations in scheduled (status early_canceled/contains 'cancel', uids[]+start); REVENUE already solved by existing functions_analytics.py::_compute_revenue_analytics (returns byMembership/byProduct/byService/totalsByDate from clinic transactions — payload in nested data); ADHERENCE data EXISTS in users/{uid}/events (eventType, completionStatus, protocolId) — was wrongly thought unplumbed. DECISIONS: hybrid (nightly rollup + live fallback); per-client metrics written ONTO clinic_users/{uid}.analytics (NOT a giant rollup doc — scale) so query_clients uses Firestore range filters; reuse warm heartbeat_dispatcher (24h-gated maybe_run_daily_rollup), no new scheduled fn. BUILT (slice 1): NEW functions_atlas_analytics.py (compute_booking_rollup → clinic_users.analytics {lastBookingAt,bookingCount,cancelCount}; query_clients_rollup) + tools query_clients (min_cancellations/no_booking_days/status) and get_revenue_report (wraps _compute_revenue_analytics, membership vs one-off %) — 4 wirings each (schema/dispatch/perms[query_clients=VIEW_CLINIC_MEMBERS+COPILOT_OPS, revenue=VIEW_REVENUE+COPILOT_OPS]/orchestrator OPS). Hooked rollup into heartbeat_dispatcher. Tested live on axuk-khwf-prkr: 17 clients rolled up, all queries + revenue split (33% membership/66% one-off) work. Slice 1.5 — GENERAL FILTER GRAMMAR (DEPLOYED 2026-06-30). Refactored query_clients from fixed knobs → a general grammar so we add COLUMNS not queries. query_clients_grammar(filters[], sort, aggregate, limit): filters = [{field,op,value}] AND-ed; ops gt/gte/lt/lte/eq/ne/contains; sort {field,dir}; aggregate {op:count|avg|sum, field?, group_by?}. Published FIELD CATALOG in the tool description (Atlas composes from NL): NUMERIC cancelCount,bookingCount,orderCount,revenueToDate(cents),daysSinceLastBooking,daysSinceLastPurchase,daysSinceLastLab(999999=never),tenureDays; STRING status,membershipType; BOOL isMember. Rollup expanded → compute_client_rollup adds orders(LTV: revenueToDate/orderCount/lastPurchaseAt) + lab recency(lastLabAt, needs collectedAt desc index per clinic_users/{uid}/labs — may log index warnings until built) onto clinic_users.analytics. Read engine: bounded scan _MAX_SCAN=5000 + status-eq server-side anchor + in-memory eval; sets truncated flag. Tested live axuk-khwf-prkr: combined filters, count, avg-group-by-membership, top-spenders sort all work. NOTE: group_by membershipType shows raw membership IDs (stores id not name) — cosmetic, map later. No-show NOT a distinct status in data (only none/early_canceled/confirmed) — omitted, not faked. Slice 2 — adherencePct column DONE + DEPLOYED 2026-06-30. Computed in compute_client_rollup per client from users/{uid}/events over last 30d: adherencePct = completed/(completed+missed) of PAST habit occurrences. completionStatus semantics: 'unknown'(past)=missed; {completed,autoCompleted,createCompleted}=done; future occurrences ignored. None when no protocol data → numeric ops SKIP (clients w/o data NOT false-flagged as low-adherence — verified). Added to NUMERIC_FIELDS + record + tool catalog. Tested live: George 14% (real), 'adherence<80' finds him, avg works, 16 no-data clients correctly excluded. Needs start index on users/{uid}/events (likely exists). Original 5 cross-client example queries all now answerable. Catalog DONE for now: cancelCount,bookingCount,orderCount,revenueToDate,daysSinceLastBooking/Purchase/Lab,tenureDays,adherencePct,status,membershipType,isMember. Future columns slot in identically. COSMETIC TODO: map membershipType id→name in group_by output. PLACEHOLDER (was "Slice 2 TODO"): (compute expected-vs-completed from protocol habits recurrence vs users/{uid}/events.completionStatus → clinic_users.analytics.adherencePct; add no_adherence filter to query_clients). Note _read_intake precedent. Don't forget single-field indexes may be needed for analytics.cancelCount / analytics.lastBookingAt range queries at scale.

2026-06-28 get_intake_forms + snapshot include (DEPLOYED, backend live): New Atlas tool reads completed intake/onboarding form ANSWERS from clinicsv2/{clinic}/clinic_users/{uid}/onboardingForms/{formKey}. TWO storage shapes exist and BOTH are handled: answersJson (JSON STRING, current _save_submission_and_pdf pipeline) AND answers (native DICT, older write path — e.g. real user GoOU3d2QcyRZMD83IhbQeIclpwh2 in xdoz-pjao-zxvm). Reader prefers answersJson, falls back to answers-dict. RETROACTIVE — verified live on real data. get_documents only lists the PDF name, NOT its contents; this returns structured Q&A (humanized labels, submitted date, empties skipped, base64/data-URI signatures + >300/500-char blobs collapsed to [provided], optional form_key filter). 4 wirings: TOOLS schema + execute_tool + TOOL_PERMISSION_MAP (VIEW_DOCUMENTS) + orchestrator CLINICAL+RESEARCH. re imported locally in _humanize_field_key. ALSO added _read_intake section to build_patient_snapshot (compact: ≤8 forms, ≤12 answers each, blob-guarded) → observations + chat-drafts now factor intake context. All in functions_orchestrator.py + functions_ai_agent.py.

2026-06-23 dashboard polish: AtlasBriefingCard hover bug fixed (unnamed Tailwind groupgroup/obs named group; ancestor group was leaking group-hover to every row's Discuss). Card now caps at top 5 + "View all (N) →" → new app/(main)/attention/page.tsx (full observations list, status new/seen/snoozed, orderBy score, filters: search / source / type; Discuss+snooze+dismiss). No new index (orderBy score = auto single-field).

What's built (FRONTEND, localhost only — ships on publish)

components/copilot/CopilotDrawer.tsx (SSE client, grounding badge+sources, draft cards pinned by requestId/session, ClientMessageCard with ✓Sent, generic confirm cards with ✓done, token usage meter, AtlasMemoryPanel toggle), AtlasBriefingCard.tsx (dashboard proactive briefing → seeded fresh thread + deep-link), DashboardMessagesCard.tsx (top-5 inbox preview), ChatDrawer.tsx + app/(main)/messages/page.tsx (✦ Draft-with-Atlas + category chips + urgent-first sort), AtlasMemoryPanel.tsx, sendClientChatMessage.ts, Settings→Need Attention default-monitoring toggles.


REMAINING STEPS (in intended order)

  1. Scheduled tasks / crons — ✅ DONE + DEPLOYED 2026-06-23. New Atlas tool schedule_task (confirm-card gated; _exec_schedule_task + _parse_when in functions_ai_agent.py; registered in _COPILOT_ACTION_WRITERS/_COPILOT_ACTION_PERMS, TOOLS schema, execute_tool dispatch, UNIVERSAL_TOOLS, TOOL_PERMISSION_MAP→COPILOT_OPERATIONS). Creates a heartbeat_task with kind (reminder|message|report) + payload + oneShot + schedule.runAt (one-time) or schedule.cron (recurring).

    • Dispatcher (functions_heartbeat.py): passes kind+payload into the queue item; one-shot/runAt-only tasks set enabled=false after firing (no nextRunAt recompute); recurring recompute cron.
    • Worker: _handle_scheduled_kind — reminder → _write_inbox_result (heartbeat_results + push); message → pre-authored text sent to chats/{clientId} (ArrayUnion, role coach) + result, else degrade to reminder (never auto-send unreviewed Atlas content); report/empty kind → existing Atlas-run branch.
    • Results surface in existing components/copilot/HeartbeatInbox.tsx (reads heartbeat_results). LIVE-TESTED: enqueued reminder → worker wrote result, queue done.
    • NOTE: create_heartbeat_task kept as-is for recurring monitoring; schedule_task is the richer one-time/dated tool. Deferred: scheduled_action kind (execute an arbitrary writer); draft-on-date for messages (currently pre-authored auto-send or degrade-to-reminder); push-to-CLIENT on scheduled message (only writes to chat doc now).
  2. Clinic-config knowledge + capability self-awareness — ✅ DONE + DEPLOYED 2026-06-23.

    • Capabilities: added a "WHAT YOU CAN DO" section to SYSTEM_PROMPT (functions_ai_agent.py, after PLATFORM HELP) — understand / analyze / act-with-approval / schedule / watch / configure, + honesty-about-limits. So "what can you do?" answers accurately.
    • Config how-tos: rewrote the PLATFORM_HELP dict entries in _exec_get_platform_help with the REAL flows (mapped from basisflow-web UI): services, memberships, products, billing/Stripe, staff, protocols, forms, settings(+availability/booking params/Need Attention). Fixed stale nav (Services live under Services & Plans, Stripe connects in Settings → Integrations, real staff roles, etc.).
    • Deployed via clinical_agent_sse; public invoker re-verified (204).
  3. PUBLISH to production — ✅ DONE 2026-06-23. Built clean + deployed hosting:platformhttps://basis-hybrid-platform.web.app (platform.basishealth.io). The whole Atlas frontend (copilot SSE, briefing card, inbox card, /attention, chat draft-assist + category chips, capability/help) is now LIVE alongside the already-live backend.

    • Fixed 7 pre-existing build-blocking type errors with type-only casts (no runtime change): dashboard ClinicFulfillmentWidget (clinicId ?? undefined, const u: any), LabsTab (selectedAnalyte as any), PaymentTab clinicId ?? undefined, PlanTab (a as any).anytime, protocols icon: t.icon as any. (No ignoreBuildErrors in next.config — kept type-checking on.)
    • Build: rm -rf .next out && npm run build (output:'export' → out/) → cp -R out/* basis-functions/public-platform/firebase deploy --only hosting:platform.
  4. Self-improvement + ops loops (PLANNED 2026-06-30, decisions locked, NOT built):

    • Knowledge-gap loop (task #30) — SLICE 1 BUILT + DEPLOYED 2026-06-30. NEW functions_atlas_gaps.py: capture_deflection (inline regex _DEFLECTION_RE, NO LLM — wired into clinical_agent_sse after answer save, surface-tagged) → writes clinicsv2/{clinic}/atlas_gaps. maybe_enqueue_weekly_digest (7-day gate, in heartbeat_dispatcher) enqueues type='gap_digest'. heartbeat_worker branch gap_digestclassify_and_digest (has ANTHROPIC_API_KEY): Haiku batch-classify (capability_gap|knowledge_gap|feature_request|appropriate|other) + topic, cluster, write clinicsv2/{clinic}/atlas_gap_digests. Tested live: regex correct (caught 3 deflections, skipped real LDL answer), capture+digest work; classification verified-by-pattern (same httpx+Haiku as the working chat categorizer). SURFACE CLARIFIED 2026-06-30: the "Atlas tells clients to ask team" report is from BASISFLOW-WEB (clinician told George), NOT consumer. basisflow-web has TWO Atlas answer paths — clinical_agent_sse (main copilot, 5 frontend refs) AND clinical_agent_stream (onboarding chat AtlasOnboardingChat.tsx, 2 refs) — capture_deflection now wired into BOTH (surface='copilot'/'onboarding'). Digest now DELIVERS to Slack: classify_and_digest → _post_digest_to_slack DMs OWNER_SLACK_ID via the Support bot's post_slack_message (get_slack_bot_token reads system/slack_config, no secret-scope issue). Did NOT fire a live test (would DM George's real Slack) — delivers on weekly schedule or on manual trigger. REMAINING TODO: (a) frontend digest panel in basisflow-web (optional, Slack covers visibility); (b) closing-the-loop KB self-fill + auto-route bugs to build pipeline (gated on git reconciliation — PARKED, addressing this week).
    • Ops-troubleshooting (task #31) — detect+correlate+notify ONLY (read-only). Sentry/CloudLogging/PostHog error clusters ↔ client-reported issue (chat auto-categorize) → Slack with cause+suggested fix. SAFETY: no autonomous prod code deploys (HIPAA). Future gated tiers: propose-fix-as-PR (build agent), reversible config-only auto-remediate w/ approval. Mostly CONNECTS existing infra (functions_sentry_webhook.py, functions_slack_bot.py triage, build agent, Sourcebot, nightly orchestrator) — verify before building.
    • NOTE: three initiatives now in flight — (a) analytics grammar+column catalog, (b) Slack full push, (c) these two loops. Confirm sequencing with George before starting a new track.

SLACK P1 — install + identity + Settings UI (BACKEND DEPLOYED 2026-06-30; frontend localhost).

  • NEW functions_atlas_slack.py (helpers, lazily imported — no standalone deploy): store_atlas_slack_install (config doc clinicsv2/{clinic}/integrations/slack [staff-readable] + ENCRYPTED token in integrations/slack_secret [server-only, encrypt_message/STRIPE_ENCRYPTION_KEY] + reverse index slackTeams/{teamId}→clinic), get_clinic_slack_token, clinic_id_for_team, slack_config, clinician_uid_by_email (P2 identity).
  • ENHANCED existing deployed slack_oauth_callback (functions_slack_bot.py) — when state=clinic_id, calls store_atlas_slack_install (SECURE) instead of the old plaintext-token-in-clinic-doc write; added STRIPE_ENCRYPTION_KEY to its secrets. Verified public (HTTP 200). Reuses existing SLACK_CLIENT_ID/SECRET (prototype creds).
  • Firestore rules added + DEPLOYED: integrations/slack (staff read, admin create/update for phi toggle+channel), integrations/slack_secret (deny all client), top-level slackTeams (deny all client).
  • Frontend (localhost, ships next publish): components/settings/AtlasSlackCard.tsx in Settings→Integrations — "Add to Slack" (OAuth authorize URL w/ state=clinicId, scopes, redirect=slack_oauth_callback), connection status, default channel, PHI attestation toggle (writes phiEnabled/phiAttestedBy/At). Needs NEXT_PUBLIC_SLACK_CLIENT_ID env set (the app's public client id) for the Connect button.
  • GEORGE'S PREREQ (P0 manual): register the dedicated multi-tenant "Atlas" Slack app (or enable OAuth distribution + add slack_oauth_callback as an allowed redirect URL on the existing app), set NEXT_PUBLIC_SLACK_CLIENT_ID. Until then the install flow can't complete end-to-end. SLACK P2 — inbound ops Q&A (DEPLOYED 2026-07-01, endpoint-verified). In functions_slack_bot.py (reuses verify_slack_signature): NEW atlas_slack_events (on_request, HMAC, 3s ack, dedup on retry header; handles app_mention + DM message.im; resolves team→clinic via slackTeams; enqueues clinicsv2/{clinic}/slack_queue) + NEW atlas_slack_worker (on_document_created slack_queue): _slack_user_email (users.info w/ clinic token) → clinician_uid_by_email → perms(copilot_access) → PHI GATE: if NOT phiEnabled, filter allowed_tools to _ATLAS_NO_PHI_TOOLS (ops/config/revenue only, no patient tools) + system note to link out → orchestrate_query(user_id='') → _post_as_clinic (chat.postMessage w/ decrypted CLINIC token, in-thread). Registered in main.py (both spots). atlas_slack_events granted allUsers invoker; url_verification challenge test passes. SEPARATE from Support bot's slack_events (different app, different workspaces). Full end-to-end needs the Atlas app configured (George's prereq: Event Subscriptions URL → atlas_slack_events, DM/mention scopes, install to a clinic workspace). NEXT: P4 approvals as Slack buttons (P3 ops tools partly done via analytics engine).

ATLAS PROACTIVE EMPLOYEE #32 (DEPLOYED 2026-07-01). Decision: aggressive presence but grounded + back-off (no filler — the Viktor cautionary tale). In functions_atlas_slack.py: (1) onboarding_welcome_text/post_onboarding_welcome — ONE-TIME welcome DM to the installer on OAuth (slack_oauth_callback, gated on welcomeSent flag). (2) push_attention_pings(clinic_id, enc_key) — earned proactive nudge: posts clinic's tier>=3 (push-tier) observations to Slack (defaultChannelId or installer DM), once each (slackPinged flag), PHI-GATED (attested → name+title+recommendation+client link; else generic link-out, no PHI). Called post-scan from heartbeat_dispatcher (added secrets=['STRIPE_ENCRYPTION_KEY']). max 3 pings/clinic/tick. Verified: welcome renders, ping safely no-ops (0) on non-connected clinics. v1 pings are informative + deep-link (interactive [Draft it] buttons = P4). Live test needs Slack app registered + a tier-3 observation. NEXT logical: P4 (approvals as buttons → makes pings + answers actionable), then publish (ships AtlasSlackCard), then P3 ops tools.

SLACK P4 (approvals) + P3 (partial) DEPLOYED 2026-07-01. P4: extracted shared run_copilot_draft core (functions_ai_agent.py) used by BOTH execute_copilot_action (web) AND atlas_slack_interactivity (new on_request, HMAC, allUsers-invoker) — Slack worker posts staged drafts as Block Kit Approve/Discard cards (matched by draft_request_id → _post_draft_cards), click → run_copilot_draft → replace_original message. VERIFIED the two risky assumptions: verify_slack_signature uses RAW body (works for JSON events AND form-encoded interactivity); draft_request_id → run_claude_agent contextvar → stamped on drafts (orchestrator line 999 forwards it). P3: get_orders READ tool DONE+deployed (fulfillment status). P3 REMAINING = invite_client (add_clinic_user) + invite_staff writers (auth-creating writes; do carefully w/ email validation — see task #24). All Slack machinery (install/Q&A/proactive/approvals/orders) now deployed; live e2e still gated on George registering the Atlas app.

  1. After publish (deferred): Slack AI employee (Viktor-style) — PLAN WRITTEN: docs/claude/atlas-slack-plan.md (decisions locked 2026-06-23: PHI hybrid = no-PHI default + per-clinic attestation toggle; surface = operational command center to run the clinic from Slack, channel or DM; shared multi-workspace app). Build is ops-first, 6 phases, includes a tool-gap buildout (orders/fulfillment, invite client, invite staff, clinic reports). Reuses orchestrate_query + execute_copilot_action + observation/heartbeat producers. + growth detectors on the obs engine; client-facing "talk to your record"; continuous-learning loop; observation auto-resolve for atlas insights; lastMessageAt-indexed inbox query (replace per-client getDoc scan in DashboardMessagesCard); deep-link inbox rows to /messages?chat=; dashboard ClinicFulfillmentWidget type fix.

Key gotchas (hard-won)

  • Decorator displacement: inserting a def immediately above a decorated fn silently moves the decorator → fn de-registers → 404 + silent no-op deploys. Verify with gcloud functions describe NAME --gen2 --region=us-central1 or curl; don't trust "Deploy complete".
  • New Gen2 fns start PRIVATE → re-grant allUsers invoker for self-authing public endpoints.
  • Cloud Run throttles CPU after response → don't do LLM work in fire-and-forget daemon threads; run inline within the request (e.g., staff fact-extraction, grounding gate).
  • Big-codebase cold start can fail new on_call healthchecks → prefer extending warm functions/dispatchers/triggers over new standalone callables (e.g., categorize hooked into chat_unread_counter; autonomous review via heartbeat_worker).
  • /messages page and ChatDrawer are separate composers — features must be added to both.
  • Confirm cards: via:'copilot'execute_copilot_action; else → clinic_service. Drafts stamped session_id/request_id; cards pin to the message.