Build recap · no resident data

Pilot Monitor

Read-only observability for the 7-day resident pilot, built HIPAA-ready for the 30-day pilot that follows.

Two residents go live on Reachy units for a week. The failure modes that matter are silent — a robot off Wi-Fi at 6am, a resident who couldn't hear the robot all Tuesday, a clinical alert that never reached a human. By the time a dashboard shows it, a pilot day is gone.

This is a Cloud Run Job that reads production Postgres read-only every 30 minutes, posts to a dedicated Slack channel every tick, and recaps at 20:05 CT. It writes nothing to the product database and changes no backend or robot code.

11detectors (cut from 53)
15blockers found by review
25tests, all pinning real regressions
~$4total cost for the pilot

How it works

Cloud Scheduler ─┬─ */30 5-21 * * *      America/Chicago ─┐
                 ├─ 0 22,23,0-4 * * *    America/Chicago ─┼─▶ Cloud Run Job
                 └─ 5 20 * * *           America/Chicago ─┘    (concurrency 1)
                                                                     │
  ┌──────────────────────────────────────────────────────────────────┤
  │ Direct VPC egress — no connector VMs                             │
┌─▼───────────┐                          ┌────────────┐   ┌──────────▼───┐
│ collectors/ │─── Snapshot ────────────▶│ detectors/ │──▶│   notify/    │
│ portal_ro   │                          │  pure fns  │   │ pseudonymous │
└─────────────┘                          └─────┬──────┘   └──────────────┘
┌─────────────┐                                │  one label  ┌────────────┐
│   store/    │◀───────────────────────────────┴────────────▶│   judge/   │
│  GCS JSON   │                                              │ GPT-5.4    │
└─────────────┘                                              └────────────┘

Boundaries. collectors/ is the only module that opens a database connection. detectors/ are pure functions with no I/O. judge/ is the only module that egresses transcript text, and it receives only transcript turns. store/ is the only writer. notify/ is the only module that talks to Slack.

What it watches

SignalRuleSev
Telemetry silent for a robotno frame > 5 min (frames are every 30s)P1
Robot can't hear the residentended conversation, ≥2 robot turns, 0 resident turnsP1
Zero conversations todaynone by 14:00 CT — the pilot's thesis failingP1
Forgotten fact resurfacedlocal similarity vs tombstones, zero egressP1
Monitor blindDB unreachable, RLS denying rows, job crashedP1
Speaker volume broken0, below 40, or unreadableP2
Resident asked robot to repeatLLM label over new turnsP2
Robot health alerts opensurface what the robot already decidedP2
Reminders missed / escalatedmedication adherenceP2
Config drift mid-pilotdeploy/model/prompt stamp changedP2
Reply latency degradedp95 > 2.5sP2

An earlier revision had 53 signals. Three reviewers cut ~42 as noise at two residents, duplicates of a neighbour, or self-comparisons — one compared the robot's telemetry to the robot's own reported config. Everything cut became an unthresholded number in the daily recap instead.

Privacy design

Slack never sees a name, a quote, or a clinical detail

Residents appear as R-A / R-B. A P1 is a pointer, not a payload — it carries severity, pseudonym, and a finding key. The evidence stays behind authentication.

🔴 P1 · R-A · robot offline
   gap_minutes=28
   Finding telemetry_silent:R-A. Runbook: §A1.

This preserves the whole operational value — look now, at R-A — and none of the exposure. Slack ships no BAA on standard plans and retains by default, so week-1 messages are still there in week 5; there is no DROP SCHEMA for a Slack channel.

No verbatim resident speech is ever stored

Evidence is an utterance_id pointer resolved at read time, so engram's 90-day retention, its row-level security, and the ADR 0006 forget-purge all continue to apply to the underlying content automatically. A copied fragment would have escaped all three.

The forgetting paradox, resolved the right way

One detector catches the robot repeating something a resident asked it to forget. The obvious implementation sends the forgotten text to a model so it can look for it — a permanent fail-open on the exact gate ADR 0006 exists to enforce. The resident's own companion may not see that text.

Instead it scores locally against already-calibrated similarity floors, and separately instruments the forget gate's failure causes — partial purge, fail-open flag set in production, tombstone nearing expiry. That catches the failure before a resident hears a forgotten memory, where the model approach caught it only after.

What adversarial review found

Five fresh-context reviewers ran against the plan and then the implementation. They returned 15 blockers. Four were claims stated confidently about this codebase that were simply false.

The monitor would have read zero rows — and reported green

The synthetic-isolation row-level security lives in an Alembic migration, and every policy is role-literal. A new database role has no applicable policy, which Postgres treats as deny-all. Conversations, alerts, and patients would all have returned empty, with no error — while a different code path failed open on unfiltered telemetry.

Three faults aborted every single tick

The driver returns JSONB as a string without a type codec. A column named metadata exists on both joined tables, making the reference ambiguous. And a status value used in a query wasn't a member of its enum at all. All three were reproducible against a real database in under a second — and nothing in a green test suite touched them.

Blindness rendered as a green tick

Blindness findings went through the same confirmation delay as everything else, so the first blind tick posted nothing. Worse, a blind tick returned only "I can't see", which made every real open incident look resolved — Slack would have announced that a still-dead robot had recovered.

The privacy gate was defeated by the most common line in Python

logger.error(exc) skipped scrubbing entirely, because the filter only handled strings and Python stringifies the exception after the filter runs. Containers passed through raw, so a dict carrying a transcript printed verbatim while the credential beside it was correctly redacted — protection exactly inverted from what it promised.

A separate finding worth its own ticket

The admin portal's HIPAA §164.312(b) audit control does not write in production. It issues its INSERT through the read-only query path as a SELECT-only role, so every audit write is permission-denied and swallowed. The table also appears in no migration. The control is believed met and unmet — which is worse than unmet, because it's the control that answers "who read this resident's data" after an incident. This predates the monitor entirely.

Cost

Line itemPilotPer month
Cloud Run Job (~17.6k vCPU-s vs 180k free)$0$0
Cloud Scheduler (3 jobs, 3 free per account)$0–0.07$0–0.30
Direct VPC egress — no connector VMs~$0~$0
Secret Manager, Artifact Registry, GCS~$0.05~$0.30
LLM (one label, empty-delta skip)~$2~$8
Burn-in day + model bake-off~$1
Total~$4~$9

The largest avoided cost never appears on that table. The first design specified a Serverless VPC Access connector to reach the internal-only database — which provisions Compute Engine instances that run 24/7, roughly $14/month, to keep a network path warm for a job that executes for about a minute, 42 times a day. Direct VPC egress does the same work with no compute charge and scales to zero.

Before day 1

One thing stated plainly

@here does not wake anyone. No phone rings by default. Overnight P1s are read at 07:00 unless someone wires a Slack workflow with mobile push. The runbook says so in its opening lines rather than implying an escalation path that doesn't exist.

Deliberately not built