Meeting notes pipeline
The decisions from Alex's constraints, so the puller is built to this and not re-argued.
The constraint that shapes everything: clients must never be able to see notes. So Google sharing stays maximally closed, and the portal is the only sharing surface.
Decisions
No auto-invited accounts. Calendar has no native always-add-guest rule, a group on the invite is visible to clients, and a service account invited as a guest gets nothing — Gemini shares with internal humans. Domain-wide delegation makes the whole question moot: the portal reads without being invited.
Write access is membership, not scope. The service account is drive.readonly across the org (DWD) and a Contributor member of the "Atlas Clients" Shared Drive — a role that can add files and structurally cannot delete or move them. Filing copies the notes doc into [CODE] 05_Admin/Meeting Notes; the original in the note-taker's Drive is never touched.
Client matching is deterministic first. Attendee email domains against client websites, event title against client names. Haiku only breaks ties; unmatched notes file to General rather than being guessed into the wrong client.
Each doc is processed once, ever. Dedupe on Drive file id. Listing is free; extraction runs only on new docs — about $0.22 each, roughly $4.50/month at twenty meetings.
The portal shows the learning, links the detail. Extracted facts live on Knowledge; every source is a pill linking to the original doc. The portal never holds the only copy of a meeting — it holds what was learned, plus the way back.
Completeness and discretion are separate problems
Alex, 23 Aug 2026: *"I do need the brain to know everything about the agency... but in terms of answering people's questions, we want to be sensitive as to who is asking. If we're discussing something about Jil, we shouldn't tell her."*
Storage is total. The filter lives at retrieval. An earlier version withheld sensitive files at ingest, which made the brain ignorant of exactly the material it most needed to reason about. That is reversed: everything under the Shared Drive is read and stored.
Notes that match a client go to content_sources as before. Everything else — syncs, briefings, one-to-ones, interviews, Atlas Homes — goes to agency_notes (0068), which exists rather than making content_sources.client_id nullable: a weekly sync is not a client record, and putting it in the table client reporting reads from is one forgotten WHERE clause away from a client seeing our internal notes.
Three labels, written at ingest because none can be reconstructed later without re-reading everything:
| Label | What it answers |
|---|---|
category | internal · recruitment · atlas_homes |
audience | team or founders — who may be *shown* it |
subjects | who is discussed in it |
subjects is the one that does the real work. Audience alone cannot express the rule that matters: once a user system exists, a manager may legitimately read most internal material and still must not read the part about the person asking.
Audience is strict by default. Anything not identifiable as ordinary team business is founders-only. A wrongly-restricted weekly sync is an inconvenience; a leaked appraisal is not recoverable. Weekly syncs, stand-ups, briefings and kick-offs are team; one-to-ones, employment matters, recruitment and Atlas Homes are founders.
Atlas Homes is a label, not a tenant — Alex: *"it's just a small thing to have here"*.
Atlas Brain is not wired to agency_notes yet, on purpose. It has no concept of who is asking, so connecting it before the retrieval filter exists would publish all of this to anyone with the URL. Ingesting is safe only because the table is unread. The filter is the next thing to build: default-deny audience = 'founders' while there is no user system, then per-asker — filter by audience, then subtract any note where the asker is a subject.
A file is retried until it has an outcome
unmatched and ambiguous are not outcomes, they are the absence of one, so the sweep looks at those files again. Everything else is terminal.
This is not only about clearing a backlog: the call that matched no client in July belongs to a signed client in August, and a "have we seen this file" check can never notice. Twenty real prospect calls sat recorded-as-handled with nothing stored anywhere.
Nothing is deleted to achieve this. The ledger is the record of what we have seen, so a row that finally gets a real outcome is updated in place — every swept_files write is an upsert. agency_notes keeps on conflict do nothing: that row *is* the meeting, and a retry must not write a second copy.
Traps
The dev directory contains the production compose file. docker-compose.yml there is production's; dev is docker-compose.dev.yml, service app-dev. A bare docker compose up in /srv/atlas-portal-dev builds a parallel stack nothing routes to — and a caddy that fights production for port 80. COMPOSE_FILE=docker-compose.dev.yml is now set in that directory's .env so bare commands cannot pick the wrong one. Use scripts/deploy.sh dev regardless.
Sweeping the full history in one call returns nothing. The route's duration limit cuts the request after it has done real work, so the response is empty while the database has changed. Walk it in bounded windows with sinceDays/untilDays.
Built now (works with pasted notes)
meeting_notessource kind;external_urlon sources- "Log meeting notes" on the client Knowledge tab: paste text + optional doc link → extraction
- Source pills on Knowledge linking to the originals
Live since 24 Aug 2026
The GCP service account and DWD grant landed, so the sweep runs across the whole team. All five Drives — GOOGLE_WORKSPACE_USERS was three for months, which silently excluded Nigel and Rayne entirely; a sweep that reports no errors is not the same as a sweep that covers everybody.