openapi: 3.1.0
info:
  title: Throughline API
  version: "1.0"
  description: |
    The portable-self store. Everything is Bearer-authenticated under /selves/{self}/…
    Hosts SHOULD declare provenance via x-throughline-source and x-throughline-model headers —
    that powers conformance telemetry and the fidelity gate (weak substrates lose auto-save).
    Errors are always JSON {"error": string} with stable, prefix-matchable messages
    ("invalid event:", "unknown self:", "limit:", "rate limited", "inference failed:").
    Format spec: https://getthroughline.ai/spec
servers:
  - url: https://getthroughline.ai
security:
  - bearer: []
paths:
  /selves:
    get: { summary: List selves, responses: { "200": { description: "{ selves: string[] }" } } }
  /config:
    get: { summary: "Account config (default_self, paused)", responses: { "200": { description: Config } } }
    post:
      summary: Update config — default_self must name an existing self
      requestBody: { content: { application/json: { schema: { type: object, properties: { default_self: { type: string }, paused: { type: boolean } } } } } }
      responses: { "200": { description: Config }, "400": { description: "unknown self" } }
  /selves/{self}:
    parameters: [{ name: self, in: path, required: true, schema: { type: string, pattern: "^[A-Za-z0-9._-]{1,40}$" } }]
    post: { summary: Create a self (first one becomes the default), responses: { "200": { description: "{ name, default }" }, "400": { description: invalid name }, "402": { description: plan limit } } }
    delete: { summary: Delete a self and ALL its data (permanent — export first), responses: { "200": { description: "{ deleted }" } } }
  /selves/{self}/journal:
    post:
      summary: "Journal — the raw prose inlet (no schema, no evidence; reflection distills later)"
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [content], properties: { content: { type: string }, ts: { type: string, format: date-time } } } } } }
      responses: { "200": { description: "{ id, deduped, redacted?, reflection_due? } — reflection_due rides the write that tips due-ness" } }
  /selves/{self}/import:
    post:
      summary: "Cold-start seed: paste another AI's memory list, one line each (max 500/call)"
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [text], properties: { text: { type: string } } } } } }
      responses: { "200": { description: "{ imported, deduped, skipped, capped?, reflection_due? }" } }
  /selves/{self}/context:
    get:
      summary: "The rendered self (persona + rules + salient memories) — bounded, never grows with log length"
      parameters: [{ name: mode, in: query, schema: { type: string, enum: [full, companion, work] } }]
      responses: { "200": { description: "text/markdown context pack" } }
  /selves/{self}/bootstrap:
    get:
      summary: "One call at session start: context + reflection status + governance + pending count"
      parameters: [{ name: mode, in: query, schema: { type: string, enum: [full, companion, work] } }]
      responses: { "200": { description: Bootstrap } }
  /selves/{self}/recall:
    get:
      summary: "Search memory. Relationship streams rank by relevance+recency+salience; governed streams stay deterministic. Whole-query verbatim hits always sort first."
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: k, in: query, schema: { type: integer, default: 8 } }
        - { name: stream, in: query, schema: { type: string } }
        - { name: since, in: query, schema: { type: string, format: date-time } }
        - { name: until, in: query, schema: { type: string, format: date-time } }
      responses: { "200": { description: "{ events: Event[] }" } }
  /selves/{self}/coverage:
    get:
      summary: "Computed honesty: how well does the self actually know a topic? Literal subject grounding — semantic similarity ranks, never admits."
      parameters: [{ name: q, in: query, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ level: none|thin|partial|strong, count, grounded, newestAgeDays, conflicted, basis }" } }
  /selves/{self}/capture/propose:
    post:
      summary: "Tiered capture: observational streams auto-save (retractable); behavior-shaping streams stage for confirmation; weak models stage everything"
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [events], properties: { events: { type: array, items: { $ref: "#/components/schemas/EventInput" } }, source: { type: string }, strict: { type: boolean } } } } } }
      responses: { "200": { description: "{ saved, staged, rejected, deduped }" } }
  /selves/{self}/capture/pending:
    get: { summary: Staged candidates awaiting confirmation, responses: { "200": { description: "{ pending: Candidate[] }" } } }
  /selves/{self}/capture/confirm:
    post:
      summary: Write one staged candidate into the permanent log
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } } } } } }
      responses: { "200": { description: "{ event }" }, "404": { description: unknown id } }
  /selves/{self}/capture/reject:
    post:
      summary: Discard a staged candidate
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } } } } } }
      responses: { "200": { description: "{ rejected }" } }
  /selves/{self}/capture/retract:
    post:
      summary: "Delete a wrong memory outright, with its derived traces (salience, embeddings, dangling relations) — the undo that makes auto-save safe"
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [id], properties: { id: { type: string } } } } } }
      responses: { "200": { description: "{ retracted }" }, "404": { description: unknown id } }
  /selves/{self}/reflect:
    get: { summary: "The raw journal batch due for distillation + cursor", responses: { "200": { description: "{ count, cursor, raw, guidance }" } } }
  /selves/{self}/reflect/complete:
    post:
      summary: Advance the distillation watermark after the host distilled and the user confirmed
      requestBody: { required: true, content: { application/json: { schema: { type: object, required: [cursor], properties: { cursor: { type: integer } } } } } }
      responses: { "200": { description: "{ ok, distilled_to }" } }
  /selves/{self}/events:
    get:
      summary: Sync pull — events after a cursor id, or the whole log
      parameters: [{ name: since, in: query, schema: { type: string } }]
      responses: { "200": { description: "{ events: Event[] }" } }
    post:
      summary: Direct append (validated + discipline-checked; content-addressed dedup)
      requestBody: { required: true, content: { application/json: { schema: { oneOf: [{ $ref: "#/components/schemas/EventInput" }, { type: array, items: { $ref: "#/components/schemas/EventInput" } }] } } } }
      responses: { "200": { description: "{ appended, deduped, ids }" }, "400": { description: "invalid event: …" }, "402": { description: "limit: …" } }
  /selves/{self}/export:
    get: { summary: "NDJSON export ending in a hash-chain manifest — verify offline; ids are content hashes, the chain locks the sequence", responses: { "200": { description: application/x-ndjson } } }
  /selves/{self}/archive:
    get: { summary: "Full archive: export + salience weighting (what it remembers MOST moves with it)", responses: { "200": { description: archive NDJSON } } }
    post: { summary: Restore an archive (idempotent — content addressing makes re-import safe), responses: { "200": { description: "{ imported }" } } }
  /selves/{self}/eval:
    get: { summary: "Fidelity probes: can the self find its own memories from their own cues? Misses reported, never hidden", responses: { "200": { description: probe results } } }
  /selves/{self}/conformance:
    get: { summary: "Which hosts truly adopt this self, and which just talk (per source × model)", responses: { "200": { description: conformance table } } }
  /selves/{self}/provenance:
    get:
      summary: Who wrote a memory (host + model + when)
      parameters: [{ name: id, in: query, required: true, schema: { type: string } }]
      responses: { "200": { description: "{ id, source, model, created_at }" } }
  /account:
    delete: { summary: "Total, immediate erasure of everything (export first — this is final)", responses: { "200": { description: "{ deleted }" } } }
components:
  securitySchemes:
    bearer: { type: http, scheme: bearer, description: "API key from the dashboard (Key & data). Rotating the key kills every old copy instantly." }
  schemas:
    EventInput:
      type: object
      required: [self, stream, type, ts, body]
      properties:
        self: { type: string }
        stream: { type: string, description: "see the stream registry in the spec" }
        type: { type: string }
        ts: { type: string, format: date-time }
        body: { type: object }
        evidence: { type: array, items: { type: string }, description: "required non-empty for proposed events except journal/self-state" }
        supersedes: { type: [string, "null"], description: "id of the row this one retires (history stays)" }
    Event:
      allOf:
        - $ref: "#/components/schemas/EventInput"
        - type: object
          properties:
            id: { type: string, description: "evt_<sha256 of canonical envelope> — content-addressed" }
    Error:
      type: object
      required: [error]
      properties: { error: { type: string } }
