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.
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.
| Signal | Rule | Sev |
|---|---|---|
| Telemetry silent for a robot | no frame > 5 min (frames are every 30s) | P1 |
| Robot can't hear the resident | ended conversation, ≥2 robot turns, 0 resident turns | P1 |
| Zero conversations today | none by 14:00 CT — the pilot's thesis failing | P1 |
| Forgotten fact resurfaced | local similarity vs tombstones, zero egress | P1 |
| Monitor blind | DB unreachable, RLS denying rows, job crashed | P1 |
| Speaker volume broken | 0, below 40, or unreadable | P2 |
| Resident asked robot to repeat | LLM label over new turns | P2 |
| Robot health alerts open | surface what the robot already decided | P2 |
| Reminders missed / escalated | medication adherence | P2 |
| Config drift mid-pilot | deploy/model/prompt stamp changed | P2 |
| Reply latency degraded | p95 > 2.5s | P2 |
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.
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.
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.
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.
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 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.
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 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.
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.
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.
| Line item | Pilot | Per 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.
@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.