S
Sidekick · architecture & flows
generated 2026-07-01 · reflects the current build

Conversation becomes executable memory

Sidekick listens to professional conversations and turns them into trustworthy, provenance-backed, long-horizon memory — then, once trusted, acts on it through your tools via MCP, behind a confirmation gate. 90% of the system is commodity glue; the memory layer is the entire company.

01 The north-star pipeline

Six stages, but effort is deliberately lopsided. Capture and transcription are rented or thin; the defensible ground is Remember + Connect — the moat.

flowchart LR
  A["🎙 Capture
any sensor"]:::commodity --> B["📝 Understand
STT + diarise"]:::commodity B --> C["🧠 Remember
commitments + graph"]:::moat C --> D["🤝 Connect
people · time · contradictions"]:::moat D --> E["⚡ Suggest
stage behind the gate"]:::yours E --> F["✅ Act
MCP · gated + audited"]:::yours classDef commodity fill:#e8e4da,stroke:#9a968c,color:#232220; classDef moat fill:#0e7c76,stroke:#0a5d58,color:#ffffff; classDef yours fill:#5b4bd6,stroke:#4a3db0,color:#ffffff;
commodity — buy / thin the moat — build (the company) yours — the loop & the gate
P1
The memory layer is the product. Everything else is rented or stubbed.
P2
pgvector is an index, not the memory. A vector hit never asserts a commitment.
P3
Every memory links to its source utterance + audio offset. No provenance ⇒ it's a question.
P4
Commitments are first-class, stateful objects. A wrong "you promised" kills the product.
P5
Suggest before Act; the gate is architecture. Nothing reaches a tool un-gated.
P6
Build the loop, keep MCP. Bounded action space (~dozen types).
P7
Privacy/consent is a design constraint, in the schema — not bolted on.
P8
Capture is throwaway; the sensor is interchangeable.

02 System architecture

A TypeScript monorepo (npm workspaces) over Postgres + pgvector, with a native Swift capture client and thin local MCP tool-servers. Offline-first: whisper.cpp STT, sherpa-onnx diarisation, Ollama for the LLM.

flowchart TB
  subgraph SENSORS["Sensors · P8 (interchangeable)"]
    IOS["apps/capture-ios
Swift · record → encrypt → ship"]:::commodity MIC["room mic · manual upload"]:::commodity end subgraph WEB["packages/web · Vite + React"] DASH["Dashboard
Memory · Review · Capabilities · Activity"]:::yours end subgraph API["packages/api · Fastify (read layer · planner · gate · audit)"] CAP["POST /v1/captures"]:::yours AUTH["auth · per-request owner ctx"]:::scale READ["/v1/meetings · /recall · /commitments · /audio"]:::yours PAR["/v1/capabilities · /activity · /proposals"]:::act end subgraph MEM["packages/memory · THE MOAT"] STT["STT seam
whisper · deepgram · assemblyai"]:::commodity DIA["diarise · sherpa-onnx"]:::commodity EXT["extract → commitments + questions"]:::moat IDN["identity · temporal · 3-modality recall"]:::moat EMB["embeddings · pgvector"]:::moat PA["PA: capability switches · email triage/answer/trust · inbox loop"]:::act GATE["executor · autonomy · MCP client"]:::act end subgraph DATA["Postgres 16 + pgvector · system of record"] DB[("schema.sql + gate.sql
RLS per owner")]:::data BLOB[("BlobStore · AES-256-GCM audio")]:::data end subgraph TOOLS["tools/ · maintained MCP servers"] MAIL["mcp-email · drafts"]:::commodity GMAIL["Gmail MCP · read + send (OAuth)"]:::commodity GH["GitHub / Linear / Calendar / LinkedInPoster"]:::commodity end IOS --> CAP MIC --> CAP DASH -->|Bearer token| API CAP --> STT --> DIA --> EXT --> IDN --> EMB READ --> DB EXT --> DB PA --> GATE GATE -->|@modelcontextprotocol/sdk| MAIL GATE --> GMAIL GATE --> GH PA --> GMAIL CAP --> BLOB API --> DB classDef commodity fill:#efece4,stroke:#9a968c,color:#232220; classDef moat fill:#0e7c76,stroke:#0a5d58,color:#ffffff; classDef yours fill:#1f6f68,stroke:#0a5d58,color:#ffffff; classDef act fill:#5b4bd6,stroke:#4a3db0,color:#ffffff; classDef scale fill:#8a6d1f,stroke:#6e561a,color:#ffffff; classDef data fill:#2b2a27,stroke:#000,color:#ffffff;

03 Capture → Memory

The throwaway sensor funnels through one authenticated contract; STT + diarisation produce provenance-grade utterances; the LLM extracts commitments — under-asserting anything it can't ground.

sequenceDiagram
  autonumber
  participant S as Sensor (iOS / mic)
  participant API as API · POST /v1/captures
  participant B as BlobStore (AES-256-GCM)
  participant STT as STT + diarise
  participant DB as Postgres (RLS)
  participant LLM as LLM extract
  S->>API: multipart audio · capture_id · sensor · Bearer
  API->>B: encrypt + store (per-owner namespace)
  API->>STT: transcribe (word timestamps + speakers)
  STT-->>DB: utterances (ms offsets = provenance)
  Note over API,DB: P7 consent — a non-consenting speaker is RETAINED but never processed
  API->>LLM: extract over consenting utterances
  LLM-->>DB: commitments (facts) + commitment_questions
  Note over DB: P3 — every fact links its source utterance + audio offset
  

04 The trust contract

The single most important rule: a claim that can't render its provenance chain is surfaced as a question, not asserted as a fact. Facts and questions live in separate tables. This is what makes the recall trustworthy enough to act on.

flowchart TD
  U["utterance
(text + audio offset)"] --> EX{"extract · validate · gate"} EX -->|"grounded + confident (P4)"| F["commitment — FACT
debtor → creditor · modality · status · confidence"]:::fact EX -->|"ungrounded / low-confidence"| Q["commitment_question
surfaced, never asserted"]:::q EX -->|"small talk / vague"| X["dropped"]:::drop F --> P["▶ provenance — play the exact 8 seconds"]:::prov Q --> P classDef fact fill:#0e7c76,stroke:#0a5d58,color:#fff; classDef q fill:#b9791a,stroke:#8a5b14,color:#fff; classDef drop fill:#efece4,stroke:#9a968c,color:#232220; classDef prov fill:#fff,stroke:#0e7c76,color:#0e7c76;

05 The memory layer — three-modality recall

One question, routed across three lookup paths. pgvector is one path of three — never the asserting one (P2).

flowchart LR
  Q["question
'what did Simon commit to?'"] --> R{route} R -->|who owes what| REL["relational
SQL over commitments + entities"]:::moat R -->|what changed| TMP["temporal
supersession chains"]:::moat R -->|sounds like this| FUZ["fuzzy
pgvector → rows"]:::idx REL --> A["answer + ▶ provenance"]:::prov TMP --> A FUZ --> A classDef moat fill:#0e7c76,stroke:#0a5d58,color:#fff; classDef idx fill:#1f6f68,stroke:#0a5d58,color:#fff; classDef prov fill:#fff,stroke:#0e7c76,color:#0e7c76;

Supporting subsystems: identity resolution (speakers → entities, human-in-the-loop, MEM-2), temporal (supersede + contradiction detection, MEM-3), embeddings (Ollama / OpenAI, MEM-6).

06 The gate — Suggest → Act

Every external mutation flows proposed_actions → executor → audit (P5). The executor is the only thing that touches a real tool, and it refuses while SUGGEST_ONLY is on — the kill switch.

flowchart LR
  C["commitment"] --> PL["planner"]
  PL --> ST["proposed_actions
status = pending"]:::gate ST -->|reject| RJ["rejected"]:::drop ST -->|approve / auto-approve| AP["approved"]:::gate AP --> KS{"SUGGEST_ONLY ?"} KS -->|"true · kill switch"| HOLD["held — nothing dispatched"]:::drop KS -->|false| EX["executor → MCP server"]:::act EX --> TOOL[("Gmail · GitHub · Calendar · LinkedInPoster")]:::data EX --> AU["audit_log · before/after · reversible (undo)"]:::gate classDef gate fill:#b9791a,stroke:#8a5b14,color:#fff; classDef act fill:#5b4bd6,stroke:#4a3db0,color:#fff; classDef drop fill:#efece4,stroke:#9a968c,color:#232220; classDef data fill:#2b2a27,stroke:#000,color:#fff;

The action catalog — artifacts, reversibility-tiered

TierClassesGate behaviour
0 · readcheck_email · read_calendarcontext only — never asserts a commitment (P2)
1 · artifactdraft_email · answer_email · sort_email · propose_meeting · create_reminder · draft_linkedin_post · stage_issuereversible — auto-approve eligible once proven
2 · outwardsend_email · send_reply · send_meeting_invite · publish_linkedin_postirreversible — always confirm; never auto without trust

07 Per-capability autonomy (PA-1)

The autonomy ladder is a user-facing switch per capability: Off / Suggest / Auto. Two invariants keep "Auto" safe: Auto = auto-approve, never gate-bypass; and the switch is a ceiling — a per-item trust gate can only lower it.

flowchart TD
  SW["capability_settings
(per tenant)"] --> M{mode} M -->|off| SKIP["skip — not staged"]:::drop M -->|suggest| PEND["stage · pending"]:::gate M -->|auto| K{"SUGGEST_ONLY on?"} K -->|"yes (Inv-1)"| PEND K -->|no| RV{reversible?} RV -->|"yes (artifact)"| TD{"trust = deny?"} RV -->|"no (outward)"| TA{"trust = allow?"} TD -->|no| AA["auto_approve"]:::act TD -->|"yes (Inv-2)"| PEND TA -->|"yes · trusted + low-stakes"| AA TA -->|"no (Inv-2)"| PEND classDef act fill:#0e7c76,stroke:#0a5d58,color:#fff; classDef gate fill:#b9791a,stroke:#8a5b14,color:#fff; classDef drop fill:#efece4,stroke:#9a968c,color:#232220;

08 The email PA — inbox → stage loop

The capability that makes the switches do something. npm run pa -- loop reads the inbox (Gmail MCP), triages it, drafts memory-grounded replies, and stages send_reply through the gate — where the switch + the trust classifier decide. An ungroundable reply is never auto-sent.

flowchart TD
  IN["EmailSource.listInbox
Gmail MCP / fixture"]:::commodity --> TR["triageInbox · PA-2"]:::act TR -->|"needs_action"| TH["getThread"] TR -->|"newsletter / fyi / spam"| IGN["labelled, no reply"]:::drop TH --> DR["draftReply · PA-3.1
ground in memory (who · what's owed · what's settled)"]:::moat DR --> CF{confident?} CF -->|"no (P3/P4)"| DRAFT["force draft · pending
never auto-sent"]:::gate CF -->|yes| TG["emailTrustGate · PA-3.2
known contact? low-stakes?"]:::act TG --> DEC["decideForAction · send_reply switch"] DEC -->|"auto + trusted + routine"| SEND["auto-approve → queued to send"]:::sendc DEC -->|"new / sensitive / dense"| DRAFT DEC -->|"off"| SKIP["skip"]:::drop classDef commodity fill:#efece4,stroke:#9a968c,color:#232220; classDef moat fill:#0e7c76,stroke:#0a5d58,color:#fff; classDef act fill:#5b4bd6,stroke:#4a3db0,color:#fff; classDef gate fill:#b9791a,stroke:#8a5b14,color:#fff; classDef sendc fill:#1f6f68,stroke:#0a5d58,color:#fff; classDef drop fill:#efece4,stroke:#9a968c,color:#232220;

09 Multi-tenancy & isolation (SCALE-1)

Isolation is enforced twice: the app scopes every query by owner_id, and Postgres row-level security backstops it. The runtime drops to an unprivileged role per connection, so a missing WHERE still can't leak across tenants. Unscoped ⇒ 0 rows (fails closed).

sequenceDiagram
  autonumber
  participant C as Client (Bearer)
  participant API as API
  participant ALS as runWithOwner (AsyncLocalStorage)
  participant PG as Postgres
  C->>API: request + session token
  API->>API: resolve token → user_id (sha256 at rest)
  API->>ALS: runWithOwner(user_id)
  ALS->>PG: SET app.user_id · SET ROLE sidekick_app
  PG-->>API: RLS-filtered rows (owner only)
  Note over PG: superuser/owner bypass RLS — so the runtime never connects as one
  Note over PG: unscoped connection → 0 rows (fails closed)
  

10 Data model (system of record)

Postgres is the substrate (P2). Every table carries owner_id + RLS. Facts and questions are distinct; embeddings are a secondary index keyed by subject_table/subject_id.

erDiagram
  users ||--o{ meetings : owns
  users ||--o{ capability_settings : configures
  users ||--o{ retention_policy : sets
  meetings ||--o{ utterances : has
  utterances ||--o{ commitments : grounds
  utterances ||--o{ commitment_questions : grounds
  entities ||--o{ entity_aliases : "known as"
  entities ||--o{ commitments : "debtor / creditor"
  commitments ||--o{ proposed_actions : triggers
  proposed_actions ||--o{ audit_log : audited
  embeddings }o--|| utterances : "subject (polymorphic)"
  users {
    uuid id PK
    text email
    text role "member | admin"
  }
  commitments {
    uuid id PK
    text debtor
    text creditor
    text modality "firm|tentative|hypothetical"
    text status "open|superseded|done"
    float confidence
    uuid owner_id
  }
  proposed_actions {
    uuid id PK
    text type
    jsonb payload
    text status "pending|approved|executed|failed"
    uuid owner_id
  }
  capability_settings {
    uuid owner_id PK
    text capability PK
    text mode "off|suggest|auto"
  }
  

11 Build status

The phased build (0→4) is structurally complete and gate-green; the PA layer (autonomy switches + email brain + live connector) sits on top. What remains needs the real world, not more code.

LayerStateNotes
Phase 0 · CapturebuiltiOS client (CAP-1) · backend ingest · whisper STT live; STT vendor verdict (DEC-1) open
Phase 1 · Memory (moat)builtextraction · trust contract · identity · temporal · 3-modality recall · embeddings — precision gate (MEM-1.6) awaits a real labelled corpus
Phase 2 · Suggestbuiltplanner → gate → §8 demo runs (stages, never sends)
Phase 3 · Actbuiltexecutor + real MCP draft_email (reversible) behind the kill switch · autonomy ladder · expanded action catalog
Phase 4 · Scalebuiltmulti-tenant RLS · consent enforcement · GDPR governance · repeatable onboarding · wedge profiles
PA · the assistantbuiltPA-1 switches · PA-2/3 email triage/answer/trust · PA-4 dashboard · PA-5 Gmail connector + inbox→stage loop
Go-liveneeds youa Gmail MCP server + your OAuth · flip EMAIL_SOURCE=gmail-mcp + SUGGEST_ONLY=false
QualityCIvitest · eslint · GitHub Actions (INF-3) · per-layer verify gates

Canonical sources: sidekick-architecture.md · MASTER-PLAN.md · TODO.md · schema.sql · gate.sql