{
  "openapi": "3.0.3",
  "info": {
    "title": "MythOS v3 Internal API",
    "description": "Agent-facing API for MythOS memo library operations, community browsing, embedding, and chat. Key-authenticated writes (POST, PUT, PATCH, DELETE) require an x-mythos-context header carrying the contextSessionId from GET /api/internal/augmentation; without one they answer 428 GET_CONTEXT_REQUIRED.",
    "version": "1.1.0"
  },
  "servers": [
    {
      "url": "https://mythos.one",
      "description": "MythOS v3"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-mythos-key",
        "description": "Internal API key for agent authentication"
      },
      "InternalServiceKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-internal-key",
        "description": "Service credential; not an account mtk_ key."
      },
      "DripWorkerKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-drip-secret"
      },
      "SessionBearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Human session JWT; account API keys are not equivalent."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "success": {
            "type": "boolean",
            "example": false
          },
          "error": {
            "type": "string"
          }
        },
        "required": [
          "success",
          "error"
        ]
      },
      "NewsletterSubscriber": {
        "type": "object",
        "properties": {
          "subscriptionId": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "firstName": {
            "type": "string",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "nullable": true
          },
          "company": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "location": {
            "type": "string",
            "nullable": true
          },
          "customFields": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "pending",
              "unsubscribed"
            ]
          },
          "frequency": {
            "type": "string",
            "enum": [
              "instant",
              "daily",
              "weekly",
              "monthly",
              "none"
            ]
          },
          "digestMode": {
            "type": "string",
            "enum": [
              "bundled",
              "separate"
            ]
          },
          "scope": {
            "type": "string",
            "enum": [
              "creator",
              "tag"
            ]
          },
          "topics": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                }
              }
            }
          },
          "subscribedAt": {
            "type": "integer"
          },
          "unsubscribedAt": {
            "type": "integer",
            "nullable": true
          },
          "lastDigestSentAt": {
            "type": "integer",
            "nullable": true
          }
        }
      },
      "MemoSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "snippet": {
            "type": "string"
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "link",
              "private",
              "hidden"
            ]
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "collaboratorTags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "author": {
            "type": "object",
            "properties": {
              "username": {
                "type": "string"
              },
              "displayName": {
                "type": "string"
              }
            }
          },
          "createdAt": {
            "type": "number",
            "nullable": true
          },
          "createdAtIso": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updatedAt": {
            "type": "number",
            "nullable": true
          },
          "updatedAtIso": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "content": {
            "type": "string"
          },
          "collaboratorContent": {
            "type": "string"
          },
          "seoTitle": {
            "type": "string",
            "nullable": true,
            "maxLength": 70
          },
          "seoDescription": {
            "type": "string",
            "nullable": true,
            "maxLength": 160
          }
        }
      },
      "MemoInput": {
        "type": "object",
        "required": [
          "title"
        ],
        "properties": {
          "memoType": {
            "type": "string",
            "enum": [
              "loop"
            ],
            "description": "Staff-only while Loops is unreleased. Creates the memo already on /loops, in the caller's own library only, private unless visibility is passed."
          },
          "loopTemplate": {
            "type": "object",
            "description": "With memoType loop and no content: compose the body from a starting template, as the surface's create form does. The title becomes the /command; Status is draft.",
            "required": [
              "templateId"
            ],
            "properties": {
              "templateId": {
                "type": "string",
                "enum": [
                  "queue",
                  "sweep",
                  "digest",
                  "review",
                  "prodtest",
                  "blank"
                ]
              },
              "triggerKind": {
                "type": "string",
                "enum": [
                  "command",
                  "schedule",
                  "queue",
                  "none"
                ],
                "default": "command"
              },
              "summary": {
                "type": "string",
                "description": "One line."
              }
            }
          },
          "title": {
            "type": "string",
            "description": "Memo title"
          },
          "content": {
            "type": "string",
            "description": "Memo content in markdown. Hashtag chips are serialized as [#name](/tag/name) — always write that explicit form. A bare #word is auto-linked into a chip unless sourceMarkupVersion: 2 is passed."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags for the memo. If omitted, tags are extracted from content."
          },
          "visibility": {
            "type": "string",
            "enum": [
              "public",
              "link",
              "private"
            ],
            "default": "link",
            "description": "Memo visibility level"
          },
          "collaboratorContent": {
            "type": "string",
            "description": "Notes/collaborator section content displayed separately below the memo body"
          },
          "seoTitle": {
            "type": "string",
            "nullable": true,
            "maxLength": 70,
            "description": "Optional <title>/og:title override for public memo pages"
          },
          "seoDescription": {
            "type": "string",
            "nullable": true,
            "maxLength": 160,
            "description": "Optional <meta name=\"description\">/og:description override"
          },
          "sourceMarkupVersion": {
            "type": "integer",
            "description": "Optional. Markup version of the content being sent. Omit (or 1) for legacy behavior — bare #word is auto-linked into a tag. Set to 2 when the content is already v2 markdown where bare #word is literal text and tags are written as [#name](/tag/name)."
          }
        }
      },
      "Memo": {
        "type": "object",
        "properties": {
          "_id": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "visibility": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "properties": {
              "username": {
                "type": "string"
              },
              "uid": {
                "type": "string"
              },
              "displayName": {
                "type": "string"
              }
            }
          },
          "header": {
            "type": "object",
            "properties": {
              "createdAt": {
                "type": "number"
              },
              "updatedAt": {
                "type": "number"
              },
              "title": {
                "type": "string"
              }
            }
          },
          "content": {
            "type": "string"
          },
          "collaboratorContent": {
            "type": "string"
          },
          "contentVersion": {
            "type": "integer"
          },
          "seoTitle": {
            "type": "string",
            "nullable": true,
            "maxLength": 70
          },
          "seoDescription": {
            "type": "string",
            "nullable": true,
            "maxLength": 160
          }
        }
      },
      "CommunitySummary": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "description": "Community URL identifier"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "memberCount": {
            "type": "integer"
          },
          "postCount": {
            "type": "integer"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CommunityPost": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string"
          },
          "snippet": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "author": {
            "type": "object",
            "properties": {
              "displayName": {
                "type": "string"
              },
              "username": {
                "type": "string"
              }
            }
          },
          "score": {
            "type": "integer"
          },
          "submittedAt": {
            "type": "number"
          },
          "postSlug": {
            "type": "string"
          }
        }
      }
    }
  },
  "paths": {
    "/api/internal/channels": {
      "get": {
        "operationId": "listChannels",
        "summary": "List the owner's channels",
        "description": "Owner-scoped, staff-gated. Each channel carries unread (agent/loop top-level posts since the last read) and openAsks (unresolved asks). Archived channels are left out unless includeArchived=1. shared[] lists the channels others shared with the caller, each named by its key (@<channelId>), with the caller's own unread and notification level; a loop run's key gets none.",
        "parameters": [
          {
            "name": "includeArchived",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "1 adds archived channels, each with its archivedAt."
          }
        ],
        "responses": {
          "200": {
            "description": "success, channels[] with slug, section, topic, private, loopMemoIds, archivedAt, unread, openAsks, lastReadAt, and shared[] with key, id, slug, topic, owner, unread, unreadImportant, lastReadAt, notifications."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "createChannel",
        "summary": "Create a channel, or return the existing one",
        "description": "Idempotent on slug: a repeat returns the existing channel unchanged and never overwrites topic or section. Posting to a missing channel also creates it. The slug search is reserved: creating it, or a first post that would, is refused (400 RESERVED_CHANNEL_SLUG); a search channel made before the reservation keeps working.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "slug"
                ],
                "properties": {
                  "slug": {
                    "type": "string"
                  },
                  "section": {
                    "type": "string"
                  },
                  "topic": {
                    "type": "string"
                  },
                  "private": {
                    "type": "boolean"
                  },
                  "loopMemoIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Loops that post here; a gate's Needs-you message goes to the first channel naming its loop, else #loops."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Already existed; returned unchanged."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/search": {
      "get": {
        "operationId": "searchChannelMessages",
        "summary": "Search the owner's channel messages",
        "description": "Owner-scoped, staff-gated, read-only. Messages and thread replies whose title or text contains q (literal, case-insensitive), newest first, at most 40 — across every channel, or only the one channel names. The same search and cap as the web's channel search; a run key may call it.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Text to find; the first 200 characters are used. Blank or missing is 400 INVALID_QUERY."
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Search only this channel slug; a leading # (URL-encoded) is ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "success and messages[], each as the channel messages route returns it (id, channel, author, title, text, threadId, ...)."
          },
          "400": {
            "description": "INVALID_QUERY or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}": {
      "get": {
        "operationId": "readChannelDetails",
        "summary": "Channel Details: a channel and the loops that post to it",
        "description": "The channel, archived or not: slug, section, topic, private, loopMemoIds, archivedAt, createdAt and updatedAt, the lock token for settings and archive. loops[] lists the owner's loops that post here, each { memoId, title, alertChannel, via[] }: alert (its Policy Alert channel names this channel), default (it names none and this is #loop-health), listed (the channel's loopMemoIds name it, so its gates' Needs-you post here). A loop run's key may read it.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "success, channel and loops."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateChannelSettings",
        "summary": "Change a channel's topic, section or private flag",
        "description": "REQUIRES expectedUpdatedAt from the channel as last read; the CAS is in the write filter and the new token always moves forward. A field left out is left alone; a request that changes nothing, or sends a wrong type, is refused whole. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "topic": {
                    "type": "string",
                    "description": "One line, at most 300 characters; empty clears it."
                  },
                  "section": {
                    "type": "string",
                    "nullable": true,
                    "description": "Sidebar group, at most 60 characters; empty or null files it under none."
                  },
                  "private": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; returns the channel with its new updatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_SETTINGS or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/archive": {
      "post": {
        "operationId": "archiveChannel",
        "summary": "Archive a channel, or undo that",
        "description": "archived: true archives; archived: false restores, the Undo, sent with the updatedAt the archive returned. REQUIRES expectedUpdatedAt; a stale token is refused, so an Undo never reverses a later change. An archived channel leaves the default channel list and keeps its messages; posts still land in it. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "archived",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "archived": {
                    "type": "boolean"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns the channel with archivedAt (null when restored) and its new updatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_SETTINGS (archived not a boolean) or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/delete": {
      "post": {
        "operationId": "deleteChannel",
        "summary": "Delete a channel with its messages",
        "description": "Moves the channel, its messages and the owner's read mark into the channel trash, where they stay restorable for 30 days (the memo trash's window). REQUIRES expectedUpdatedAt, the channel's lock token. Refused while a loop gate in the channel waits on its decision. A later post to the slug starts a new channel. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns the deletion { id, kind, channel, messageId, messageCount, deletedAt, updatedAt, purgeAfter }; its id and updatedAt are the Undo (restore)."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or OPEN_GATE (an undecided loop gate is in the channel)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/rename": {
      "post": {
        "operationId": "renameChannel",
        "summary": "Rename a channel",
        "description": "Changes the channel's slug; its messages and read mark move with it. REQUIRES expectedUpdatedAt. Undo is a rename back on the updatedAt this returns. loopsNamingPreviousSlug lists the caller's loops whose Alert channel still names the old slug: their next post starts a new channel there until their Policy is updated. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "to": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns channel (with its new slug and updatedAt), previousSlug and loopsNamingPreviousSlug[] { memoId, title, alertChannel, via[] }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_RENAME (to missing, invalid or the same slug), RESERVED_CHANNEL_SLUG (to is search) or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or SLUG_TAKEN."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/restore": {
      "post": {
        "operationId": "restoreChannelDeletion",
        "summary": "Undo a channel or message deletion",
        "description": "Restores a deletion from the channel trash: deletionId with the updatedAt the deletion returned, under the slug it was deleted from. A channel cannot come back once a post has started a new one under its slug (SLUG_TAKEN); a message comes back only into a channel, and a thread, that still exists. A refused Undo leaves the trash as it was. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "deletionId",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "deletionId": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns kind, restored (messages moved back) and channel (the restored channel, or null for a message)."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), DELETION_NOT_FOUND (unknown, another owner's, or past its 30 days), CHANNEL_NOT_FOUND or THREAD_NOT_FOUND (a message's channel or thread is gone)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or SLUG_TAKEN."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/messages/{messageId}/delete": {
      "post": {
        "operationId": "deleteChannelMessage",
        "summary": "Delete one message",
        "description": "Moves the message into the channel trash for 30 days; a top-level message takes its thread with it, and a reply leaves its parent's replyCount one lower. REQUIRES expectedUpdatedAt, the message's lock token. Refused for a loop gate still waiting on its decision. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          },
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Message id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Returns the deletion { id, kind, channel, messageId, messageCount, deletedAt, updatedAt, purgeAfter }; its id and updatedAt are the Undo (restore)."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED — a loop run's key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or no such message in the caller's channel (NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or OPEN_GATE (an undecided loop gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/messages": {
      "get": {
        "operationId": "listChannelMessages",
        "summary": "Read a channel's stream or one thread",
        "description": "Oldest first with a cursor. Pass the cursor back as after to read only newer messages — how an agent polls for replies and chosen actions. {channel} is a slug, or @<channelId> for a channel shared with the caller, read as a member; a channel the caller is not a member of is 404 CHANNEL_NOT_FOUND.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          },
          {
            "name": "threadId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Read this top-level message's replies instead of the stream."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Cursor from a previous read."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success, messages[], cursor."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED (a loop run's key naming a shared channel)."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "postChannelMessage",
        "summary": "Post a message, an ask, or a reply",
        "description": "An append: takes no lock token, and a retry without episodeKey can duplicate. An API key posts as an agent or loop, never a person. episodeKey collapses repeats into the open message (deduped: true). A reply advances its parent's updatedAt. In a channel shared with the caller (@<channelId>) a member's agent posts plain messages only (text, title, threadId, clientMessageId), named '<author.name> · @<member username>', under the 30-a-minute per-person limit; anything an owner's agent may add is 403 OWNER_ONLY.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "text"
                ],
                "properties": {
                  "text": {
                    "type": "string",
                    "maxLength": 8000
                  },
                  "title": {
                    "type": "string"
                  },
                  "authorImage": {
                    "type": "string",
                    "description": "Optional avatar: an image you uploaded (/api/internal/upload publicUrl). Without it, a saved poster identity whose name equals author.name exactly supplies one. Any other URL is 400 INVALID_AUTHOR_IMAGE."
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "agent",
                          "loop"
                        ]
                      },
                      "name": {
                        "type": "string"
                      },
                      "loopMemoId": {
                        "type": "string"
                      }
                    }
                  },
                  "ask": {
                    "type": "boolean"
                  },
                  "importance": {
                    "type": "string",
                    "enum": [
                      "normal",
                      "high"
                    ],
                    "description": "normal (default) or high. A top-level high post reaches the owner's phone like an ask, unless the channel is muted; mark high only a decision, a failure needing action or a production risk. Anything else is 400 INVALID_IMPORTANCE."
                  },
                  "actions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 6
                  },
                  "episodeKey": {
                    "type": "string"
                  },
                  "askKey": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "On a top-level ask only: the key every copy of this decision shares (a Session ask's ask.askKey). Answering any copy resolves the others into a label naming who decided, what and where."
                  },
                  "memoId": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "On a loop's top-level ask only (author.loopMemoId set): the one input memo the ask is about, any memo-id form. Choosing one of the ask's actions queues it into the loop's next run, and the ask stays followUp.status answered until the acting run marks it done (POST .../done). Elsewhere 400 INVALID_ASK_MEMO; a memo the owner cannot read is 404 MEMO_NOT_FOUND."
                  },
                  "run": {
                    "type": "object",
                    "properties": {
                      "runId": {
                        "type": "string"
                      },
                      "stepId": {
                        "type": "string"
                      }
                    }
                  },
                  "threadId": {
                    "type": "string"
                  },
                  "action": {
                    "type": "string",
                    "description": "On a reply: one of the parent's actions."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "deduped: true — counted onto the open episode message."
          },
          "201": {
            "description": "Posted."
          },
          "400": {
            "description": "INVALID_CHANNEL, INVALID_MESSAGE, INVALID_AUTHOR or UNKNOWN_ACTION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "OWNER_ONLY (a member's agent sending anything but a plain message in a shared channel), RUN_KEY_REFUSED (a loop run's key naming a shared channel), or a loop run's binding refusal."
          },
          "404": {
            "description": "Not staff, or THREAD_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/messages/{messageId}/resolve": {
      "post": {
        "operationId": "resolveChannelMessage",
        "summary": "Resolve a top-level message",
        "description": "REQUIRES expectedUpdatedAt from the message as last read; the CAS is in the write filter. with must be one of the message's actions. A loop gate's message is decided by a signed-in person and is refused here.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          },
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "with": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "agent",
                          "loop"
                        ]
                      },
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolved; returns the message."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or UNKNOWN_ACTION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "HUMAN_DECISION_REQUIRED — a loop gate's message."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or ALREADY_RESOLVED."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/messages/{messageId}/done": {
      "post": {
        "operationId": "markChannelAskDone",
        "summary": "Mark an answered memo ask done",
        "description": "The run that acted on an answered memo ask marks it done: followUp.status answered -> done. An ask posted with memoId moves open -> answered when one of its actions is chosen (its memo is queued into the posting loop's next run) -> done, here. REQUIRES expectedUpdatedAt from the message as last read; the CAS is in the write filter. A loop run marks only its own loop's asks.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          },
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "note": {
                    "type": "string"
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "agent",
                          "loop"
                        ]
                      },
                      "name": {
                        "type": "string"
                      },
                      "loopMemoId": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Marked done; returns the message."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_BINDING_REFUSED — a loop run marking another loop's ask."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, NOT_ANSWERED (still open), ALREADY_DONE, or NO_FOLLOW_UP (no memoId)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channel-notifications": {
      "get": {
        "operationId": "readAccountChannelNotifications",
        "summary": "Your account default for channel notifications",
        "description": "The level every channel without its own setting takes. The level that applies (all, mentions, muted), its source (channel, account or default), and the channel and account settings, each { level, updatedAt } or null. All and mentions push asks and high posts; muted pushes nothing. A loop run's key may read it.",
        "responses": {
          "200": {
            "description": "success, channel: null, notifications."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "put": {
        "operationId": "setAccountChannelNotifications",
        "summary": "Set your account default for channel notifications",
        "description": "REQUIRES expectedUpdatedAt: null when the setting does not exist yet, else its updatedAt; the CAS is in the write filter. level null clears it. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "level",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "level": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "all",
                      "mentions",
                      "muted",
                      null
                    ],
                    "description": "null clears the setting back to what it inherits."
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true,
                    "description": "null when the setting does not exist yet; else its updatedAt as last read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, setting { channel, level, createdAt, updatedAt } or null once cleared, and notifications."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_LEVEL or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt (null when there is no setting)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/members": {
      "get": {
        "operationId": "readChannelMembers",
        "summary": "A channel's members: the derived list and the people it is shared with",
        "description": "members: the owner and the library's grantees, derived on every read through the library grant check, never stored; they hold notification settings, not access. explicit: the people the owner shared the channel with, who read and post in it on the signed-in surface. membersUpdatedAt: the explicit list's lock token, null before its first edit. A loop run's key may read it. you: the caller's own username. On your own channel each members entry carries canJoin (whether an add would admit them). suggest adds suggestions, who Add People offers: grantees and the people on your other channels matching the query, plus anyone by exact username, only people who can open Loops and not already here, at most 8; the owner's only.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          },
          {
            "name": "suggest",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "A name or username to suggest people to add for; empty suggests everyone known."
          }
        ],
        "responses": {
          "200": {
            "description": "success, channel, role, you, members[] { username, displayName, role, canJoin? }, explicit[] { username, displayName, addedAt }, membersUpdatedAt, and with suggest, suggestions[] { username, displayName, libraryAccess }."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "OWNER_ONLY: suggest on a channel shared with you."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "addChannelMember",
        "summary": "Share one of your channels with a person",
        "description": "REQUIRES expectedUpdatedAt — read the members first and pass membersUpdatedAt (null before the first edit). The username resolves on the server; the person must be able to open Loops and is never the owner. At most 50 explicit members per channel. The member then reads and posts in the channel on the signed-in surface; nothing else of the owner's.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "username": {
                    "type": "string",
                    "description": "The person's MythOS username; case-insensitive, a leading @ ignored."
                  },
                  "expectedUpdatedAt": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "membersUpdatedAt from the members read; null before the list's first edit."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, explicit[] and the new membersUpdatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_CHANNEL, INVALID_MEMBER, MEMBER_IS_OWNER, MEMBER_NO_ACCESS."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED (a loop run's key), OWNER_ONLY (a member naming a shared channel)."
          },
          "404": {
            "description": "Not staff (Loops release gate), CHANNEL_NOT_FOUND, MEMBER_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, MEMBER_EXISTS, MEMBER_LIMIT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "removeChannelMember",
        "summary": "Take a person off one of your channels",
        "description": "REQUIRES expectedUpdatedAt — read the members first and pass membersUpdatedAt. Their access ends on their next request; every message stays and their posts keep their name.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "username": {
                    "type": "string",
                    "description": "The person's MythOS username; case-insensitive, a leading @ ignored."
                  },
                  "expectedUpdatedAt": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "membersUpdatedAt from the members read; null before the list's first edit."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, explicit[] and the new membersUpdatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_CHANNEL, INVALID_MEMBER, MEMBER_IS_OWNER, MEMBER_NO_ACCESS."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED (a loop run's key), OWNER_ONLY (a member naming a shared channel)."
          },
          "404": {
            "description": "Not staff (Loops release gate), CHANNEL_NOT_FOUND, MEMBER_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, MEMBER_EXISTS, MEMBER_LIMIT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/notifications": {
      "get": {
        "operationId": "readChannelNotifications",
        "summary": "Your notification level in a channel",
        "description": "The level that applies (all, mentions, muted), its source (channel, account or default), and the channel and account settings, each { level, updatedAt } or null. All and mentions push asks and high posts; muted pushes nothing. A loop run's key may read it.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "success, channel and notifications."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "put": {
        "operationId": "setChannelNotifications",
        "summary": "Set your notification level in a channel",
        "description": "REQUIRES expectedUpdatedAt: null when the setting does not exist yet, else its updatedAt; the CAS is in the write filter. level null clears it. A loop run's key is refused (403 RUN_KEY_REFUSED).",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "level",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "level": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "all",
                      "mentions",
                      "muted",
                      null
                    ],
                    "description": "null clears the setting back to what it inherits."
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true,
                    "description": "null when the setting does not exist yet; else its updatedAt as last read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, setting { channel, level, createdAt, updatedAt } or null once cleared, and notifications."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_LEVEL or INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel is not the caller's (CHANNEL_NOT_FOUND)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt (null when there is no setting)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channel-identities": {
      "get": {
        "operationId": "listChannelIdentities",
        "summary": "List the owner's poster identities",
        "description": "A poster identity is a name and an image the owner's agents post under. An agent or loop post whose author.name equals one exactly (case-sensitive) carries its image as author.imageUrl, copied on at post time. A loop run's key may read them.",
        "responses": {
          "200": {
            "description": "success and identities[] with id, name, imageUrl, createdAt, updatedAt (the lock token), by name."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "put": {
        "operationId": "saveChannelIdentity",
        "summary": "Save a poster identity",
        "description": "REQUIRES expectedUpdatedAt: null creates (409 STALE_LOCK_TOKEN with currentUpdatedAt when the name is already saved); the identity's updatedAt changes its image. imageUrl must be the owner's own confirmed upload; the server never fetches it. At most 50 per owner. A loop run's key is refused.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "imageUrl",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "imageUrl": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Image changed: success, created: false, identity."
          },
          "201": {
            "description": "Created: success, created: true, identity."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_IDENTITY or INVALID_IMAGE."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt (null when the name is not saved), or IDENTITY_LIMIT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "deleteChannelIdentity",
        "summary": "Delete a poster identity",
        "description": "Posts already made keep their image. REQUIRES expectedUpdatedAt. A loop run's key is refused.",
        "parameters": [
          {
            "name": "name",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_IDENTITY."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_KEY_REFUSED."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/channels/{channel}/read": {
      "post": {
        "operationId": "markChannelRead",
        "summary": "Mark a channel read up to now",
        "description": "Idempotent; the mark only moves forward.",
        "parameters": [
          {
            "name": "channel",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Channel slug, e.g. bbot-health; a leading # (URL-encoded) is ignored."
          }
        ],
        "responses": {
          "200": {
            "description": "success and lastReadAt."
          },
          "400": {
            "description": "INVALID_CHANNEL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or the channel/message is not the caller's."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices/enroll": {
      "post": {
        "operationId": "enrollDevice",
        "summary": "Exchange a single-use install code for a device's key",
        "description": "No header credential: the enrollment code from Add device (POST /api/loops/devices/enrollments, ten minutes, single use) is the credential, with a per-IP limit of ten an hour. Returns the device's key once, uncached. The key carries scopes [\"device\", \"loop-executor\"] with executorId equal to deviceId, so it heartbeats and claims runs as that device. The code is spent before anything is minted, so a refusal spends it too. The runner's setup (`mythos-device setup --code … --url …`, which the installer at `/install/device` runs) stores the key in the login Keychain or a 0600 file and never prints it.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "XXXX-XXXX; case, spaces and the hyphen are ignored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The device's key, shown this once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "key": {
                      "type": "string"
                    },
                    "deviceId": {
                      "type": "string"
                    },
                    "executorId": {
                      "type": "string"
                    },
                    "settings": {
                      "type": "object",
                      "description": "What Add device chose, for the installer to adopt; each field is null when not chosen. The same choices fill whatever the device's first heartbeat omits.",
                      "properties": {
                        "platform": {
                          "type": [
                            "string",
                            "null"
                          ],
                          "description": "Platform label, e.g. macOS, PC, Linux."
                        },
                        "intervalSeconds": {
                          "type": [
                            "integer",
                            "null"
                          ],
                          "description": "The chosen interval after the 60-second floor."
                        },
                        "alertChannel": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "INVALID_CODE: unknown, expired or already used."
          },
          "403": {
            "description": "ACCESS_DENIED (before release, the owner is not staff) or TIER_REQUIRED (after release, the owner is below Oracle)."
          },
          "409": {
            "description": "DEVICE_LIMIT (the account already has five devices with a live key), KEY_LIMIT or DEVICE_KEY_LIVE."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices/heartbeat": {
      "post": {
        "operationId": "sendDeviceHeartbeat",
        "summary": "A device reports its health",
        "description": "Device-scoped key only (scopes: [\"device\"], carrying deviceId); an owner or executor key is refused with 403 SCOPE_REFUSED. The first heartbeat creates the device record. Each heartbeat replaces the latest report; a check that turns failing opens an episode alert in the device's alertChannel and turning passing resolves it. A beat sooner than the 60-second floor after the last one that reached history updates the device but adds no history. A device silent for three effective intervals is marked missed by the run-loops tick, unless no live key names it, and the next heartbeat resolves that alert. Safe to retry.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "hostname": {
                    "type": "string"
                  },
                  "platform": {
                    "type": "string"
                  },
                  "intervalSeconds": {
                    "type": "integer",
                    "minimum": 30,
                    "maximum": 86400,
                    "description": "How often the device reports, 30 to 86400; default 300. Below the 60-second floor it is lifted to 60, the effective interval the record stores and settings.intervalSeconds returns. Kept from earlier heartbeats when omitted."
                  },
                  "alertChannel": {
                    "type": "string",
                    "description": "Channel slug for this device's alerts, e.g. bbot-health; default device-health. Kept when omitted."
                  },
                  "uptimeSeconds": {
                    "type": "number"
                  },
                  "disk": {
                    "type": "object",
                    "properties": {
                      "totalBytes": {
                        "type": "number"
                      },
                      "freeBytes": {
                        "type": "number"
                      }
                    }
                  },
                  "memory": {
                    "type": "object",
                    "description": "RAM. availableBytes is what a new process can take without swapping. Optional: older runners omit it.",
                    "properties": {
                      "totalBytes": {
                        "type": "number"
                      },
                      "availableBytes": {
                        "type": "number"
                      }
                    }
                  },
                  "checks": {
                    "type": "array",
                    "maxItems": 20,
                    "items": {
                      "type": "object",
                      "required": [
                        "name",
                        "ok"
                      ],
                      "properties": {
                        "name": {
                          "type": "string",
                          "maxLength": 60
                        },
                        "ok": {
                          "type": "boolean"
                        },
                        "detail": {
                          "type": "string",
                          "maxLength": 500
                        },
                        "level": {
                          "type": "string",
                          "enum": [
                            "fail",
                            "warn"
                          ],
                          "description": "Read only when ok is false. warn shows amber and never opens or closes an alert; fail, the default, alerts on the transition."
                        }
                      }
                    }
                  },
                  "tmuxSessions": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "attached": {
                          "type": "boolean"
                        },
                        "windows": {
                          "type": "integer"
                        },
                        "createdAt": {
                          "type": "number",
                          "description": "Epoch ms, from tmux #{session_created}."
                        },
                        "lastActivity": {
                          "type": "number",
                          "description": "Epoch ms, from tmux #{session_activity}: when the session last saw input or output."
                        },
                        "parent": {
                          "type": "string",
                          "maxLength": 100,
                          "description": "A managed session's tag: the tmux name of the session managing it. The device page nests the session under that one."
                        },
                        "summary": {
                          "type": "string",
                          "maxLength": 100,
                          "description": "A managed session's tag: a few words on its work."
                        },
                        "sessionId": {
                          "type": "string",
                          "maxLength": 100,
                          "description": "A managed session's tag: its MythOS Session id, which the device page links to."
                        },
                        "runnerVersion": {
                          "type": "string",
                          "description": "Runner 0.6.27 and later: the runner version the session was launched under, which its hooks, recorder and process guard keep calling until it ends. Stored only when it reads as a version."
                        }
                      }
                    }
                  },
                  "readiness": {
                    "type": "object",
                    "description": "Whether this device can run loops, as its setup sees it. Stored apart from checks, so a heartbeat-only device never alerts; replaced whole each heartbeat. A device with runnerEnabled true that reports agentSignedIn false opens one sign-in alert in its alertChannel. A malformed block is refused.",
                    "properties": {
                      "runnerVersion": {
                        "type": "string",
                        "description": "The runner's version, e.g. 1.2.0. Below runner.minimumVersion the device's claims are refused. A runner that sends none is never refused (the legacy exemption)."
                      },
                      "runnerEnabled": {
                        "type": "boolean",
                        "description": "Run claiming is enabled on this device."
                      },
                      "agentCli": {
                        "type": "boolean",
                        "description": "The agent CLI is installed."
                      },
                      "agentSignedIn": {
                        "type": "boolean",
                        "description": "The agent CLI is signed in for background runs."
                      },
                      "tmux": {
                        "type": "boolean",
                        "description": "tmux is installed."
                      },
                      "plugins": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "pattern": "^[a-z][a-z0-9-]{0,31}$"
                        },
                        "description": "The capability plug-ins installed on this device, by name only, never their commands. Shown on the device page. Names that are not capability names are dropped and at most 32 are kept; a value that is not a list is refused."
                      },
                      "designAccess": {
                        "type": "boolean",
                        "nullable": true,
                        "description": "Claude Code on this device holds a Claude Design sign-in every run can use (`mythos-device design-login`, runner 0.6.16 and later). Null when the runner can't tell. Optional: omitted, or anything but a boolean, reads as unknown. Shown as the device page's optional Design access row; it is never a step left and opens no alert."
                      },
                      "agents": {
                        "type": "object",
                        "description": "Runner 0.6.23 and later, only when the device declares accounts: each agent's accounts on the device, each in its own config directory there (for Claude Code, CLAUDE_CONFIG_DIR), by the label its person chose (`mythos-device account add`) and whether it is signed in. Labels and signed-in state only: never a directory or an account's email, and anything else an account carries is dropped. At most 8 agents and 20 accounts each; labels are unique per agent. A malformed block is refused. Omitted, the stored readiness has no agents. It decides no alert; agentSignedIn keeps its meaning.",
                        "additionalProperties": {
                          "type": "object",
                          "properties": {
                            "accounts": {
                              "type": "array",
                              "maxItems": 20,
                              "items": {
                                "type": "object",
                                "properties": {
                                  "label": {
                                    "type": "string",
                                    "pattern": "^[A-Za-z0-9][A-Za-z0-9_.-]{0,39}$"
                                  },
                                  "signedIn": {
                                    "type": "boolean"
                                  }
                                },
                                "required": [
                                  "label",
                                  "signedIn"
                                ]
                              }
                            }
                          },
                          "required": [
                            "accounts"
                          ]
                        }
                      }
                    }
                  },
                  "processGuard": {
                    "type": "boolean",
                    "description": "The runner applies its own process guard (the kill-processes safeguard's deny rule) to every session it launches. Shown in the device page's Readiness; omitted, Readiness says nothing about it. A value that is not a boolean is dropped. Replaced each heartbeat."
                  },
                  "lastClaimPoll": {
                    "type": "object",
                    "description": "Runner 0.6.11 and later, once it has asked for work: when it last polled for a claim and what it got. Shown in the device page's Readiness and in List Devices. A block without a whole non-negative at and a non-empty result is dropped, never refused. Replaced each heartbeat; omitted, it is removed.",
                    "properties": {
                      "at": {
                        "type": "integer",
                        "minimum": 0,
                        "description": "Epoch ms, by the device's clock."
                      },
                      "result": {
                        "type": "string",
                        "maxLength": 20,
                        "description": "claimed (a run), empty (nothing claimable), refused (until the runner updates) or failed (any other error). Another word is kept as written."
                      }
                    }
                  },
                  "hooks": {
                    "type": "object",
                    "description": "Runner 0.6.11 and later: the hooks every run it launches gets. Shown in the device page's Readiness in place of the processGuard label, and in List Devices. A member that is not a boolean is dropped; a block with none readable is dropped, never refused. Replaced each heartbeat; omitted, it is removed.",
                    "properties": {
                      "recorder": {
                        "type": "boolean",
                        "description": "The hook recorder."
                      },
                      "processGuard": {
                        "type": "boolean",
                        "description": "The runner's process guard."
                      },
                      "safeguards": {
                        "type": "boolean",
                        "description": "A Policy's safeguards are installed on the device."
                      }
                    }
                  },
                  "laptop": {
                    "type": "boolean",
                    "description": "The device is a laptop: silent past three intervals it is asleep, not missed, and opens no heartbeat alert until the silence passes 24 hours. Kept when omitted."
                  },
                  "attachUrl": {
                    "type": "string",
                    "maxLength": 500,
                    "description": "The device's own web terminal for its tmux sessions, as an https URL template; {session} marks where the session name goes, after the host. The device page opens it only when its host is a Tailscale name (*.ts.net) or one the owner approved, and falls back to an ssh command otherwise. Replaced whole each heartbeat; a malformed value is dropped."
                  },
                  "runnerUpdate": {
                    "type": "object",
                    "description": "Runner 0.6.27 and later, while the served release is newer than it: the update it has yet to apply. A beat without it clears it; a malformed block is dropped.",
                    "properties": {
                      "targetVersion": {
                        "type": "string"
                      },
                      "pendingSince": {
                        "type": "integer",
                        "description": "Epoch ms, by the device's clock: when it first saw that release."
                      },
                      "waitingFor": {
                        "type": [
                          "string",
                          "null"
                        ],
                        "enum": [
                          "drain",
                          "retry",
                          null
                        ],
                        "description": "drain: a release marked to drain, waiting for live sessions to end until drainUntil; retry: an attempt failed and the next comes within the hour; null: it applies on the next tick."
                      },
                      "drainUntil": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Epoch ms the drain wait ends; null unless waitingFor is drain."
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "success, created: false, device, settings, alertsOpened, alertsResolved. settings is { keepFinishedRunsDays, maxSessions, sessionLimit, intervalSeconds }: keepFinishedRunsDays is how long the device's runner keeps a settled run's folder and tmux socket, in days (0 removes it at once, null keeps it; default 7), and sessionLimit is how many runs the runner may hold at once, 1 to 20 or \"none\" for No limit (default 1), both chosen on the device page, and maxSessions is the same kept within 1 to 8 (8 for a larger choice or No limit) for runners before 0.6.26, which read only it; intervalSeconds is the effective heartbeat interval, never below the 60-second floor, for the runner to adopt. relay is { fastIntervalSeconds: 3, quietCeilingSeconds: 15, recentChangeSeconds: 120 }: sync a live run's Session every fastIntervalSeconds while an ask is open, a reply is pending or the pane changed within recentChangeSeconds, else back off to at most quietCeilingSeconds. runner is { currentVersion, minimumVersion, drain }, the versions null until MythOS serves a packaged runner; below the minimum the device lists updateRequired true and its claims are refused 403 RUNNER_UPDATE_REQUIRED. drain is true only when the served release must wait for the device's live sessions to end before it applies; otherwise a runner applies it on its next tick with sessions live, each keeping the version it launched under. updateNow is the owner's open Update now request { requestId, version }, or null, handed on every beat until the device reports that version or later; the runner acts on a requestId once. woke is true on a laptop's first beat after it slept, and a check failing in that beat is stored as a warning."
          },
          "201": {
            "description": "The device's first heartbeat: its record was created."
          },
          "400": {
            "description": "INVALID_HEARTBEAT: malformed checks (including a level other than fail or warn), intervalSeconds, alertChannel, readiness or laptop."
          },
          "401": {
            "description": "MISSING_CREDENTIAL or INVALID_CREDENTIAL."
          },
          "403": {
            "description": "SCOPE_REFUSED (not a device key) or DEVICE_ID_MISSING."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices/revoke": {
      "post": {
        "operationId": "revokeThisDevice",
        "summary": "A device revokes its own keys",
        "description": "Device-scoped key only. Revokes every live key of the calling device (its device key and any executor key under its id) and evicts them from this instance's identity cache; another instance may honour a revoked key for up to 60 seconds. The device stays listed as Revoked with its history. With forget: true the keys are revoked and evicted first, then the device and its history are deleted. Not behind the Loops gate: revoking only removes access.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "forget": {
                    "type": "boolean",
                    "description": "Also delete the device and its history (mythos-device uninstall --forget). Default false."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, revoked (keys revoked), forgotten (the device was deleted)."
          },
          "400": {
            "description": "INVALID_BODY: forget is not true or false."
          },
          "401": {
            "description": "MISSING_CREDENTIAL or INVALID_CREDENTIAL."
          },
          "403": {
            "description": "SCOPE_REFUSED (not a device key) or DEVICE_ID_MISSING."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices/run-key": {
      "post": {
        "operationId": "redeemRunKeyCode",
        "summary": "An enrolled device redeems a run-key code for its run key",
        "description": "Device-scoped key only (x-mythos-key). Redeems the single-use, ten-minute code the signed-in owner opened with Get a run key on the device page (POST /api/loops/devices/{deviceId}/run-key-code), for a run key bound to the calling device, minted as the enrollment exchange mints one: scope loop-run, the device's deviceId, counted against the account's 100 active keys. The code must belong to the key's owner and device; it is spent by the lookup itself, so a refusal after the lookup spends it too. Tries are limited to five per ten minutes per device key, and five failed tries burn the device's open code. A code opened with Replace revokes the device's old run key after the new one is minted; the device key is never touched. Returns the run key once, uncached. `mythos-device run-key --code … [--url …]` stores it in the login Keychain or a 0600 file and never prints it.",
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "XXXX-XXXX; case, spaces and the hyphen are ignored."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The run key, shown this once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "runKey": {
                      "type": "string"
                    },
                    "deviceId": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_CODE: unknown, expired, used, burned, or another owner's."
          },
          "401": {
            "description": "MISSING_CREDENTIAL, INVALID_CREDENTIAL, or DEVICE_REVOKED (the device key was revoked)."
          },
          "403": {
            "description": "SCOPE_REFUSED (not a device key), DEVICE_MISMATCH (the code was opened for another of the owner's devices, or before this device key was issued; it is spent), ACCESS_DENIED or TIER_REQUIRED."
          },
          "404": {
            "description": "USER_NOT_FOUND."
          },
          "409": {
            "description": "RUN_KEY_LIVE (a run key is live and the code did not choose Replace) or KEY_LIMIT."
          },
          "429": {
            "description": "RATE_LIMITED."
          }
        }
      }
    },
    "/api/internal/data-tables": {
      "get": {
        "operationId": "listDataTables",
        "summary": "List a library's data tables",
        "description": "Oracle only. Each table with its columns, key column, updatedAt and rowCount (archived rows included).",
        "parameters": [
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ tables }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "createDataTable",
        "summary": "Create a data table",
        "description": "Oracle only. A library holds at most 50 tables (422 DATA_TABLE_LIMIT) of at most 40 columns (422 DATA_TABLE_COLUMN_LIMIT). keyColumn is an optional upsert key: when set, it names a text column holding one opaque string per row, unique per table, that batch upserts write by, and never changes. Without it, rows are named by id.",
        "parameters": [
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "columns"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "columns": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "key",
                        "label",
                        "type"
                      ],
                      "properties": {
                        "key": {
                          "type": "string",
                          "description": "Stable machine name: lower-case letters, digits, - or _, starting with a letter, at most 48 characters. Rows store values by it; it never changes."
                        },
                        "label": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "text",
                            "number",
                            "date",
                            "select",
                            "url",
                            "memo",
                            "checkbox",
                            "email",
                            "phone",
                            "longtext",
                            "rating",
                            "duration",
                            "createdtime",
                            "modifiedtime",
                            "createdby",
                            "modifiedby",
                            "autonumber",
                            "recordid"
                          ]
                        },
                        "options": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "select only: 1–50 distinct values, set per table."
                        },
                        "personOnlyOptions": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "select only: options only a person on the web may set. An agent or API-key write that sets one is refused 403 PERSON_ONLY_VALUE; an agent may add marks but never lift one (403 PERSON_ONLY_SCHEMA)."
                        },
                        "transitions": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "An option id or label, null (empty) or \"*\" (any)."
                              },
                              "to": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "An option id or label, or null (clearing)."
                              },
                              "by": {
                                "type": "string",
                                "enum": [
                                  "person",
                                  "agent",
                                  "either"
                                ]
                              },
                              "requires": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            },
                            "required": [
                              "from",
                              "to"
                            ]
                          },
                          "description": "select only: the moves rows may make, read by option id. Absent allows any move; a move the list does not name, by the wrong actor, or missing a required field is refused 422 TRANSITION_REFUSED. Omitted on write keeps the stored list; null clears it. An agent may tighten but never loosen (403 PERSON_ONLY_SCHEMA)."
                        },
                        "numberFormat": {
                          "type": "object",
                          "nullable": true,
                          "description": "number only, display only: {style: currency, currency (an ISO 4217 code such as USD), precision?} or {style: percent, precision?}. precision is 0–4 decimal places (default 2 for currency, 0 for percent). Stored values stay plain numbers and are never rounded; a percent is a fraction, so 25% is 0.25. Setting, changing or removing it leaves rows as they are; null or absent is a plain number.",
                          "properties": {
                            "style": {
                              "type": "string",
                              "enum": [
                                "currency",
                                "percent"
                              ]
                            },
                            "currency": {
                              "type": "string"
                            },
                            "precision": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 4
                            }
                          },
                          "required": [
                            "style"
                          ]
                        },
                        "ratingMax": {
                          "type": "integer",
                          "minimum": 2,
                          "maximum": 10,
                          "nullable": true,
                          "description": "rating only: the most stars, 2–10; absent is 5. A rating value is a whole number from 1 to it. Raising it is always allowed; lowering it is refused 400 RATING_MAX_IN_USE (with column and rows) while any row, archived included, holds more."
                        },
                        "durationFormat": {
                          "type": "string",
                          "enum": [
                            "h:mm",
                            "h:mm:ss"
                          ],
                          "nullable": true,
                          "description": "duration only: how the web shows it; absent is h:mm. A duration value is whole seconds from 0 to 35999999; an h:mm column takes only multiples of 60. Switching to h:mm:ss is always allowed; switching to h:mm is refused 400 DURATION_FORMAT_IN_USE (with column and rows) while any row, archived included, holds leftover seconds."
                        },
                        "includeTime": {
                          "type": "boolean",
                          "nullable": true,
                          "description": "date only: the column holds a moment, written ISO 8601 with Z or an offset and stored in UTC as YYYY-MM-DDTHH:mm:ssZ; absent or false is a calendar date. A date alone or a time without an offset is refused 400, as a value or a filter. Switching it on or off is refused 400 DATE_TIME_IN_USE (with column and rows) while any row, archived included, holds a value."
                        },
                        "timeZone": {
                          "type": "string",
                          "nullable": true,
                          "description": "date with includeTime only: the IANA zone the web shows and takes times in, e.g. America/Los_Angeles; absent is each viewer's own. Display only: changing it never touches a stored moment."
                        }
                      }
                    }
                  },
                  "keyColumn": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional upsert key: a text column's key, or null for a table whose rows are named by id."
                  },
                  "retention": {
                    "type": "object",
                    "nullable": true,
                    "description": "{staleAfterDays?, purgeArchivedAfterDays?}, each 0, 30, 60, 90 or 180 days or null for off; null turns both off; anything else is 400 INVALID_RETENTION. The hourly sweep archives unarchived rows created longer than staleAfterDays ago (archiveNote stale) and deletes archived rows archived longer than purgeArchivedAfterDays ago, with their history.",
                    "properties": {
                      "staleAfterDays": {
                        "type": "integer",
                        "nullable": true,
                        "enum": [
                          0,
                          30,
                          60,
                          90,
                          180,
                          null
                        ]
                      },
                      "purgeArchivedAfterDays": {
                        "type": "integer",
                        "nullable": true,
                        "enum": [
                          0,
                          30,
                          60,
                          90,
                          180,
                          null
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ table, updatedAt }."
          },
          "400": {
            "description": "INVALID_TABLE."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "422": {
            "description": "DATA_TABLE_LIMIT or DATA_TABLE_COLUMN_LIMIT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}": {
      "get": {
        "operationId": "readDataTable",
        "summary": "Read a data table",
        "description": "Columns, key column, rowCount, and updatedAt — the token update and delete need.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ table, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateDataTable",
        "summary": "Rename a data table or replace its columns",
        "description": "CAS on the table's own updatedAt. changeColumnType changes one column's type (dryRun previews it without a token); insertColumn adds one column at a position in the order; columns replaces the whole list: a column keeps its type, the key column stays, and a removed column's values are cleared from every row after the CAS lands.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "retention": {
                    "type": "object",
                    "nullable": true,
                    "description": "{staleAfterDays?, purgeArchivedAfterDays?}, each 0, 30, 60, 90 or 180 days or null for off; null turns both off; anything else is 400 INVALID_RETENTION. The hourly sweep archives unarchived rows created longer than staleAfterDays ago (archiveNote stale) and deletes archived rows archived longer than purgeArchivedAfterDays ago, with their history.",
                    "properties": {
                      "staleAfterDays": {
                        "type": "integer",
                        "nullable": true,
                        "enum": [
                          0,
                          30,
                          60,
                          90,
                          180,
                          null
                        ]
                      },
                      "purgeArchivedAfterDays": {
                        "type": "integer",
                        "nullable": true,
                        "enum": [
                          0,
                          30,
                          60,
                          90,
                          180,
                          null
                        ]
                      }
                    }
                  },
                  "name": {
                    "type": "string"
                  },
                  "changeColumnType": {
                    "type": "object",
                    "description": "Change one column's type (Tables TB2), as a write of its own: beside any other change is 400 INVALID_TYPE_CHANGE. Any stored type to text; text to any stored type, each value parsed; a value that won't convert is kept (the row's unconverted) and changing back restores it exactly. The key column is 400 KEY_COLUMN_TYPE_LOCKED; computed columns and pairs that skip text are 400 TYPE_CHANGE_REFUSED; another column change while one still converts is 409 TYPE_CHANGE_PENDING. The response carries typeChange, what it did.",
                    "required": [
                      "column",
                      "type"
                    ],
                    "properties": {
                      "column": {
                        "type": "string"
                      },
                      "type": {
                        "type": "string"
                      }
                    }
                  },
                  "dryRun": {
                    "type": "boolean",
                    "description": "With changeColumnType: answer { preview, updatedAt } — the same plan the change runs — and write nothing. expectedUpdatedAt is not needed."
                  },
                  "insertColumn": {
                    "type": "object",
                    "description": "One new column (as a columns item) at position in the column order, in place of columns. position is 0 for first up to the current column count for last; omitted is last. Every other column and every row stay as they are. A position that is not a whole number from 0 to the column count, a position outside insertColumn, or insertColumn beside columns is 400 INVALID_POSITION.",
                    "required": [
                      "column"
                    ],
                    "properties": {
                      "column": {
                        "type": "object"
                      },
                      "position": {
                        "type": "integer",
                        "minimum": 0
                      }
                    }
                  },
                  "columns": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "required": [
                        "key",
                        "label",
                        "type"
                      ],
                      "properties": {
                        "key": {
                          "type": "string",
                          "description": "Stable machine name: lower-case letters, digits, - or _, starting with a letter, at most 48 characters. Rows store values by it; it never changes."
                        },
                        "label": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "text",
                            "number",
                            "date",
                            "select",
                            "url",
                            "memo",
                            "checkbox",
                            "email",
                            "phone",
                            "longtext",
                            "rating",
                            "duration",
                            "createdtime",
                            "modifiedtime",
                            "createdby",
                            "modifiedby",
                            "autonumber",
                            "recordid"
                          ]
                        },
                        "options": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "select only: 1–50 distinct values, set per table."
                        },
                        "personOnlyOptions": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "select only: options only a person on the web may set. An agent or API-key write that sets one is refused 403 PERSON_ONLY_VALUE; an agent may add marks but never lift one (403 PERSON_ONLY_SCHEMA)."
                        },
                        "transitions": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "type": "object",
                            "properties": {
                              "from": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "An option id or label, null (empty) or \"*\" (any)."
                              },
                              "to": {
                                "type": [
                                  "string",
                                  "null"
                                ],
                                "description": "An option id or label, or null (clearing)."
                              },
                              "by": {
                                "type": "string",
                                "enum": [
                                  "person",
                                  "agent",
                                  "either"
                                ]
                              },
                              "requires": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            },
                            "required": [
                              "from",
                              "to"
                            ]
                          },
                          "description": "select only: the moves rows may make, read by option id. Absent allows any move; a move the list does not name, by the wrong actor, or missing a required field is refused 422 TRANSITION_REFUSED. Omitted on write keeps the stored list; null clears it. An agent may tighten but never loosen (403 PERSON_ONLY_SCHEMA)."
                        },
                        "numberFormat": {
                          "type": "object",
                          "nullable": true,
                          "description": "number only, display only: {style: currency, currency (an ISO 4217 code such as USD), precision?} or {style: percent, precision?}. precision is 0–4 decimal places (default 2 for currency, 0 for percent). Stored values stay plain numbers and are never rounded; a percent is a fraction, so 25% is 0.25. Setting, changing or removing it leaves rows as they are; null or absent is a plain number.",
                          "properties": {
                            "style": {
                              "type": "string",
                              "enum": [
                                "currency",
                                "percent"
                              ]
                            },
                            "currency": {
                              "type": "string"
                            },
                            "precision": {
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 4
                            }
                          },
                          "required": [
                            "style"
                          ]
                        },
                        "ratingMax": {
                          "type": "integer",
                          "minimum": 2,
                          "maximum": 10,
                          "nullable": true,
                          "description": "rating only: the most stars, 2–10; absent is 5. A rating value is a whole number from 1 to it. Raising it is always allowed; lowering it is refused 400 RATING_MAX_IN_USE (with column and rows) while any row, archived included, holds more."
                        },
                        "durationFormat": {
                          "type": "string",
                          "enum": [
                            "h:mm",
                            "h:mm:ss"
                          ],
                          "nullable": true,
                          "description": "duration only: how the web shows it; absent is h:mm. A duration value is whole seconds from 0 to 35999999; an h:mm column takes only multiples of 60. Switching to h:mm:ss is always allowed; switching to h:mm is refused 400 DURATION_FORMAT_IN_USE (with column and rows) while any row, archived included, holds leftover seconds."
                        },
                        "includeTime": {
                          "type": "boolean",
                          "nullable": true,
                          "description": "date only: the column holds a moment, written ISO 8601 with Z or an offset and stored in UTC as YYYY-MM-DDTHH:mm:ssZ; absent or false is a calendar date. A date alone or a time without an offset is refused 400, as a value or a filter. Switching it on or off is refused 400 DATE_TIME_IN_USE (with column and rows) while any row, archived included, holds a value."
                        },
                        "timeZone": {
                          "type": "string",
                          "nullable": true,
                          "description": "date with includeTime only: the IANA zone the web shows and takes times in, e.g. America/Los_Angeles; absent is each viewer's own. Display only: changing it never touches a stored moment."
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ table, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_TABLE, RATING_MAX_IN_USE (lowering a rating column's ratingMax below a row's value), DURATION_FORMAT_IN_USE (switching a duration column to h:mm while a row holds leftover seconds), or DATE_TIME_IN_USE (switching a date column's includeTime on or off while a row holds a value); each carries column and rows."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "PERSON_ONLY_SCHEMA, or an access refusal."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "DATA_TABLE_COLUMN_LIMIT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "deleteDataTable",
        "summary": "Delete a data table and its rows",
        "description": "CAS on the table's own updatedAt; its rows are deleted after the table.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "description": "The updatedAt from the last read; missing is 400 MISSING_LOCK_TOKEN, stale 409 STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ deleted, rowsDeleted }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/rows": {
      "get": {
        "operationId": "queryDataTableRows",
        "summary": "Query a data table's rows",
        "description": "A page: filter (JSON array of {column, op: eq|ne|in|nin|lt|lte|gt|gte|contains|empty, value}, at most 10), q (rows where any text, url, memo, email, phone or longtext column contains it, case-insensitively, at most 200 characters; a memo column matches its stored id or path or the title its row's memos would show, a phone column its stored formatting), sort (a column key or createdAt|updatedAt|lastChangedAt|key, leading - for descending), limit (1–200, default 50) and cursor (nextCursor from the previous page, same sort). Or changes: changedSince (a millisecond timestamp, inclusive, or the changeCursor of an earlier query) returns rows in (lastChangedAt, id) order — rows written in the same millisecond are never skipped or repeated — with changeCursor to poll from next. Archived rows are left out unless includeArchived=true. Every row carries lastChangedBy {kind: person|agent, id} and lastChangedAt.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          },
          {
            "name": "filter",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "JSON array of {column, op, value}."
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "maxLength": 200
            },
            "description": "Full-text search across the table's text, url and memo columns; ANDed with filter."
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "changedSince",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Millisecond timestamp or changeCursor; takes no sort or cursor."
          },
          {
            "name": "keys",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "JSON array of row keys."
          },
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "JSON array of 1–100 row ids, exactly as reads return them."
          },
          {
            "name": "includeArchived",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "createdSince",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Rows created at or after this moment: a millisecond timestamp or ISO 8601 with Z or an offset. Combines with every other parameter."
          }
        ],
        "responses": {
          "200": {
            "description": "{ rows, hasMore, nextCursor } or, with changedSince, { rows, hasMore, changeCursor }."
          },
          "400": {
            "description": "INVALID_QUERY or INVALID_CURSOR."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "createDataTableRow",
        "summary": "Create a row",
        "description": "No token. A keyed table needs key; a key already held is 409 ROW_KEY_EXISTS with rowId and currentUpdatedAt. values are by column key; an agent setting a person-only option is 403 PERSON_ONLY_VALUE. A table holds at most 10,000 rows, archived included (422 DATA_TABLE_ROW_LIMIT).",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "key": {
                    "type": "string"
                  },
                  "values": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ row, updatedAt }."
          },
          "400": {
            "description": "INVALID_VALUES or DATA_TABLE_NOT_KEYED."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "PERSON_ONLY_VALUE, or an access refusal."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "ROW_KEY_EXISTS."
          },
          "422": {
            "description": "DATA_TABLE_ROW_LIMIT, or TRANSITION_REFUSED: a move a select column's transitions refuse (column, from, to, reason, by or missing)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/rows/batch": {
      "post": {
        "operationId": "upsertDataTableRows",
        "summary": "Upsert rows by key",
        "description": "At most 100 rows, keyed tables only. A new key needs no token. An existing key needs its row's updatedAt as expectedUpdatedAt — unless the write would change nothing, which answers unchanged, so a retry is idempotent. Bad values (400 INVALID_ROWS), a missing token (400 MISSING_LOCK_TOKEN), a stale token (409 STALE_LOCK_TOKEN, rows: [{key, currentUpdatedAt}]), an agent setting a person-only option (403 PERSON_ONLY_VALUE) and the row cap (422 DATA_TABLE_ROW_LIMIT) refuse the whole batch with nothing written. A row that loses a race mid-batch answers 409 with each row's outcome in results.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "rows"
                ],
                "properties": {
                  "rows": {
                    "type": "array",
                    "maxItems": 100,
                    "items": {
                      "type": "object",
                      "required": [
                        "key"
                      ],
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "values": {
                          "type": "object"
                        },
                        "expectedUpdatedAt": {
                          "type": "number",
                          "nullable": true
                        },
                        "archived": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ results: [{ key, status: created|updated|unchanged, row }], conflicts: 0 }."
          },
          "400": {
            "description": "INVALID_ROWS, BATCH_TOO_LARGE, MISSING_LOCK_TOKEN or DATA_TABLE_NOT_KEYED."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "PERSON_ONLY_VALUE, or an access refusal."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN."
          },
          "422": {
            "description": "DATA_TABLE_ROW_LIMIT, or TRANSITION_REFUSED: a move a select column's transitions refuse (column, from, to, reason, by or missing)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/rows/{rowId}": {
      "get": {
        "operationId": "readDataTableRow",
        "summary": "Read a row",
        "description": "Archived or not.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "rowId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Row id."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ row, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND or ROW_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateDataTableRow",
        "summary": "Change or archive a row",
        "description": "CAS on the row's own updatedAt. values names only the columns it changes (null clears one); a row keeps its key (400 ROW_KEY_IMMUTABLE). archived: true hides it from default queries; false restores it. An agent setting a person-only option the row does not already hold is 403 PERSON_ONLY_VALUE.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "rowId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Row id."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "values": {
                    "type": "object"
                  },
                  "archived": {
                    "type": "boolean"
                  },
                  "archiveNote": {
                    "type": "object",
                    "description": "Why the row is archived: {disposition: handled|dropped, target, reason}. handled needs target (the memo path or URL it went to), dropped needs reason (one line); each at most 500 characters. Rides archived: true or replaces an archived row's note; restoring clears it. stale is set only by the retention sweep. Otherwise 400 INVALID_ARCHIVE_NOTE.",
                    "properties": {
                      "disposition": {
                        "type": "string",
                        "enum": [
                          "handled",
                          "dropped"
                        ]
                      },
                      "target": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 500
                      },
                      "reason": {
                        "type": "string",
                        "nullable": true,
                        "maxLength": 500
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ row, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_VALUES or ROW_KEY_IMMUTABLE."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "PERSON_ONLY_VALUE, or an access refusal."
          },
          "404": {
            "description": "ROW_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "TRANSITION_REFUSED: a move a select column's transitions refuse (column, from, to, reason, by or missing)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "deleteDataTableRow",
        "summary": "Delete a row",
        "description": "CAS on the row's own updatedAt. Deleting frees the row's slot; archiving does not.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "rowId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Row id."
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "description": "The updatedAt from the last read; missing is 400 MISSING_LOCK_TOKEN, stale 409 STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ deleted, id }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "ROW_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/view-settings": {
      "get": {
        "operationId": "readDataTableViewSettings",
        "summary": "Read your view settings for a table",
        "description": "The key holder's own view settings for this table — the same ones they see on the web, never another viewer's. Display only: they never change the table, its columns or a row. Answers { settings, updatedAt }; updatedAt is null before the first save. settings is {columnOrder: [column keys, every column in the grid's order], hiddenColumns: [keys], columnWidths: {key: pixels 80–800}, groupBy: a select column key or null (the board then takes the table's defaultView, then its first select), cardFields: [up to 6 keys] or null (a card's subtitle shows these, joined by ·; null shows the card as drawn), sort: {field, dir: 1|-1} or null, filters: [{column, op, value}] as the rows query's filter (≤ 10), includeArchived: boolean, laneOrder: {select column key: [option ids, ≤ 200]} (the board's lanes in the viewer's order; options left out follow in the column's order)}. Read against the table's columns now: a removed column drops out, a new one joins columnOrder right after the column before it in the table's order, or at the end when no saved column follows it, a removed option drops out of laneOrder, and a sort or filter the rows query would refuse is dropped. A view-only grantee reads and saves their own.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "responses": {
          "200": {
            "description": "{ settings, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED, no grant, DATA_TABLES_SCOPE_REFUSED, or RUN_LIBRARY_REFUSED."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateDataTableViewSettings",
        "summary": "Save your view settings for a table",
        "description": "REQUIRES expectedUpdatedAt: the settings' own updatedAt from the read, or null for the first save. Each field named replaces that setting; the others keep their saved value. Every key must name a column of the table now, and every laneOrder id an option of its select column. Saves the key holder's own settings only; any read-capable grant may save its own, and a device run key needs Loop management.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "updatedAt from the read; null before the first save."
                  },
                  "columnOrder": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "hiddenColumns": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "columnWidths": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "integer",
                      "minimum": 80,
                      "maximum": 800
                    }
                  },
                  "groupBy": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "cardFields": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 6
                  },
                  "sort": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "properties": {
                      "field": {
                        "type": "string"
                      },
                      "dir": {
                        "type": "integer",
                        "enum": [
                          1,
                          -1
                        ]
                      }
                    }
                  },
                  "filters": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "maxItems": 10
                  },
                  "includeArchived": {
                    "type": "boolean"
                  },
                  "laneOrder": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "maxItems": 200
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ settings, updatedAt } as they now read."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN (no expectedUpdatedAt) or INVALID_VIEW_SETTINGS (a key that is no column, a width out of range, a groupBy that is no select column, a sort or filter the rows query refuses, an unknown field)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED, no grant, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt (null when nothing is saved yet)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/inbound-email": {
      "get": {
        "operationId": "readDataTableInboundEmail",
        "summary": "Read a table's inbound email address",
        "description": "A table's own address, table-<32 hex>@<domain>, where each message mailed becomes one row. Answers { inboundEmail: {exists, prefix, enabled, canManage, address?, senders?, createdAt?, rotatedAt?}, updatedAt }. address, senders and dates only for the library owner on their own key (a device run key needs Loop management); every grantee, editors included, gets exists, prefix and enabled. The address is a credential: anyone who knows it can add rows.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. The full address and every write are for the library owner on their own key only."
          }
        ],
        "responses": {
          "200": {
            "description": "{ inboundEmail, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED, no grant, DATA_TABLES_SCOPE_REFUSED, or RUN_LIBRARY_REFUSED."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND, or INBOUND_EMAIL_NOT_FOUND when the table has no address."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "mintDataTableInboundEmail",
        "summary": "Mint or rotate a table's inbound email address",
        "description": "REQUIRES expectedUpdatedAt (the table's). Owner only. Mints the address and adds the columns mail lands in where missing: from (email), subject (text), received (date with time), body (longtext), links (longtext, one URL per line, never fetched), message_id (text). rotate: true replaces an existing address; the old one stops at once. Each message is one row, an agent write on the owner's behalf, deduped per table on its Message-ID (else the provider's email id). Answers 201 { inboundEmail (with address), table, updatedAt }.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. The full address and every write are for the library owner on their own key only."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The table's updatedAt from read."
                  },
                  "rotate": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ inboundEmail, table, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_INBOUND_EMAIL, or a column rule (DATA_TABLE_COLUMN_LIMIT is 422)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "INBOUND_EMAIL_OWNER_ONLY, RUN_DATA_TABLE_WRITE_REFUSED, DATA_TABLES_TIER_REQUIRED, or no grant."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND, or INBOUND_EMAIL_NOT_FOUND when the table has no address."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt; INBOUND_EMAIL_EXISTS (mint without rotate); INBOUND_EMAIL_COLUMN_CONFLICT (a column holds one of the keys as another type, or one of the labels)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateDataTableInboundEmail",
        "summary": "Set a table inbound address's senders or turn it on or off",
        "description": "REQUIRES expectedUpdatedAt (the table's). Owner only. senders: exact addresses or whole @domains (at most 100; never a subdomain), null for anyone; enabled: false stops mail without losing the address. Answers { inboundEmail, table, updatedAt }.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. The full address and every write are for the library owner on their own key only."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The table's updatedAt from read."
                  },
                  "senders": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "nullable": true
                  },
                  "enabled": {
                    "type": "boolean"
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ inboundEmail, table, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_INBOUND_EMAIL."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "INBOUND_EMAIL_OWNER_ONLY, RUN_DATA_TABLE_WRITE_REFUSED, DATA_TABLES_TIER_REQUIRED, or no grant."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND, or INBOUND_EMAIL_NOT_FOUND when the table has no address."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "removeDataTableInboundEmail",
        "summary": "Remove a table's inbound email address",
        "description": "REQUIRES expectedUpdatedAt as a query parameter (the table's). Owner only. The address stops at once; the table keeps its columns and rows. Answers { inboundEmail, table, updatedAt }.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. The full address and every write are for the library owner on their own key only."
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "description": "The table's updatedAt from read."
          }
        ],
        "responses": {
          "200": {
            "description": "{ inboundEmail, table, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "INBOUND_EMAIL_OWNER_ONLY, RUN_DATA_TABLE_WRITE_REFUSED, DATA_TABLES_TIER_REQUIRED, or no grant."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND, or INBOUND_EMAIL_NOT_FOUND when the table has no address."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/data-tables/{tableId}/rows/{rowId}/history": {
      "get": {
        "operationId": "readDataTableRowHistory",
        "summary": "Read a row's history",
        "description": "The row's changes newest first: who changed which columns from what to what, and when. Read with exactly the access that reads the row. Each entry is {id, tableId, rowId, kind (created|updated), changedAt, changedBy, changes: [{column, from, to}], archived: {from, to} | null}. Records row writes only; a refused write records nothing; deleting the row deletes its history, archiving keeps it.",
        "parameters": [
          {
            "name": "tableId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Data table id from list or create."
          },
          {
            "name": "rowId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Row id."
          },
          {
            "name": "library",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Library (username) whose tables to use; defaults to the key's own. A grantee reads with any read-capable grant and writes with an editor-or-above grant (the dataTables capability); a grant narrowed by content scope is refused 403 DATA_TABLES_SCOPE_REFUSED."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            },
            "description": "Entries per page; default 50."
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "nextCursor from the previous page."
          }
        ],
        "responses": {
          "200": {
            "description": "{ entries, hasMore, nextCursor }."
          },
          "400": {
            "description": "INVALID_QUERY (a bad limit or cursor)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DATA_TABLES_TIER_REQUIRED (the library owner is not on Oracle), no grant or a role without the capability, DATA_TABLES_SCOPE_REFUSED, RUN_LIBRARY_REFUSED, or RUN_DATA_TABLE_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "DATA_TABLE_NOT_FOUND or ROW_NOT_FOUND."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices": {
      "get": {
        "operationId": "listDevices",
        "summary": "The owner's devices and their health",
        "description": "Owner key. Each device carries status: healthy, failing (a check fails), missed (silent past three intervals; a laptop, past 24 hours as well), asleep (a laptop silent past three intervals but under 24 hours) or never (a live device key names it but it has not reported), and hasLiveKey. readiness is the latest heartbeat's readiness block or null, laptop its laptop flag, updateRequired true below the runner minimum (unknown when a minimum is set and the device reports no version), and takeUnpinnedLoops whether its runner is offered loops pinned to no device. Each also carries strays (the runner's strays sweep, 0.6.12 and later; null when the runner does not report them, never read as clean), sweep, cleanUp (the latest clean-up request: pending, delivered, done, failed or withdrawn, with targets, notTargeted and the runner's result) and cleanUpUpdatedAt, cleanUpDevice's lock token (0 before any request). share and capacity are the latest heartbeat's share and capacity blocks (null when its runner does not send them); tokens is each agent's rolling 5-hour and 7-day token totals from run receipts with the owner's soft ceilings (counted = input + output + cache writes), and tokenCeilingsUpdatedAt is setDeviceTokenCeiling's lock token (0 before any ceiling). keepFinishedRunsDays, maxSessions, takeUnpinnedLoops and secureValues are the owner's settings, and settingsUpdatedAt is updateDeviceSettings' lock token (0 before any settings write). lastClaimPoll ({ at, result }) and hooks ({ recorder, processGuard, safeguards }) are the latest heartbeat's (null when its runner does not send them, before 0.6.11). runnerUpdate ({ targetVersion, pendingSince, waitingFor, drainUntil }) is the latest heartbeat's pending runner update (null when none is pending or its runner does not say, before 0.6.27), updateNow is the open Update now request (null when none) and updateNowUpdatedAt is updateDeviceRunner's lock token (0 before any request); each tmuxSessions entry carries runnerVersion when its runner reports it. deviceId narrows the list to that device; with it, sessions=true adds sessions, the device's tmux sessions and strays joined to their Sessions.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "query",
            "required": false,
            "description": "Only this device.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sessions",
            "in": "query",
            "required": false,
            "description": "With deviceId, true adds the sessions view.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and devices."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate)."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/devices/{deviceId}": {
      "patch": {
        "operationId": "updateDeviceSettings",
        "summary": "Change a device's settings held on MythOS",
        "description": "Owner key only; a loop run's request key is refused 403 RUN_SCOPE_REFUSED. Body { keepFinishedRunsDays?, maxSessions?, takeUnpinnedLoops?, expectedUpdatedAt }, any of the settings, each replacing its value: keepFinishedRunsDays 0, 1, 7, 30, 60, 90 or null (keep); maxSessions 1 to 20 or \"none\" (No limit); takeUnpinnedLoops true or false. The heartbeat response hands Retention and Max sessions to the runner. secureValues, runOptIns and attachHosts are the owner's on the device page and are refused 403 PERSON_ONLY. CAS on settingsUpdatedAt, which every settings write advances, the device page's included.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "keepFinishedRunsDays": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "enum": [
                      0,
                      1,
                      7,
                      30,
                      60,
                      90,
                      null
                    ]
                  },
                  "maxSessions": {
                    "type": [
                      "integer",
                      "string"
                    ],
                    "description": "1 to 20, or \"none\" for No limit."
                  },
                  "takeUnpinnedLoops": {
                    "type": "boolean"
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The device's settingsUpdatedAt from listDevices (0 before any settings write)."
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, settings (as the heartbeat hands them), takeUnpinnedLoops and settingsUpdatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_DEVICE_SETTINGS."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_SCOPE_REFUSED (a loop run's request key) or PERSON_ONLY (secureValues, runOptIns or attachHosts)."
          },
          "404": {
            "description": "DEVICE_NOT_FOUND, or not staff."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN (with currentUpdatedAt)."
          }
        }
      }
    },
    "/api/internal/devices/{deviceId}/clean-up": {
      "post": {
        "operationId": "cleanUpDevice",
        "summary": "Ask a device's runner to clean up its strays",
        "description": "Owner key, or a loop run's request key with its device's Loop management. Body { scope, discardUncommitted?, expectedUpdatedAt } requests; { withdraw: true, expectedUpdatedAt } withdraws the open request. scope is { kind: family, sessionId } (a coordinator's Session and every descendant), { kind: run, runId } (a settled run) or { kind: device }. The scope resolves against the device's latest strays: targets are the strays the runner started; the rest are notTargeted with reason not_runner_started. The request stays pending, then delivered, until the runner's heartbeat reports done or failed. discardUncommitted: true is refused 403 PERSON_ONLY here; without it a dirty worktree is always kept. A loop run may name only a family rooted at a Session of a run its device holds. CAS on cleanUpUpdatedAt.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scope": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "family",
                          "run",
                          "device"
                        ]
                      },
                      "sessionId": {
                        "type": "string"
                      },
                      "runId": {
                        "type": "string"
                      }
                    },
                    "required": [
                      "kind"
                    ]
                  },
                  "discardUncommitted": {
                    "type": "boolean"
                  },
                  "withdraw": {
                    "type": "boolean"
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The device's cleanUpUpdatedAt from listDevices."
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, cleanUp and cleanUpUpdatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_CLEAN_UP."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "PERSON_ONLY (discardUncommitted from a key), RUN_SCOPE_REFUSED (a loop run naming anything but its family), or RUN_LOOP_WRITE_REFUSED."
          },
          "404": {
            "description": "DEVICE_NOT_FOUND, SESSION_NOT_FOUND (from a loop run, also a family it does not hold), RUN_NOT_FOUND, or not staff."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN (with currentUpdatedAt), CLEAN_UP_PENDING, NO_OPEN_CLEAN_UP, RUN_LIVE, STRAYS_UNREPORTED or NOTHING_TO_CLEAN (with notTargeted)."
          }
        }
      }
    },
    "/api/internal/devices/{deviceId}/token-ceiling": {
      "post": {
        "operationId": "setDeviceTokenCeiling",
        "summary": "Set or clear a device's soft token ceilings for an agent",
        "description": "Owner key only; a loop run's request key is refused 403 RUN_SCOPE_REFUSED. Body { agent?, account?, fiveHour?, sevenDay?, expectedUpdatedAt }: agent defaults to claude; a window is a whole number of tokens (1 to 10^12), null clears it, omitted leaves it. Without account (or account null) it sets the agent's ceiling on the device's background-run sign-in: it counts input, output and cache-write tokens from that sign-in's run receipts, never cache reads, and the heartbeat response hands it to the runner as settings.tokenCeiling. With account, a device account label, it sets that account's own ceiling, kept apart in accountTokenCeilings, weighed when a claim chooses an account and never handed to the runner. Published plan limits are not exact token counts, so it is the owner's soft number. CAS on tokenCeilingsUpdatedAt.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent": {
                    "type": "string"
                  },
                  "account": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "A device account label: that account's own ceiling. Absent or null: the background-run sign-in's."
                  },
                  "fiveHour": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "sevenDay": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The device's tokenCeilingsUpdatedAt from listDevices (0 before any ceiling)."
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, tokenCeilings, tokenCeilingsUpdatedAt and tokens; accountTokenCeilings [{agent, account, fiveHour, sevenDay}] too while the device holds a per-account ceiling."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_TOKEN_CEILING."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_SCOPE_REFUSED (a loop run's request key)."
          },
          "404": {
            "description": "DEVICE_NOT_FOUND, or not staff."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN (with currentUpdatedAt)."
          }
        }
      }
    },
    "/api/internal/devices/{deviceId}/update": {
      "post": {
        "operationId": "updateDeviceRunner",
        "summary": "Ask a device's runner to update now",
        "description": "Owner key only; a loop run's request key is refused 403 RUN_SCOPE_REFUSED. Body { expectedUpdatedAt }. Stores an Update now request for the release /install/device serves; every heartbeat response hands it to the runner as updateNow until the device reports that version or later, which clears it. The runner then skips its hour after a failed attempt and any drain wait. A request while one is open replaces it with a new requestId. CAS on updateNowUpdatedAt.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The device's updateNowUpdatedAt from listDevices (0 before any request)."
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, updateNow { requestId, version, requestedAt, requestedBy } and updateNowUpdatedAt."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_UPDATE_NOW."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_SCOPE_REFUSED (a loop run's request key)."
          },
          "404": {
            "description": "DEVICE_NOT_FOUND, or not staff."
          },
          "409": {
            "description": "RUNNER_CURRENT (the device already reports the served release or later) or STALE_LOCK_TOKEN (with currentUpdatedAt)."
          }
        }
      }
    },
    "/api/internal/usage-readings": {
      "get": {
        "operationId": "readAccountUsage",
        "summary": "The owner's agent accounts' usage readings, with device ceilings beside them",
        "description": "Owner key; a loop run's key may read. Each account (agent, account label) with usageUpdatedAt (the write's lock token), latest, readings over the last days by readAt oldest first (readAt, sessionPercent, weeklyPercent, sessionResetsAt, weeklyResetsAt, source; at most the newest 5000, with truncated), and deviceCeilings: that agent's 5-hour and 7-day counted tokens, ceiling and reached on each of the owner's devices with receipts or a ceiling for it. Kept 30 days by TTL.",
        "parameters": [
          {
            "name": "agent",
            "in": "query",
            "required": false,
            "description": "Only this agent's accounts.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "account",
            "in": "query",
            "required": false,
            "description": "Only accounts with this label.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "days",
            "in": "query",
            "required": false,
            "description": "Days of readings, 1 to 30; default 30.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and accounts."
          },
          "400": {
            "description": "INVALID_QUERY."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff."
          }
        }
      },
      "post": {
        "operationId": "recordAccountUsage",
        "summary": "Record one usage reading for one of the owner's agent accounts",
        "description": "Owner key only; a loop run's request key is refused 403 RUN_SCOPE_REFUSED. The owner's own reader posts what it saw; MythOS never reads a provider's usage itself. Any field not in the schema is refused 400 UNKNOWN_FIELD, named but never echoed, and nothing is stored. One reading per account per 60 seconds (429 RATE_LIMITED with retryAfterMs and Retry-After); at most 20 accounts per owner (422 ACCOUNT_LIMIT_REACHED). CAS on the account's usageUpdatedAt. Readings and account records are kept 30 days by TTL.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "agent": {
                    "type": "string",
                    "description": "The agent CLI; default claude."
                  },
                  "account": {
                    "type": "string",
                    "description": "The reader's label: 1 to 64 characters of A-Z a-z 0-9 space . _ @ -."
                  },
                  "sessionPercent": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 100
                  },
                  "weeklyPercent": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "minimum": 0,
                    "maximum": 100
                  },
                  "sessionResetsAt": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ],
                    "description": "ISO 8601 or epoch ms."
                  },
                  "weeklyResetsAt": {
                    "type": [
                      "string",
                      "integer",
                      "null"
                    ],
                    "description": "ISO 8601 or epoch ms."
                  },
                  "source": {
                    "type": "string",
                    "description": "Up to 64 printable characters."
                  },
                  "readAt": {
                    "type": [
                      "string",
                      "integer"
                    ],
                    "description": "ISO 8601 or epoch ms; at most 5 minutes ahead, no older than 30 days."
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The account's usageUpdatedAt (0 before its first reading)."
                  }
                },
                "required": [
                  "account",
                  "source",
                  "readAt",
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, usageUpdatedAt and account."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_USAGE_READING or UNKNOWN_FIELD."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_SCOPE_REFUSED (a loop run's request key)."
          },
          "404": {
            "description": "Not staff."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN (with currentUpdatedAt)."
          },
          "422": {
            "description": "ACCOUNT_LIMIT_REACHED (with limit)."
          },
          "429": {
            "description": "RATE_LIMITED (with retryAfterMs)."
          }
        }
      }
    },
    "/api/internal/devices/{deviceId}/heartbeats": {
      "get": {
        "operationId": "readDeviceHeartbeats",
        "summary": "One device's last 30 days of heartbeats",
        "description": "Owner key. The device page's strip, exactly as the reader gives it: days oldest first, one per calendar day in tz, each with state (failing, gap, warn, ok, asleep or none), beats, failed, warned, late, failingChecks and warningChecks. A device with no history is 30 none days. With series=hourly, the device's capacity history instead (tz ignored): hours, one per UTC hour that had a beat over the last 30 days, oldest first, each with hourStart, beats, loadPeak, loadAverage, memoryAvailableFloorBytes, memoryAvailableAverageBytes, diskFreeFloorBytes, sessionsInUsePeak, sessionsInUseAverage, beatsAtMax and maxSessions; a metric no beat reported that hour is null. Kept 30 days by TTL.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tz",
            "in": "query",
            "required": false,
            "description": "IANA time zone for day boundaries; UTC when missing or unknown.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "series",
            "in": "query",
            "required": false,
            "description": "hourly: capacity history by UTC hour instead of the daily strip.",
            "schema": {
              "type": "string",
              "enum": [
                "hourly"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success, timeZone and days; with series=hourly, success, series and hours."
          },
          "400": {
            "description": "INVALID_SERIES for a series other than hourly."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "DEVICE_NOT_FOUND for a malformed id, or not staff (Loops release gate)."
          }
        }
      }
    },
    "/api/internal/monitors": {
      "get": {
        "operationId": "listMonitors",
        "summary": "The owner's registered monitors and their liveness",
        "description": "Owner key. Each monitor carries status: healthy, running, never, failing, missed, silent or paused. deviceId keeps only the monitors that declare that device; a malformed id is an empty list.",
        "parameters": [
          {
            "name": "deviceId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and monitors."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's monitor."
          }
        }
      }
    },
    "/api/internal/monitors/{monitorId}": {
      "get": {
        "operationId": "getMonitor",
        "summary": "One registered monitor and its liveness",
        "description": "Owner key. Its declaration, last run, next due slot, open alerts, and status.",
        "parameters": [
          {
            "name": "monitorId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and monitor."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's monitor."
          }
        }
      },
      "put": {
        "operationId": "declareMonitor",
        "summary": "Declare when a monitor should run, so a missed or silent run alerts",
        "description": "Idempotent upsert, no lock token: the monitor re-declares itself as config and the last declaration stands. paused: true stops the watch and closes open missed and silent alerts; un-pausing restarts the clock.",
        "parameters": [
          {
            "name": "monitorId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "schedule"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 80
                  },
                  "schedule": {
                    "type": "object",
                    "description": "Either { everySeconds } (60–604800) or { times: [\"HH:MM\"], days?: [\"mon\"…\"sun\"], timezone } — slots are computed in the monitor's IANA zone.",
                    "properties": {
                      "everySeconds": {
                        "type": "integer",
                        "minimum": 60,
                        "maximum": 604800
                      },
                      "times": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
                        },
                        "maxItems": 48
                      },
                      "days": {
                        "type": "array",
                        "items": {
                          "type": "string",
                          "enum": [
                            "sun",
                            "mon",
                            "tue",
                            "wed",
                            "thu",
                            "fri",
                            "sat"
                          ]
                        }
                      },
                      "timezone": {
                        "type": "string"
                      }
                    }
                  },
                  "graceSeconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 86400,
                    "default": 900
                  },
                  "maxRunSeconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 86400,
                    "default": 3600
                  },
                  "alertChannel": {
                    "type": "string",
                    "default": "monitor-health"
                  },
                  "deviceId": {
                    "type": "string"
                  },
                  "paused": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Re-declared: success, created false, monitor."
          },
          "201": {
            "description": "Declared: success, created true, monitor."
          },
          "400": {
            "description": "INVALID_MONITOR — a malformed schedule, grace, channel or id is refused, not dropped."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's monitor."
          }
        }
      }
    },
    "/api/internal/monitors/{monitorId}/runs": {
      "post": {
        "operationId": "reportMonitorRun",
        "summary": "Report a monitor's run: started, succeeded or failed",
        "description": "A failed run opens a failed alert once (a repeat is absorbed); the next success resolves it. Any run resolves an open missed or silent alert. Not idempotent: a retried finish moves the last-run time.",
        "parameters": [
          {
            "name": "monitorId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "started",
                      "succeeded",
                      "failed"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  },
                  "runKey": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, monitor, alertsOpened, alertsResolved."
          },
          "400": {
            "description": "INVALID_RUN"
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "NOT_FOUND — declare the monitor first; also the release gate and another owner's monitor."
          }
        }
      }
    },
    "/api/internal/loop-sessions": {
      "get": {
        "operationId": "listLoopSessions",
        "summary": "The owner's agent sessions, newest activity first",
        "description": "Owner key. status filters to running, needs_you, waiting or ended; origin to one origin (coding includes rows still stored as kickoff); deviceId to one device's sessions; parentSessionId to one session's children. A device-scoped key lists its own device's sessions only, whatever deviceId names. Each session carries description and descriptionSource: the description its agent set (set), else its latest agent line (agent), else null. Each also carries orphaned: true when it has not ended and its parent has ended or is gone; adopt it through /children/{childSessionId}/adopt.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "running",
                "needs_you",
                "waiting",
                "ended"
              ]
            }
          },
          {
            "name": "deviceId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "parentSessionId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "origin",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "coding",
                "runner",
                "loop_step",
                "coordinator"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and sessions."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          }
        }
      },
      "post": {
        "operationId": "startLoopSession",
        "summary": "Start a session record for a running agent",
        "description": "No lock token. Pass clientKey to make a retry safe: a repeat returns the first session with 200. A device-scoped key's session is stamped with its deviceId; a body naming another device, or null, is refused 400 INVALID_SESSION. parentSessionId starts it under one of the caller's own sessions (a device key: its own device's); any other id is refused 400 INVALID_SESSION, and the parent cannot change afterwards. title is required unless the platform titles the Session: a run's Session started with a placeholder title (none, blank, Loop run, or the loop's own name) is titled from the run's first input memo plus a counter (\"Roadmap · 003\"), and an untitled coordinator naming scopeMemoId is titled \"<memo> coordinator · NNN\". An explicit title is never replaced.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "origin": {
                    "type": "string",
                    "enum": [
                      "coding",
                      "runner",
                      "loop_step",
                      "coordinator",
                      "kickoff"
                    ],
                    "description": "`coding` for a coding session. `kickoff` is a deprecated alias, stored as `coding`."
                  },
                  "parentSessionId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Start under this session of the caller's; set at start only."
                  },
                  "scopeMemoId": {
                    "type": "string",
                    "description": "Coordinator only: one of the owner's memos the coordinator works over, used to title an untitled coordinator; not stored."
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "One line under the title."
                  },
                  "compute": {
                    "type": "string",
                    "enum": [
                      "local_subscription",
                      "local_model",
                      "cloud"
                    ]
                  },
                  "deviceId": {
                    "type": "string",
                    "nullable": true
                  },
                  "repo": {
                    "type": "string",
                    "nullable": true
                  },
                  "branch": {
                    "type": "string",
                    "nullable": true
                  },
                  "worktree": {
                    "type": "string",
                    "nullable": true
                  },
                  "tmuxName": {
                    "type": "string",
                    "nullable": true
                  },
                  "model": {
                    "type": "string",
                    "nullable": true
                  },
                  "loopMemoId": {
                    "type": "string",
                    "nullable": true
                  },
                  "step": {
                    "type": "string",
                    "nullable": true
                  },
                  "run": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "runId": {
                        "type": "string"
                      },
                      "stepId": {
                        "type": "string"
                      }
                    }
                  },
                  "pr": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "number": {
                        "type": "integer"
                      },
                      "url": {
                        "type": "string",
                        "description": "https only"
                      }
                    }
                  },
                  "tokens": {
                    "type": "integer",
                    "nullable": true
                  },
                  "progress": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "done": {
                        "type": "integer"
                      },
                      "total": {
                        "type": "integer"
                      }
                    }
                  },
                  "touched": {
                    "type": "array",
                    "maxItems": 30,
                    "items": {
                      "type": "string"
                    }
                  },
                  "clientKey": {
                    "type": "string",
                    "description": "Makes a retried start return the first session."
                  }
                },
                "required": [
                  "origin",
                  "compute"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "clientKey repeat: the existing session."
          },
          "201": {
            "description": "success, created, session."
          },
          "400": {
            "description": "INVALID_SESSION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}": {
      "get": {
        "operationId": "getLoopSession",
        "summary": "Read one session",
        "description": "Owner key. Its updatedAt is the lock token for update and end; another owner's session reads as 404. A device-scoped key reads its own device's sessions; any other reads as 404. family=1 adds descendants (every child, their children…, each with depth, through the same scope, at most 5 generations and 200 sessions), truncated, and familyUsage (the receipt tokens by kind of the session and every descendant read, with total, sessions and truncated). Each session carries description, descriptionSource and orphaned as in the list, and usage (its run receipts' tokens by kind, null when none booked) with tokens and tokensSource: the receipt total (receipt), else the agent's own report (agent). account is the device account label the run's latest booked receipt named, null for the device's background-run sign-in, absent until a receipt books.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "family",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success and session (updatedAt is the lock token)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          }
        }
      },
      "patch": {
        "operationId": "updateLoopSession",
        "summary": "Set named session fields",
        "description": "REQUIRES expectedUpdatedAt. status moves only through asks and end; null clears a field. description is one line of at most 200 characters, trimmed. The owner's key sets any field on the owner's sessions; a loop run's request key, any field on its own device's sessions. agentState is working or idle, as the agent's runner reports it; the web reads it over the transcript when set. A device-scoped key sets only description and agentState (plus expectedUpdatedAt), on a session whose ownerUid and deviceId match its binding; any other session reads 404, and any other field is refused 403 DEVICE_FIELD_REFUSED with nothing written.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "maxLength": 200
                  },
                  "description": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 200,
                    "description": "One line under the title; unset, reads show the latest agent line."
                  },
                  "deviceId": {
                    "type": "string",
                    "nullable": true
                  },
                  "repo": {
                    "type": "string",
                    "nullable": true
                  },
                  "branch": {
                    "type": "string",
                    "nullable": true
                  },
                  "worktree": {
                    "type": "string",
                    "nullable": true
                  },
                  "tmuxName": {
                    "type": "string",
                    "nullable": true
                  },
                  "model": {
                    "type": "string",
                    "nullable": true
                  },
                  "loopMemoId": {
                    "type": "string",
                    "nullable": true
                  },
                  "step": {
                    "type": "string",
                    "nullable": true
                  },
                  "run": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "runId": {
                        "type": "string"
                      },
                      "stepId": {
                        "type": "string"
                      }
                    }
                  },
                  "pr": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "number": {
                        "type": "integer"
                      },
                      "url": {
                        "type": "string",
                        "description": "https only"
                      }
                    }
                  },
                  "tokens": {
                    "type": "integer",
                    "nullable": true
                  },
                  "agentState": {
                    "type": "string",
                    "nullable": true,
                    "enum": [
                      "working",
                      "idle",
                      null
                    ],
                    "description": "Whether the agent is at work or waiting on the person, as its runner reports it. Unset, the web reads the transcript."
                  },
                  "progress": {
                    "type": "object",
                    "nullable": true,
                    "properties": {
                      "done": {
                        "type": "integer"
                      },
                      "total": {
                        "type": "integer"
                      }
                    }
                  },
                  "touched": {
                    "type": "array",
                    "maxItems": 30,
                    "items": {
                      "type": "string"
                    }
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                },
                "required": [
                  "expectedUpdatedAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success and session."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_SESSION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "DEVICE_FIELD_REFUSED: a device-scoped key named a field other than description or agentState."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or SESSION_ENDED."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}/asks/{askId}/withdraw": {
      "post": {
        "operationId": "withdrawLoopSessionAsk",
        "summary": "Withdraw or supersede an agent's own open ask",
        "description": "REQUIRES expectedUpdatedAt, the Session's updatedAt as last read. Closes an open ask an agent posted in this Session, withdrawn with its note or superseded by a newer ask (supersededBy); its push and Channel copy clear. A loop gate is 409 DECIDED_BY_HOLD, MythOS's own ask 403 NOT_AN_AGENT_ASK, an answered ask 409 ALREADY_ANSWERED. A device-scoped key reaches its own device's sessions only; any other reads as 404.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "askId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "note": {
                    "type": "string"
                  },
                  "supersededBy": {
                    "type": "string"
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ success, askId, session }."
          },
          "400": {
            "description": "INVALID_EVENT: a missing lock token, or a malformed note or supersededBy."
          },
          "403": {
            "description": "NOT_AN_AGENT_ASK."
          },
          "404": {
            "description": "NOT_FOUND or ASK_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, SESSION_ENDED, ALREADY_ANSWERED or DECIDED_BY_HOLD."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}/end": {
      "post": {
        "operationId": "endLoopSession",
        "summary": "End a session with a result",
        "description": "REQUIRES expectedUpdatedAt. Open asks close with the session and a closing sys event is written. A device-scoped key ends its own device's sessions only; any other reads as 404. A run's Session ends when its run settles, and a device key's end before then is refused 409 RUN_NOT_SETTLED.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt",
                  "result"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "result": {
                    "type": "string",
                    "enum": [
                      "succeeded",
                      "failed",
                      "stopped",
                      "partial"
                    ]
                  },
                  "note": {
                    "type": "string"
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success and session."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_SESSION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, SESSION_ENDED, or RUN_NOT_SETTLED."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}/children/{childSessionId}/adopt": {
      "post": {
        "operationId": "coordinatorAdoptChildSession",
        "summary": "A live coordinator adopts an orphan: a child Session whose parent has ended",
        "description": "REQUIRES expectedUpdatedAt: the child's updatedAt as last read; the parent it was read under must still be its parent. Re-parents childSessionId under sessionId and writes a sys audit line in both. Afterwards the adopter's replies and answers land as its coordinator's, a run key holding the adopter's run may cancel the child's run, and the child's outcome line goes to the adopter. Same callers as writing into a child: the owner's live Session with origin coordinator, or, for a loop run's request key with its device's Loop management on, the live Session of a run that device holds. Only within the owner's account. A repeat by the same adopter is deduped. A device key is refused.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The adopting coordinator's Session."
          },
          {
            "name": "childSessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The orphan (read_loop_sessions marks it orphaned: true)."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "success, deduped and the child session."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or INVALID_SESSION (the adopter itself or one of its ancestors)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "NOT_COORDINATOR, or RUN_LOOP_WRITE_REFUSED."
          },
          "404": {
            "description": "NOT_FOUND: the child or adopter is missing or another owner's."
          },
          "409": {
            "description": "PARENT_LIVE (the child's parent is still live, or it has none), SESSION_ENDED, or STALE_LOCK_TOKEN with currentUpdatedAt."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}/children/{childSessionId}/events": {
      "post": {
        "operationId": "coordinatorWriteChildSession",
        "summary": "A coordinator answers an ask in, or replies to, its own child Session",
        "description": "REQUIRES expectedUpdatedAt: the child's updatedAt as last read. sessionId is the parent: the owner's Session with origin coordinator, or, for a loop run's request key with its device's Loop management on, the live Session of a run that device holds. A run key is bound to its device, not one run, so any run on that device may write through such a Session. Only Sessions whose parentSessionId is sessionId, in the owner's account; the child may run on another device. Body: text (a reply), or answers (the child's ask event id) with option. The event is written as a human event authored {kind: coordinator, name, sessionId} — what the device runner delivers, as it does a person's — and an answer adds a sys audit line naming the coordinator's Session. Plain asks only: an approve ask or a loop gate is refused 403 PERSON_ONLY. Not idempotent without clientEventId. A device key is refused.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The coordinator's Session."
          },
          {
            "name": "childSessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "text": {
                    "type": "string"
                  },
                  "answers": {
                    "type": "string",
                    "description": "The child's ask event id."
                  },
                  "option": {
                    "type": "string",
                    "description": "One of the ask's options, exactly."
                  },
                  "clientEventId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A retry with the same clientEventId: the first event (deduped: true)."
          },
          "201": {
            "description": "success, deduped, event and the child session."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_EVENT or UNKNOWN_OPTION."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "NOT_COORDINATOR (the parent is not a coordinator Session, or not a run Session this device holds), PERSON_ONLY (a loop gate, or an approve ask not posted with ask.coordinatorMay: true), or RUN_LOOP_WRITE_REFUSED (a run key without Loop management)."
          },
          "404": {
            "description": "Not staff (Loops release gate), NOT_FOUND (not the caller's parent Session), NOT_CHILD, or ASK_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, SESSION_ENDED, or ALREADY_ANSWERED."
          }
        }
      }
    },
    "/api/internal/loop-sessions/{sessionId}/events": {
      "get": {
        "operationId": "listLoopSessionEvents",
        "summary": "The session summary transcript, oldest first, or one summary event's full record",
        "description": "Pass the returned cursor as after to read only what arrived since. Record lines (appended with record: true) never appear; records counts the record lines behind each returned event and recordTail those after the newest. With record=<eventId>, returns the record lines behind that summary event instead (record=tail: after the newest), with truncated. A device-scoped key reads its own device's sessions only; any other reads as 404.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "record",
            "in": "query",
            "description": "A summary event id, or tail: return the full-record lines behind it.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "maximum": 500
            }
          }
        ],
        "responses": {
          "200": {
            "description": "success, events, cursor, records and recordTail; with record, success, events and truncated."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          }
        }
      },
      "post": {
        "operationId": "appendLoopSessionEvent",
        "summary": "Append an agent's event to the transcript",
        "description": "No lock token; pass clientEventId to make a retry safe. ask and approve turn the session to needs_you. record: true appends a line of the full record (sys, prompt, agent or tool), kept out of the summary reads. An API key never writes a human event. A device-scoped key appends to its own device's sessions only; any other reads as 404. ref {sessionId} must name a session the caller can reach, else 400 INVALID_REF.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind"
                ],
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "sys",
                      "prompt",
                      "agent",
                      "tool",
                      "ask",
                      "approve"
                    ]
                  },
                  "text": {
                    "type": "string",
                    "maxLength": 8000
                  },
                  "ref": {
                    "type": "object",
                    "description": "Another Session this event is about, such as a child the caller launched; rendered as a link to it. Must be one the caller can reach (a device or run key: its own device's); otherwise 400 INVALID_REF.",
                    "required": [
                      "sessionId"
                    ],
                    "properties": {
                      "sessionId": {
                        "type": "string"
                      }
                    }
                  },
                  "tool": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      },
                      "arg": {
                        "type": "string"
                      },
                      "out": {
                        "type": "string"
                      }
                    }
                  },
                  "ask": {
                    "type": "object",
                    "properties": {
                      "question": {
                        "type": "string"
                      },
                      "options": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 6,
                        "items": {
                          "type": "string"
                        }
                      },
                      "items": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      },
                      "coordinatorMay": {
                        "type": "boolean",
                        "description": "On an approve ask only: lets the Session's parent coordinator answer it."
                      },
                      "askKey": {
                        "type": "string",
                        "maxLength": 200,
                        "description": "The key every copy of this decision shares (a Channel ask's askKey). Answering any copy resolves the others; a coordinator's answer never resolves a person-only copy."
                      }
                    }
                  },
                  "author": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string"
                      }
                    }
                  },
                  "record": {
                    "type": "boolean"
                  },
                  "clientEventId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "clientEventId repeat: deduped."
          },
          "201": {
            "description": "success, deduped, event, session."
          },
          "400": {
            "description": "INVALID_EVENT."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff (Loops release gate), or not the caller's session."
          },
          "409": {
            "description": "SESSION_ENDED."
          }
        }
      }
    },
    "/api/internal/memos": {
      "get": {
        "summary": "List memos",
        "description": "Returns memos from the library, filtered by query parameters. Results are sorted by last updated descending.",
        "operationId": "listMemos",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Search query to filter memos by title",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tag names to filter by. Two or more tags require the tagMode parameter.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagMode",
            "in": "query",
            "description": "How multiple tags combine: 'all' returns memos carrying every listed tag (intersection); 'any' returns memos carrying at least one (union). Required whenever two or more tags are passed; ignored for a single tag. A multi-tag request without it returns 400.",
            "schema": {
              "type": "string",
              "enum": [
                "any",
                "all"
              ]
            }
          },
          {
            "name": "visibility",
            "in": "query",
            "description": "Comma-separated visibility filter (public, link)",
            "schema": {
              "type": "string",
              "default": "public,link"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of memos to return (1-200, default 50)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of memos",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "memos": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a memo",
        "description": "Creates a new memo in the library. If tags are not provided, they are automatically extracted from the content.",
        "operationId": "createMemo",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MemoInput"
              },
              "example": {
                "title": "My New Memo",
                "content": "This is the memo content with [#tags](/tag/tags) inline.",
                "visibility": "link"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Memo created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "memo": {
                      "$ref": "#/components/schemas/Memo"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (missing title)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a memo",
        "description": "Updates content and optionally other fields of an existing memo. Increments contentVersion. Optional SEO fields (seoTitle, seoDescription) can be included alongside content. For SEO-only updates without touching content, use PATCH /api/internal/memos/seo instead. A content write that would cut a body of 2,000+ characters to half or less returns 422 LARGE_DELETION and writes nothing; retry with confirmLargeDeletion: true to confirm it.",
        "operationId": "updateMemo",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The memo ID to update",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "Updated memo content (required). Hashtag chips are serialized as [#name](/tag/name) — always write that explicit form. A bare #word is auto-linked into a chip unless sourceMarkupVersion: 2 is passed."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Updated tags. If omitted, tags are extracted from content."
                  },
                  "collaboratorContent": {
                    "type": "string",
                    "description": "Updated notes/collaborator section. Bumps collabContentVersion when provided."
                  },
                  "seoTitle": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 70,
                    "description": "Optional SEO title. Pass null or empty string to clear."
                  },
                  "seoDescription": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 160,
                    "description": "Optional SEO description. Pass null or empty string to clear."
                  },
                  "sourceMarkupVersion": {
                    "type": "integer",
                    "description": "Optional. Markup version of the content being sent. Omit (or 1) for legacy behavior — bare #word is auto-linked into a tag. Set to 2 when the content is already v2 markdown where bare #word is literal text and tags are written as [#name](/tag/name)."
                  },
                  "confirmLargeDeletion": {
                    "type": "boolean",
                    "description": "Pass true only after a 422 LARGE_DELETION, to confirm a content write that deletes most of the body."
                  }
                }
              },
              "example": {
                "content": "Updated content with new [#tags](/tag/tags)",
                "tags": [
                  "tags",
                  "updated"
                ],
                "seoTitle": "A better title for search engines"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Memo updated successfully. contentVersion reflects the new bumped value.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "contentVersion": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing id, invalid field value, or no updatable field provided",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Memo not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteMemos",
        "summary": "DELETE /api/internal/memos",
        "description": "Soft-delete a memo using required id and expectedUpdatedAt query parameters. Ownership or the appropriate grant capability and atomic CAS apply; returns success after moving to trash. Stale tokens return 409 with currentUpdatedAt.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/seo": {
      "patch": {
        "summary": "Update memo SEO metadata",
        "description": "Updates only the seoTitle and/or seoDescription of a memo. Does not touch content, tags, or contentVersion. Pass null or empty string for a field to clear it.",
        "operationId": "updateMemoSeo",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "The memo ID to update",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "At least one of seoTitle or seoDescription is required.",
                "properties": {
                  "seoTitle": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 70,
                    "description": "SEO title override. Pass null or empty string to clear."
                  },
                  "seoDescription": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 160,
                    "description": "SEO description override. Pass null or empty string to clear."
                  }
                }
              },
              "example": {
                "seoTitle": "A better title for search engines",
                "seoDescription": "Up to 160 characters describing this memo for previews."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SEO metadata updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "seoTitle": {
                      "type": "string",
                      "nullable": true
                    },
                    "seoDescription": {
                      "type": "string",
                      "nullable": true
                    },
                    "updatedAt": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing id, invalid JSON body, no SEO field provided, or invalid field value",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Memo not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/memos/batch": {
      "post": {
        "summary": "Batch create memos",
        "description": "Creates multiple memos in a single operation. Maximum 50 memos per request.",
        "operationId": "batchCreateMemos",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memos"
                ],
                "properties": {
                  "memos": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/MemoInput"
                    },
                    "maxItems": 50
                  },
                  "sourceMarkupVersion": {
                    "type": "integer",
                    "description": "Optional batch-level markup version applied to every memo that does not set its own. See MemoInput.sourceMarkupVersion."
                  }
                }
              },
              "example": {
                "memos": [
                  {
                    "title": "First Memo",
                    "content": "Content one"
                  },
                  {
                    "title": "Second Memo",
                    "content": "Content two",
                    "tags": [
                      "batch"
                    ]
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Memos created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "created": {
                      "type": "integer"
                    },
                    "memos": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Memo"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/embed": {
      "post": {
        "summary": "Trigger memo embedding",
        "description": "Generates vector embeddings for specified memos or all memos. Used for semantic search.",
        "operationId": "embedMemos",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "type": "object",
                    "required": [
                      "memoIds"
                    ],
                    "properties": {
                      "memoIds": {
                        "type": "array",
                        "items": {
                          "type": "string"
                        },
                        "description": "Array of memo IDs to embed"
                      }
                    }
                  },
                  {
                    "type": "object",
                    "required": [
                      "all"
                    ],
                    "properties": {
                      "all": {
                        "type": "boolean",
                        "enum": [
                          true
                        ],
                        "description": "Set to true to embed all memos"
                      }
                    }
                  }
                ]
              },
              "examples": {
                "byIds": {
                  "summary": "Embed specific memos",
                  "value": {
                    "memoIds": [
                      "brianswichkow-abc123",
                      "brianswichkow-def456"
                    ]
                  }
                },
                "all": {
                  "summary": "Embed all memos",
                  "value": {
                    "all": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Embedding results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "embedded": {
                      "type": "integer",
                      "description": "Number of memos successfully embedded"
                    },
                    "failed": {
                      "type": "integer",
                      "description": "Number of memos that failed to embed"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Memos skipped because their author cannot fund indexing or has not given AI consent"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/embed/batch": {
      "post": {
        "summary": "Batch embed memos",
        "description": "Generates vector embeddings for a batch of memos. Maximum 100 memo IDs per request. Returns detailed error information for any failures.",
        "operationId": "batchEmbedMemos",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memoIds"
                ],
                "properties": {
                  "memoIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "maxItems": 100,
                    "description": "Array of memo IDs to embed"
                  }
                }
              },
              "example": {
                "memoIds": [
                  "brianswichkow-abc123",
                  "brianswichkow-def456"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch embedding results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "embedded": {
                      "type": "integer"
                    },
                    "failed": {
                      "type": "integer"
                    },
                    "skipped": {
                      "type": "integer",
                      "description": "Memos skipped because their author cannot fund indexing or has not given AI consent"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "memoId": {
                            "type": "string"
                          },
                          "error": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/upload": {
      "post": {
        "summary": "Upload an image and get a URL you can attach",
        "description": "Stores the bytes and records a confirmed upload owned by you, then returns publicUrl. That record is what makes the URL attachable: every field that stores an image — an event's flyerUrl, a community's avatarUrl, a collection's coverImageUrl — resolves the URL back to a confirmed upload owned by the caller and rejects anything else, so a URL from elsewhere on the domain will not pass. Upload first, then send publicUrl on the write that carries the field. Send raw base64 with no data URI prefix; the decoded bytes are checked against the declared format's own header and terminator, because a payload cut short in transit still decodes as valid base64 and would otherwise store as a corrupt image. Payloads above roughly 24KB are the ones that get truncated through an MCP tool call.",
        "operationId": "uploadImage",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "filename",
                  "data"
                ],
                "properties": {
                  "filename": {
                    "type": "string",
                    "description": "Filename with extension. The extension selects the content type and must match the bytes: .png, .jpg, .jpeg, .gif or .webp"
                  },
                  "data": {
                    "type": "string",
                    "description": "Base64-encoded image bytes, no data URI prefix. Decoded size is capped at 10 MB"
                  }
                }
              },
              "example": {
                "filename": "flyer.png",
                "data": "iVBORw0KGgoAAAANSUhEUgAA..."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored. publicUrl is attachable to any confirmed-upload image field",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "publicUrl": {
                      "type": "string"
                    },
                    "width": {
                      "type": "integer",
                      "description": "Intrinsic pixel width, EXIF orientation applied. Omitted when the header carries no readable size. Write it into the image markdown as width=N alongside height=N so the image can lazy-load."
                    },
                    "height": {
                      "type": "integer",
                      "description": "Intrinsic pixel height, EXIF orientation applied. Omitted together with width."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing filename or data, an unsupported extension, or bytes that are not a complete image of the declared format. The body carries a code: MALFORMED_BASE64, FORMAT_MISMATCH or TRUNCATED_IMAGE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Decoded image exceeds the 10 MB limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/chat": {
      "post": {
        "summary": "Chat with a library",
        "description": "Send a message to chat with a user's memo library. Uses RAG to find relevant memos and streams an AI response. Supports conversation threading.",
        "operationId": "chatWithLibrary",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "libraryOwnerUsername",
                  "message"
                ],
                "properties": {
                  "libraryOwnerUsername": {
                    "type": "string",
                    "description": "Username of the library owner to chat with"
                  },
                  "message": {
                    "type": "string",
                    "description": "The user's message"
                  },
                  "threadId": {
                    "type": "string",
                    "description": "Optional thread ID for continuing a conversation"
                  }
                }
              },
              "example": {
                "libraryOwnerUsername": "brianswichkow",
                "message": "What are your thoughts on AI?",
                "threadId": "abc-123"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "SSE stream of AI response",
            "headers": {
              "X-Thread-Id": {
                "description": "The thread ID for this conversation",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Missing required fields",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Library owner not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/openapi": {
      "get": {
        "summary": "Get OpenAPI specification",
        "description": "Returns this OpenAPI JSON specification document.",
        "operationId": "getOpenApiSpec",
        "responses": {
          "200": {
            "description": "OpenAPI specification",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/comment-threads": {
      "get": {
        "summary": "Read an owned comment thread",
        "description": "Returns the full comment thread only for a caller-owned memo or a caller-authored community post. Scope is verified before comments are queried; a missing target and a target outside the caller's scope both return 404. This route has no username or library parameter and does not expose platform-wide comment reads.",
        "operationId": "readCommentThread",
        "parameters": [
          {
            "name": "targetType",
            "in": "query",
            "required": true,
            "description": "`memo` or `community_post`",
            "schema": {
              "type": "string",
              "enum": [
                "memo",
                "community_post"
              ]
            }
          },
          {
            "name": "memoId",
            "in": "query",
            "description": "Required for `targetType=memo`. Accepts a short id, compound dash id, or canonical slash path. The memo must be authored by the API-key owner.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "memoScope",
            "in": "query",
            "description": "For memo targets, choose all comments, stream comments, or composition comments.",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "stream",
                "composition"
              ],
              "default": "all"
            }
          },
          {
            "name": "communitySlug",
            "in": "query",
            "description": "Required for `targetType=community_post`. The community slug containing the post.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "query",
            "description": "Required for `targetType=community_post`. Accepts the community post ObjectId or postSlug. The post must have been authored by the API-key owner.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "Thread sort order.",
            "schema": {
              "type": "string",
              "enum": [
                "hot",
                "newest",
                "oldest"
              ],
              "default": "hot"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The scoped comment thread"
          },
          "400": {
            "description": "Missing or invalid target parameters"
          },
          "403": {
            "description": "Unauthorized"
          },
          "404": {
            "description": "Comment thread not found. Also returned when the target exists but is outside the caller's scope."
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities": {
      "get": {
        "summary": "List communities you can see",
        "description": "Public communities plus the private and secret ones you are a member of, sorted by member count. Each result carries `joined` and its resolved `visibility`. Each community has its own llms.txt at /we/{slug}/llm.txt.",
        "operationId": "listCommunities",
        "parameters": [
          {
            "name": "slug",
            "in": "query",
            "description": "Filter by community slug (exact match)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of communities to return (1-100, default 50)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            }
          },
          {
            "name": "joined",
            "in": "query",
            "description": "Only communities you are a member of",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of communities",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "communities": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommunitySummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a community",
        "description": "Admin-only agent route for creating a MythOS community. Browser creates remain on /api/communities and require Scholar tier.",
        "operationId": "createCommunity",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "slug",
                  "description"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "rules": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private",
                      "secret"
                    ],
                    "default": "public"
                  },
                  "nsfw": {
                    "type": "boolean"
                  },
                  "guidelines": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "stewards": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Community created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "community": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid create payload",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Admin access required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Slug already exists",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}": {
      "get": {
        "summary": "Read one community",
        "description": "Name, description, tags, counts, the About page and your own role. A community you may not see answers 404 exactly as one that does not exist. A PRIVATE community you are not a member of answers 403 with a `gate` carrying only its name, description and the stewards' note -- counts, rules and tags stay withheld.",
        "operationId": "readCommunity",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The community",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "community": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Private community -- the request-access gate",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": false
                    },
                    "error": {
                      "type": "string"
                    },
                    "gate": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a community",
        "description": "Admin-only agent route for deleting a MythOS community. Requires the current updatedAt value as expectedUpdatedAt.",
        "operationId": "deleteCommunity",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "description": "The updatedAt value returned by reading the community immediately before delete",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Community deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid expectedUpdatedAt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Admin access required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Community not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "expectedUpdatedAt is stale",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/discover": {
      "get": {
        "summary": "Discover public communities",
        "description": "Public communities you have NOT joined, ranked by member count. Use `GET /api/internal/communities?joined=1` for the ones you are already in.",
        "operationId": "discoverCommunities",
        "parameters": [
          {
            "name": "sort",
            "in": "query",
            "description": "popular (default) | newest | random",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated tags to filter by",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "nsfw",
            "in": "query",
            "description": "'true' for nsfw only, 'false' to exclude",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max results (default 20, max 50)",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Communities to discover",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "communities": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/feed": {
      "get": {
        "summary": "Cross-community feed",
        "description": "Approved posts across communities. `scope=mine` is the communities you are a member of; `scope=all` (default) is those plus every public one. Both read your membership rows, never a stored slug list, so a community you left stops appearing immediately.",
        "operationId": "communityFeed",
        "parameters": [
          {
            "name": "scope",
            "in": "query",
            "description": "mine | all (default)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "new (default) | hot",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filter on post title or tags",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "skip",
            "in": "query",
            "description": "Posts to skip",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max results (default 20, max 50)",
            "schema": {
              "type": "integer",
              "default": 20,
              "maximum": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Feed items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/join": {
      "post": {
        "summary": "Join a community",
        "description": "Join a PUBLIC community. No `expectedUpdatedAt` -- nothing prior is replaced, and the unique `(communityId, uid)` index makes a duplicate answer 409 rather than double-counting. A private community answers 403 and must be asked through its access gate; a secret one answers 404.",
        "operationId": "joinCommunity",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Joined",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Private community -- request access instead",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already a member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/leave": {
      "post": {
        "summary": "Leave a community",
        "description": "Leave a community you are a member of. No `expectedUpdatedAt`. The creator cannot leave their own community.",
        "operationId": "leaveCommunity",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Left",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a member of this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The creator cannot leave",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/my-membership": {
      "get": {
        "summary": "Read your own membership",
        "description": "Your role, when you joined, how your name and photo render here, the bio on your member card, who it is shown to, and how you appear in your other communities. Requires membership -- visibility alone does not give you a card.",
        "operationId": "readMyCommunityMembership",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Your membership",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "membership": {
                      "type": "object"
                    },
                    "globalBio": {
                      "type": "string"
                    },
                    "directory": {
                      "type": "object"
                    },
                    "otherCommunities": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "identity": {
                      "type": "object",
                      "description": "Visibility, steward name and image ceilings, a stable per-community pseudonym, and the resolved name and photo. Steward policy narrows rendering without changing stored member choices."
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a member of this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update your own membership",
        "description": "How you present yourself in THIS community and no other. No `expectedUpdatedAt`, deliberately: the row carries no field that moves on this write, so there is no honest value to derive a token from, and only you may write your own row. `snippetVisibility: hidden` removes you from the directory and no steward can override it. `nameForm`, `imageNameForm`, `photo`, and `imagePhoto` are member choices; steward ceilings narrow them at render time without changing stored values.",
        "operationId": "updateMyCommunityMembership",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "snippetSource": {
                    "type": "string",
                    "enum": [
                      "global",
                      "local",
                      "none"
                    ],
                    "description": "Which bio the card shows: the MythOS profile bio, one written for this community, or none. `none` does not fall back to the global bio."
                  },
                  "snippet": {
                    "type": "string",
                    "description": "Plain text -- rendered as a text node, never as markdown"
                  },
                  "snippetVisibility": {
                    "type": "string",
                    "enum": [
                      "all",
                      "members",
                      "hidden"
                    ]
                  },
                  "snippetTags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "snippetLink": {
                    "type": "string"
                  },
                  "linkSource": {
                    "type": "string",
                    "enum": [
                      "creator",
                      "profile",
                      "custom",
                      "none"
                    ],
                    "description": "Which page the card links to. `custom` uses `snippetLink`; absent reads as `custom` when a link is stored and `none` when it is not."
                  },
                  "nameForm": {
                    "type": "string",
                    "enum": [
                      "full",
                      "initial",
                      "first",
                      "pseudo",
                      "anon"
                    ],
                    "description": "The form of your name here. Absent means the community default -- full in a public community, first name plus last initial in a private or secret one."
                  },
                  "photo": {
                    "type": "boolean",
                    "description": "Whether your profile photo shows here. Absent reads as shown. Forced off wherever the name renders anonymously."
                  },
                  "imageNameForm": {
                    "type": "string",
                    "enum": [
                      "full",
                      "initial",
                      "first",
                      "pseudo",
                      "anon"
                    ],
                    "description": "Name form for image-post bylines. Absent follows nameForm; a stable pseudonym carries through comment threads."
                  },
                  "imagePhoto": {
                    "type": "boolean",
                    "description": "Profile photo on image-post bylines. Absent follows photo; pseudonym and anonymous forms never show it."
                  },
                  "visibilityIntroSeen": {
                    "type": "boolean",
                    "description": "Send `true` to record that you have seen the join-time visibility explainer for this community. The server stamps the time and the steward name-hiding policy in force; a client-supplied timestamp is never read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "membership": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid snippet field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a member of this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/members/directory": {
      "get": {
        "summary": "List community members",
        "description": "The members directory. Every per-viewer decision is made on the SERVER and the payload built from it: a member whose snippet visibility is `hidden` is absent entirely, one set to `members` returns with no snippet to a non-member, and a directory whose stewards closed the tab answers `access` other than `open` with no member data at all. `manage=1` is the steward roster instead -- it keeps unlisted members, since you cannot remove someone you cannot see, and carries no snippets. `meta=1` is the three-number settings summary.",
        "operationId": "listCommunityMembers",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "description": "active | alpha | new | posts",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter",
            "in": "query",
            "description": "all | stewards | trusted | new",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filter by name or handle. Ignored when the stewards turned search off.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Members to skip (page size 24)",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "manage",
            "in": "query",
            "description": "Steward roster rather than the public directory",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "meta",
            "in": "query",
            "description": "Steward settings summary only",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Members",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "access": {
                      "type": "string"
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "counts": {
                      "type": "object",
                      "nullable": true
                    },
                    "total": {
                      "type": "integer",
                      "nullable": true
                    },
                    "hasMore": {
                      "type": "boolean"
                    },
                    "truncated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Steward-only variant requested by a non-steward",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/members/growth": {
      "get": {
        "summary": "Member growth",
        "description": "Members joined over time. Steward-only: the member count may be public, but a dated curve is a different disclosure. Counts memberships that SURVIVE -- leaving deletes the row, so there is no departure ledger and the curve can only rise.",
        "operationId": "communityMemberGrowth",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "range",
            "in": "query",
            "description": "week | month (default) | year | all",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Growth series",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "growth": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Moderation role required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/settings": {
      "patch": {
        "summary": "Update community settings",
        "description": "Identity, rules, tags, visibility, the members-directory block, the calendar block and the About page. Creator, moderator or steward, except `visibility`, `nsfw` and `stewards`, which are the creator's alone: a moderator or steward sending any of them gets a 403 naming the fields and nothing in that request is written.\n\nAddressed as `/settings` rather than as a PATCH on `/{slug}` deliberately: the agent tree's `/{slug}` is a READ, and one address carrying both a read for anyone who may see the community and a write that rewrites who may post there conflates two very different authorities.\n\nREQUIRES `expectedUpdatedAt`. A stale write here does not lose a sentence, it reinstates a visibility or a posting policy somebody has since changed. The token rides the findOneAndUpdate FILTER, so there is no window between checking and writing, and a 409 carries `currentUpdatedAt` while a genuinely missing community answers 404 -- an agent told the wrong one either retries forever or gives up on a write it could have made.\n\n`memberDirectory`, `calendar` and `preview` are each a COMPLETE block: a partial one is REJECTED WHOLE rather than merged, so a half-rendered form cannot reset the rest. Changing visibility away from `private` closes pending access requests, and that cleanup runs only AFTER the CAS write succeeds.",
        "operationId": "updateCommunitySettings",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "rules": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "The complete ordered rule list"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "private",
                      "secret"
                    ]
                  },
                  "nsfw": {
                    "type": "boolean"
                  },
                  "guidelines": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "accessWelcomeMessage": {
                    "type": "string",
                    "description": "Shown on a private community's gate; max 400 characters"
                  },
                  "accessReviewCadence": {
                    "type": "string"
                  },
                  "collectionsRequired": {
                    "type": "boolean"
                  },
                  "avatarUrl": {
                    "type": "string",
                    "description": "Must resolve to an upload the caller confirmed; empty string removes it"
                  },
                  "bannerUrl": {
                    "type": "string",
                    "description": "The header image drawn across the top of the community page. Must resolve to an upload the caller confirmed; empty string removes it. Never used as a link-preview image"
                  },
                  "appearInFeeds": {
                    "type": "boolean"
                  },
                  "appearInRecommendations": {
                    "type": "boolean"
                  },
                  "aboutPage": {
                    "type": "string",
                    "description": "Markdown; may carry memo mention chips, gated per target"
                  },
                  "memberDirectory": {
                    "type": "object",
                    "description": "The COMPLETE members-directory block"
                  },
                  "hideMemberNames": {
                    "type": "boolean",
                    "description": "Legacy all-anonymous steward override; applied at read time without changing member choices"
                  },
                  "memberIdentity": {
                    "type": "object",
                    "description": "Complete steward privacy policy. nameCeiling and imageCeiling each accept full, initial, first, pseudo, or anon; choices more identifying than the ceiling render at the ceiling without changing storage."
                  },
                  "calendar": {
                    "type": "object",
                    "description": "The COMPLETE calendar block"
                  },
                  "preview": {
                    "type": "object",
                    "description": "The COMPLETE join-link preview block: { preview, comments, order, on, card? }. `card` is { show, title, description, imageUrl } and decides what a private community's join link, token preview pages and event guest links unfurl as. `show` defaults off: a neutral card naming neither the community nor its topic. Overrides are optional; an empty string uses the community's own value. A new imageUrl must be the caller's confirmed upload, ideally 1200x630 (served as summary_large_image; without one the logo serves as a summary card). A block without `card` keeps the stored one; a partial card is a 400. Every invite page stays noindex."
                  },
                  "confirmHiddenMentions": {
                    "type": "boolean",
                    "description": "Re-send with true only after a hiddenMentions refusal"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "community": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing lock token, an invalid value, a rejected settings block, or a hidden-mention refusal carrying hiddenMentions",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Moderation role required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Community not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale lock token -- body carries currentUpdatedAt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/steward-notifications": {
      "get": {
        "summary": "Read your steward alert preferences",
        "description": "The caller's OWN preferences for this community. Moderation roles only. Nothing reads these yet -- every row is labelled \"coming soon\" on the settings screen -- so this reports a stored intention rather than an alert that fires.",
        "operationId": "readCommunityStewardNotifications",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Preferences",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Moderation role required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Write your steward alert preferences",
        "description": "No `expectedUpdatedAt` -- only this steward may write this row, so the two writers a token would arbitrate between are the same person, and the row carries no field that moves on the write to derive one from. The block is validated WHOLE: a partial payload is rejected rather than merged. Read the current block first and amend it.",
        "operationId": "updateCommunityStewardNotifications",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "stewardNotifications"
                ],
                "properties": {
                  "stewardNotifications": {
                    "type": "object",
                    "description": "The complete preferences block"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "settings": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The preferences block was rejected whole",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Moderation role required, or not a member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/invite-suggestions": {
      "get": {
        "summary": "Invite suggestions",
        "description": "People you could invite -- collaborators, active subscribers and accepted contacts, the same three sources the username invite gate enforces. Existing members and anyone holding a pending invite are excluded. Requires creator, moderator or steward. Never returns an invite token.",
        "operationId": "communityInviteSuggestions",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Filter by handle, name or email",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Suggestions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/collections": {
      "get": {
        "summary": "List collections",
        "description": "A community's collections in steward order, with derived post counts. `viewerTrusted` reports whether THIS caller's submissions publish or queue. A pending count is a moderation signal and reads 0 for a non-steward.",
        "operationId": "listCommunityCollections",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Collections",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "collectionsRequired": {
                      "type": "boolean"
                    },
                    "viewerTrusted": {
                      "type": "boolean"
                    },
                    "holdFirstPost": {
                      "type": "boolean"
                    },
                    "collections": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a collection",
        "description": "Stewards only. No `expectedUpdatedAt` -- nothing prior is replaced, and the unique (communityId, slug) index answers a tag collision with 409 rather than a read-then-write check two stewards can interleave through.",
        "operationId": "createCommunityCollection",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "description": "Tag slug; derived from the name when omitted"
                  },
                  "description": {
                    "type": "string"
                  },
                  "coverImageUrl": {
                    "type": "string",
                    "description": "Must resolve to an upload the caller confirmed"
                  },
                  "submitPolicy": {
                    "type": "string",
                    "description": "Who may file posts into it"
                  },
                  "reviewPolicy": {
                    "type": "string",
                    "description": "Whether a filed post publishes or queues"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "collection": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid input, or an unverifiable cover image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A collection already uses that tag",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Reorder collections",
        "description": "Writes the COMPLETE ordered list in one request, so two stewards dragging at once cannot interleave into an order neither chose. A partial list is a 400. No `expectedUpdatedAt` for the same reason -- the write is total rather than a delta.",
        "operationId": "reorderCommunityCollections",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "order"
                ],
                "properties": {
                  "order": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Every collection id in this community, exactly once"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reordered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "order must list every collection exactly once",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/collections/{collectionId}": {
      "get": {
        "summary": "Read a collection",
        "description": "By id or by tag slug. Carries `updatedAt`, the lock token the write endpoints require.",
        "operationId": "readCommunityCollection",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionId",
            "in": "path",
            "description": "Collection id or tag slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The collection",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "collection": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Edit a collection",
        "description": "Stewards only. REQUIRES `expectedUpdatedAt` -- this replaces stored content, so two stewards editing the same collection is exactly the lost update a hard lock exists for. The token rides the update FILTER, never a pre-check.",
        "operationId": "updateCommunityCollection",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionId",
            "in": "path",
            "description": "Collection id or tag slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "description": "Tag slug; derived from the name when omitted"
                  },
                  "description": {
                    "type": "string"
                  },
                  "coverImageUrl": {
                    "type": "string",
                    "description": "Must resolve to an upload the caller confirmed"
                  },
                  "submitPolicy": {
                    "type": "string",
                    "description": "Who may file posts into it"
                  },
                  "reviewPolicy": {
                    "type": "string",
                    "description": "Whether a filed post publishes or queues"
                  },
                  "gallery": {
                    "type": "object"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "collection": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing lock token, invalid input, or an unverifiable cover image",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale lock token -- body carries currentUpdatedAt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a collection",
        "description": "Its posts are UNFILED, never deleted, and anything still queued for it is approved on the way out rather than stranded in a queue nothing renders. REQUIRES `expectedUpdatedAt` as a query param; the collection delete runs FIRST and the post rewrites follow only on success, so a stale token leaves no partial state across documents.",
        "operationId": "deleteCommunityCollection",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionId",
            "in": "path",
            "description": "Collection id or tag slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "description": "The collection's current updatedAt",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing lock token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Stale lock token -- body carries currentUpdatedAt",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/collections/{collectionId}/posts": {
      "get": {
        "summary": "List posts that could be filed into a collection",
        "description": "Stewards only. The community's APPROVED posts as filing candidates, newest first -- what COULD go in, which is the opposite question from `GET /posts?collection=`, which answers what already is. Each candidate carries the collection it currently sits in, so gathering a post that already has a label is a visible move rather than a surprise. `q` matches title, author display name and author username.",
        "operationId": "listCollectionFilingCandidates",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionId",
            "in": "path",
            "description": "Collection id or tag slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Match on post title or author",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "1-50, default 30",
            "required": false,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "skip",
            "in": "query",
            "description": "Offset for paging",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Candidate posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "collection": {
                      "type": "object",
                      "description": "id, name and slug of the target collection"
                    },
                    "posts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Candidates, each with its current collectionId/collectionName/collectionSlug or null"
                    },
                    "total": {
                      "type": "integer",
                      "description": "Candidates matching the filter, before paging"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Not a steward of this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such collection, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "File already-published posts into a collection",
        "description": "Stewards only. NO `expectedUpdatedAt`: this sets a scalar on posts rather than replacing anything stored on the collection, and filing the same posts twice moves nothing the second time. `status` is NEVER touched -- a review policy gates what a SUBMITTER files, not whether a steward may gather a live post. ALL OR NOTHING: every named post is resolved first, so a request naming one post from another community writes none of them.",
        "operationId": "fileCommunityPostsIntoCollection",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collectionId",
            "in": "path",
            "description": "Collection id or tag slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "postIds"
                ],
                "properties": {
                  "postIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Post ids of APPROVED posts in this community; 1-50 per request"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Filed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "filed": {
                      "type": "integer",
                      "description": "Posts written into this collection"
                    },
                    "moved": {
                      "type": "integer",
                      "description": "How many of those carried a DIFFERENT collection before"
                    },
                    "alreadyFiled": {
                      "type": "integer",
                      "description": "Named posts already in this collection"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "postIds missing, malformed, empty, or over 50",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Not a steward of this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such collection, or a named post is not published in this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/members": {
      "get": {
        "summary": "The moderation roster",
        "description": "Every member with their uid and role. MODERATION ROLES ONLY, and distinct from `/members/directory`: the roster applies none of the per-member disclosure rules and therefore KEEPS a member who asked not to be listed, because a steward who cannot see someone cannot remove them.",
        "operationId": "listCommunityRoster",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Roster",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "members": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Add a member directly",
        "description": "By username. Creator and moderator only -- deliberately NARROWER than removal, because a tier that can mint its own peers is not a tier. No `expectedUpdatedAt`; the unique (communityId, uid) index answers a race with the same 409 the pre-check would.",
        "operationId": "addCommunityMember",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "username"
                ],
                "properties": {
                  "username": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "member": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "username is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such community, or no such user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already a member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/members/{uid}": {
      "patch": {
        "summary": "Change a member's role",
        "description": "Creator and moderator only -- NARROWER than removal on purpose: a steward may remove a member but may not promote one. Moderators and stewards are one tier, and below the creator a caller may neither target nor mint it, so every moderator or steward change is the creator's (403 otherwise); a moderator stepping themselves down is unaffected. The creator's role cannot be changed. No `expectedUpdatedAt`: the membership row carries no field that moves on a role change, and the write is a total assignment of a stated value.",
        "operationId": "setCommunityMemberRole",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uid",
            "in": "path",
            "description": "The member's uid",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "moderator",
                      "steward",
                      "member"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Changed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "role": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Role must be moderator, steward or member",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role, or the target is the creator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Member not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a member",
        "description": "Creator, moderator and steward -- one tier, at parity with the invite side. The creator cannot be removed, and only the creator can remove a moderator or steward (removing yourself is unaffected). No `expectedUpdatedAt`; the end state is the point. Their posts and comments survive.",
        "operationId": "removeCommunityMember",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uid",
            "in": "path",
            "description": "The member's uid",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role, or the target is the creator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Member not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/access-requests": {
      "post": {
        "summary": "Request access to a private community",
        "description": "No `expectedUpdatedAt` -- the partial unique index on a pending request makes a duplicate impossible rather than merely unlikely. A secret community answers 404, and so does a public one that needs no request: one answer for three cases, on purpose. A declined request holds for seven days, answered 429 with `retryAfterMs`.",
        "operationId": "requestCommunityAccess",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "A short note to the stewards"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Requested",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "request": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already a member, or a request is already pending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Declined recently -- body carries retryAfterMs",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Pending access requests",
        "description": "Creator, moderator and steward. Each row carries the requester and their note.",
        "operationId": "listCommunityAccessRequests",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pending requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "requests": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/access-requests/{requestId}": {
      "patch": {
        "summary": "Approve or deny an access request",
        "description": "Creator, moderator and steward. Approving inserts the membership; denying starts a seven-day cooldown. No `expectedUpdatedAt`, and this is the one review decision in the domain that takes none: the pending-to-decided transition rides INSIDE the findOneAndUpdate filter, so two moderators deciding at once produce exactly one winner and the loser is told there is no pending request.",
        "operationId": "decideCommunityAccessRequest",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "description": "The access-request id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "decision"
                ],
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "deny"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Decided",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "request": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request id or decision",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No pending request found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/join-link": {
      "get": {
        "summary": "Read community join link",
        "description": "Creator, moderator and steward. Reads the one standing not-revoked join link for the community, including the token and URL because this is the management surface. Public token metadata never discloses the token or slug.",
        "operationId": "readCommunityJoinLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The standing join link, or null when disabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "joinLink": {
                      "type": "object",
                      "nullable": true,
                      "properties": {
                        "token": {
                          "type": "string"
                        },
                        "url": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "active",
                            "expired",
                            "exhausted"
                          ]
                        },
                        "uses": {
                          "type": "number"
                        },
                        "maxUses": {
                          "type": "number",
                          "nullable": true
                        },
                        "expiresAt": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create or update community join link",
        "description": "Creator, moderator and steward. Creates or updates the standing join link. Pass `regenerate: true` to revoke the old token and mint a new one. The link always grants `member`; use limits and expiry are enforced at claim time. No `expectedUpdatedAt`: one not-revoked link per community is enforced by a partial unique index.",
        "operationId": "createCommunityJoinLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires": {
                    "type": "string",
                    "enum": [
                      "never",
                      "24h",
                      "7d",
                      "30d"
                    ]
                  },
                  "maxUses": {
                    "type": "number",
                    "nullable": true,
                    "description": "25, 100, 250, or null for no limit"
                  },
                  "regenerate": {
                    "type": "boolean",
                    "description": "When true, revoke the current token before creating the new one"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing link updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "joinLink": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Link created or regenerated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "joinLink": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid expiry or use limit",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Revoke community join link",
        "description": "Creator, moderator and steward. Revokes the standing join link. Existing members stay members; holders of the old URL see that it is no longer valid. No `expectedUpdatedAt` because revoke is an idempotent guarded write over the one live link.",
        "operationId": "revokeCommunityJoinLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked or already disabled",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/invites": {
      "get": {
        "summary": "Pending invites",
        "description": "Creator, moderator and steward. Returns `inviteId` for revoking and NEVER the token -- the token is the secret that grants acceptance and belongs only in the invitee's inbox.",
        "operationId": "listCommunityInvites",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pending invites",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "invites": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Invite someone to a community",
        "description": "Creator, moderator and steward. THIS SENDS EMAIL to a real person and cannot be taken back, so it carries its own hourly rate bucket. Inviting by EMAIL is open; by USERNAME only for someone connected to the caller as a collaborator, an active subscriber, or an accepted contact. Inviting as `steward` is restricted to the creator, so an invite is not a side door into a tier its sender could not grant directly. No `expectedUpdatedAt`.",
        "operationId": "inviteToCommunity",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "invitee"
                ],
                "properties": {
                  "invitee": {
                    "type": "string",
                    "description": "An email address or a MythOS handle"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "member",
                      "steward"
                    ]
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Invite sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string"
                    },
                    "inviteeKind": {
                      "type": "string"
                    },
                    "emailSent": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invitee is required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role, or no connection on the username path",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found, or not yours to see",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already a member, or an invite is already pending",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The invitation email could not be sent; the invite was rolled back",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/invites/{inviteId}": {
      "delete": {
        "summary": "Revoke a pending invite",
        "description": "Creator, moderator and steward. No `expectedUpdatedAt` -- the delete is scoped to the community AND to unused invites in one guarded write, so an invite already accepted cannot be revoked out from under the person who used it, and an id from another community reads as nonexistent.",
        "operationId": "revokeCommunityInvite",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "description": "The community slug",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "inviteId",
            "in": "path",
            "description": "The invite id from the pending list",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Insufficient role",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such unused invite in this community",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/community-invites": {
      "get": {
        "summary": "Read an invite token",
        "description": "What a token points at -- which community, and which address it was sent to -- so a caller can see what it is accepting before accepting. Does not consume the token. Lives at the top level because the token IS the address: the caller does not know the slug yet, which is the reason to ask.",
        "operationId": "readCommunityInviteToken",
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "description": "The invite token from the link",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Token metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "communitySlug": {
                      "type": "string"
                    },
                    "communityName": {
                      "type": "string"
                    },
                    "invitedEmail": {
                      "type": "string"
                    },
                    "invitedStatus": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing invite token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Invite not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/community-invites/accept": {
      "post": {
        "summary": "Accept an invite",
        "description": "Binds to the caller's own uid -- an account-less invitee only becomes that uid by signing up with the invited email, so no other account can accept. No `expectedUpdatedAt`: the accept is idempotent by key, a concurrent double-accept loses on the unique index and is treated as success, and a token already consumed answers 410.",
        "operationId": "acceptCommunityInvite",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "token"
                ],
                "properties": {
                  "token": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "communitySlug": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing invite token",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "This invitation is bound to another account",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Invite not found or expired",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "This invitation has already been used",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts": {
      "get": {
        "summary": "List community posts",
        "description": "Approved posts in a community you can see. Each row carries `updatedAt`, the lock token the write endpoints require. Pass `full=1` to read through the shared enricher and get the complete card payload.",
        "operationId": "listCommunityPosts",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search query to filter posts by title",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of posts to return (1-200, default 50)",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            }
          },
          {
            "name": "full",
            "in": "query",
            "description": "Return the full enriched payload rather than the lean summary",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of approved posts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CommunityPost"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a post",
        "operationId": "createCommunityPost",
        "description": "Five kinds. `memo` submits one of your own memos; the four native kinds need membership of the community. No kind is tier-gated. A text body or image caption carrying a `hidden` memo mention is refused with a `hiddenMentions` list to confirm and retry; a `private` memo is never allowed. No expectedUpdatedAt.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "actAsLibrary": {
                    "type": "string",
                    "description": "Write as this library's owner through an approved Manage Social grant. A refusal is 403 ACT_AS_DENIED and never falls back to the caller. Omit to write as yourself."
                  },
                  "requestReview": {
                    "type": "boolean",
                    "description": "true queues the post for steward review whatever the community's policy would do. Additive only."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "memo",
                      "text",
                      "image",
                      "link",
                      "poll"
                    ]
                  },
                  "memoId": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  },
                  "content": {
                    "type": "string"
                  },
                  "imageUrl": {
                    "type": "string"
                  },
                  "alt": {
                    "type": "string"
                  },
                  "caption": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "duration": {
                    "type": "string"
                  },
                  "context": {
                    "type": "string"
                  },
                  "collectionId": {
                    "type": "string"
                  },
                  "confirmHiddenMentions": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created post"
          },
          "400": {
            "description": "Validation failed, or HIDDEN_MENTION_CONFIRM with hiddenMentions"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/memos/index": {
      "get": {
        "summary": "Get memo index",
        "description": "Returns a lightweight index of all memos with IDs, titles, tags, and update timestamps. No content included.",
        "operationId": "getMemoIndex",
        "responses": {
          "200": {
            "description": "Memo index",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "memos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "tags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "updatedAt": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/journal": {
      "get": {
        "summary": "Read, search, or list daily memos",
        "description": "Returns a daily memo for a specific date, or owner-only daily memo search/list/tag results when mode=search, list, or tags.",
        "operationId": "readOrSearchDailyMemos",
        "parameters": [
          {
            "name": "mode",
            "in": "query",
            "description": "Optional daily memo mode. Omit for exact-date read.",
            "schema": {
              "type": "string",
              "enum": [
                "search",
                "list",
                "tags"
              ]
            }
          },
          {
            "name": "date",
            "in": "query",
            "description": "Date in YYYY-MM-DD format. Required when mode is omitted.",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "username",
            "in": "query",
            "description": "Username (defaults to configured default user)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Content substring for mode=search. Required unless tags is provided.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Start date for search/list in YYYY-MM-DD format",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "End date for search/list in YYYY-MM-DD format",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "description": "Comma-separated daily memo tags for search/list",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagMode",
            "in": "query",
            "description": "Required when two or more tags are provided",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "any"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Max daily memo summaries for search/list (default 50, max 200)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Daily memo content, daily memo summaries, or daily tag counts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "dailyMemo": {
                      "type": "object",
                      "properties": {
                        "_id": {
                          "type": "string",
                          "nullable": true,
                          "description": "Format: {username}-{YYYY-MM-DD}, or null if no memo exists"
                        },
                        "date": {
                          "type": "string"
                        },
                        "content": {
                          "type": "string",
                          "nullable": true,
                          "description": "Markdown content"
                        },
                        "createdAt": {
                          "type": "number",
                          "nullable": true
                        },
                        "updatedAt": {
                          "type": "number",
                          "nullable": true
                        }
                      }
                    },
                    "dailyMemos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "date": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string"
                          },
                          "firstLine": {
                            "type": "string"
                          },
                          "snippet": {
                            "type": "string"
                          },
                          "tags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "createdAt": {
                            "type": "number"
                          },
                          "updatedAt": {
                            "type": "number"
                          }
                        }
                      }
                    },
                    "tags": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "count": {
                            "type": "number"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid date",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "User not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Insert into daily memo section",
        "description": "Inserts a checklist task or bullet entry into a specific section of a daily memo. Auto-creates the daily memo from the user's default template if none exists for the date. Supports positional insertion via afterLine.",
        "operationId": "insertDailyMemoEntry",
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "required": true,
            "description": "Date in YYYY-MM-DD format",
            "schema": {
              "type": "string",
              "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
          },
          {
            "name": "username",
            "in": "query",
            "description": "Username (defaults to configured default user)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "section",
                  "entry"
                ],
                "properties": {
                  "section": {
                    "type": "string",
                    "description": "Section heading to insert into (e.g. 'Tasks', 'Timeline')"
                  },
                  "entry": {
                    "type": "string",
                    "description": "Entry text (without bullet/checkbox prefix)"
                  },
                  "entryType": {
                    "type": "string",
                    "enum": [
                      "task",
                      "bullet"
                    ],
                    "default": "bullet",
                    "description": "'task' adds '- [ ] ' prefix, 'bullet' adds '* ' prefix"
                  },
                  "afterLine": {
                    "type": "string",
                    "description": "Insert after a line matching this text exactly (trimmed). If not found, appends at end of section."
                  },
                  "sourceMarkupVersion": {
                    "type": "integer",
                    "description": "Optional. Markup version of the entry text. Omit (or 1) for legacy behavior — bare #word is auto-linked into a tag. Set to 2 to keep bare #word as literal text."
                  }
                }
              },
              "examples": {
                "addTask": {
                  "summary": "Add a checklist task",
                  "value": {
                    "section": "Tasks",
                    "entry": "Review the PR",
                    "entryType": "task"
                  }
                },
                "addTimeline": {
                  "summary": "Add a timeline entry",
                  "value": {
                    "section": "Timeline",
                    "entry": "12:30 PM — Lunch with Richard Titus"
                  }
                },
                "insertAfter": {
                  "summary": "Insert after a specific line",
                  "value": {
                    "section": "Timeline",
                    "entry": "9:45 AM — Quick sync with design team",
                    "afterLine": "* 9:00 AM — Morning standup"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Entry added successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "message": {
                      "type": "string"
                    },
                    "section": {
                      "type": "string"
                    },
                    "entry": {
                      "type": "string",
                      "description": "The formatted entry as inserted (with prefix)"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present if afterLine was not found"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid parameters",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "User not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Concurrent edit conflict",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "putJournal",
        "summary": "PUT /api/internal/journal",
        "description": "Replace a daily memo body. Query date (YYYY-MM-DD) is required; username targets a library. JSON content and expectedUpdatedAt are required; null token is supported only for first creation. Cross-library writes require the appropriate grant capability and dailyMemoAccess. Returns the daily memo with the advanced token.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {},
                  "expectedUpdatedAt": {},
                  "sourceMarkupVersion": {}
                }
              },
              "description": "Replace a daily memo body. Query date (YYYY-MM-DD) is required; username targets a library. JSON content and expectedUpdatedAt are required; null token is supported only for first creation. Cross-library writes require the appropriate grant capability and dailyMemoAccess. Returns the daily memo with the advanced token."
            }
          }
        }
      }
    },
    "/api/internal/newsletter/subscribers": {
      "post": {
        "summary": "Create a subscriber",
        "description": "Adds a subscriber to the caller's newsletter. Sets importSource=api automatically. Requires an Oracle subscription. REQUIRES an explicit boolean for bypassDoubleOptIn and skipWelcomeEmail — adding a person to a mailing list on their behalf is a decision that must be stated, never defaulted. With bypassDoubleOptIn=false the subscriber is created 'pending' and must confirm by email; unconfirmed pending records are deleted after 30 days.",
        "operationId": "createNewsletterSubscriber",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email",
                  "firstName",
                  "bypassDoubleOptIn",
                  "skipWelcomeEmail"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "firstName": {
                    "type": "string"
                  },
                  "lastName": {
                    "type": "string"
                  },
                  "company": {
                    "type": "string"
                  },
                  "phone": {
                    "type": "string",
                    "description": "Stored as E.164. A bare 10-digit number is assumed North American; anything else must carry a country code (e.g. +442079460958). Unparseable input is rejected."
                  },
                  "location": {
                    "type": "string"
                  },
                  "customFields": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Values for custom fields already defined in the caller's newsletter settings. Unknown keys are rejected, not dropped."
                  },
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic slugs the caller owns. Omit for creator-level subscription."
                  },
                  "frequency": {
                    "type": "string",
                    "enum": [
                      "instant",
                      "daily",
                      "weekly",
                      "monthly",
                      "none"
                    ],
                    "default": "weekly"
                  },
                  "digestMode": {
                    "type": "string",
                    "enum": [
                      "bundled",
                      "separate"
                    ],
                    "default": "separate"
                  },
                  "bypassDoubleOptIn": {
                    "type": "boolean",
                    "description": "Required. true = active immediately with no confirmation email; only legitimate for an audience that has already consented. false = send the confirmation email and leave them pending."
                  },
                  "skipWelcomeEmail": {
                    "type": "boolean",
                    "description": "Required. true = suppress the welcome email for this subscriber."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Subscriber created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "subscriptionId": {
                      "type": "string"
                    },
                    "pending": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already subscribed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "List subscribers",
        "description": "Lists the caller's newsletter subscribers, newest first. Returns the subscriptionId the update and delete endpoints require. Paginated — pass the returned nextOffset to page forward; it is null when no pages remain. Requires an Oracle subscription.",
        "operationId": "listNewsletterSubscribers",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Default 'active'. 'pending' = added but not yet confirmed via double opt-in.",
            "schema": {
              "type": "string",
              "enum": [
                "active",
                "pending",
                "unsubscribed",
                "all"
              ],
              "default": "active"
            }
          },
          {
            "name": "topic",
            "in": "query",
            "description": "Topic slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "frequency",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "instant",
                "daily",
                "weekly",
                "monthly",
                "none"
              ]
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring match on email address",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of subscribers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    },
                    "nextOffset": {
                      "type": "integer",
                      "nullable": true
                    },
                    "subscribers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/NewsletterSubscriber"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid status or frequency",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/newsletter/stats": {
      "get": {
        "summary": "Newsletter audience stats",
        "description": "Active total, pending confirmations, breakdown by cadence, scope and topic, and 7/30-day growth against the prior period. Requires an Oracle subscription.",
        "operationId": "getNewsletterStats",
        "responses": {
          "200": {
            "description": "Audience summary",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "integer"
                    },
                    "pendingCount": {
                      "type": "integer"
                    },
                    "byFrequency": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "byScope": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "byTopic": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "count": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "allUpdatesCount": {
                      "type": "integer"
                    },
                    "growth": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/newsletter/topics": {
      "get": {
        "summary": "List newsletter topics",
        "description": "Lists the caller's newsletter topics with subscriber counts. Topic slugs are opaque ids (t_a3f9b2c1d0), so this is how a topic NAME resolves to the slug the subscriber endpoints expect. Never construct a slug. Requires an Oracle subscription.",
        "operationId": "listNewsletterTopics",
        "responses": {
          "200": {
            "description": "Topics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "topics": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "name": {
                            "type": "string"
                          },
                          "hashtags": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "hidden": {
                            "type": "boolean"
                          },
                          "threshold": {
                            "type": "integer",
                            "nullable": true
                          },
                          "subscriberCount": {
                            "type": "integer"
                          }
                        }
                      }
                    },
                    "broadcastsOnlyCount": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/newsletter/subscribers/{id}": {
      "patch": {
        "summary": "Update a subscriber",
        "description": "Updates an existing subscription owned by the caller. At least one field must be provided. Email is immutable. For topics, pass either `topics` (replaces the whole set) or `addTopics`/`removeTopics` (amends it) — passing both is a 400. Slugs being ADDED must be owned by the caller; a slug being REMOVED need not be, so subscribers stranded on a deleted topic can still be cleaned up. Pass null to company/phone/location to clear the value. Requires an Oracle subscription.",
        "operationId": "updateNewsletterSubscriber",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "MongoDB ObjectId of the subscription",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "firstName": {
                    "type": "string"
                  },
                  "lastName": {
                    "type": "string"
                  },
                  "company": {
                    "type": "string",
                    "nullable": true
                  },
                  "phone": {
                    "type": "string",
                    "nullable": true,
                    "description": "Stored as E.164. A bare 10-digit number is assumed North American; anything else must carry a country code. Unparseable input is rejected."
                  },
                  "location": {
                    "type": "string",
                    "nullable": true
                  },
                  "customFields": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string"
                    },
                    "description": "Values for custom fields already defined in the caller's newsletter settings. Unknown keys are rejected."
                  },
                  "topics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces existing topic preferences. Empty array resets to creator-level scope. Do not combine with addTopics/removeTopics."
                  },
                  "addTopics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic slugs to add, leaving the subscriber's other topics untouched."
                  },
                  "removeTopics": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Topic slugs to remove, leaving the subscriber's other topics untouched."
                  },
                  "frequency": {
                    "type": "string",
                    "enum": [
                      "instant",
                      "daily",
                      "weekly",
                      "monthly",
                      "none"
                    ]
                  },
                  "digestMode": {
                    "type": "string",
                    "enum": [
                      "bundled",
                      "separate"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Subscriber updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error or no fields to update",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Subscription not found or belongs to another creator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Remove a subscriber",
        "description": "Unsubscribes a subscriber and flags the record removedByCreator. A soft removal, not a hard delete: growth metrics distinguish 'they left' from 'the creator removed them', and destroying the row would let a removed person silently re-subscribe as new. Requires an Oracle subscription.",
        "operationId": "removeNewsletterSubscriber",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "MongoDB ObjectId of the subscription",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Subscriber unsubscribed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid id",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized, or the caller has no Oracle subscription (NEWSLETTER_NOT_ENTITLED)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Subscription not found or belongs to another creator",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/memos/changes": {
      "get": {
        "summary": "Delta sync",
        "description": "Returns memos that have changed since the given timestamp. Used for incremental synchronization.",
        "operationId": "deltaSyncMemos",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "description": "ISO 8601 timestamp to get changes since",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of results",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Changed memos since timestamp",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "example": true
                    },
                    "memos": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MemoSummary"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events": {
      "get": {
        "summary": "List a month of a community calendar",
        "description": "Occurrences you may see for the given month, plus `viewer.canCreate` — whether you may create, may only propose, or may do neither is the community's policy.",
        "operationId": "listCommunityEvents",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "year",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Four-digit year (defaults to the current UTC month)"
          },
          {
            "name": "month",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 12
            },
            "description": "Month 1-12"
          }
        ],
        "responses": {
          "200": {
            "description": "The month's occurrences"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "post": {
        "summary": "Create or propose an event",
        "description": "No expectedUpdatedAt — there is no prior document to lock. Publishing versus queueing for a steward is the community's policy and rides back as `queued`; it is not an error. Send `requestReview: true` to queue your own event whatever that policy says.",
        "operationId": "createCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "dtstart",
                  "dtend",
                  "timezone"
                ],
                "properties": {
                  "actAsLibrary": {
                    "type": "string",
                    "description": "Write as this library's owner through an approved Manage Social grant. A refusal is 403 ACT_AS_DENIED and never falls back to the caller. Omit to write as yourself."
                  },
                  "title": {
                    "type": "string"
                  },
                  "dtstart": {
                    "type": "object",
                    "description": "Wall-clock start { year, month, day, hour, minute }"
                  },
                  "dtend": {
                    "type": "object"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA zone — an event happens at a wall-clock time in a place"
                  },
                  "rrule": {
                    "type": "string",
                    "description": "RFC 5545 RRULE"
                  },
                  "place": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "description": "External link for the event (tickets, host's own page, livestream). Absolute http(s) URL only."
                  },
                  "description": {
                    "type": "string"
                  },
                  "cost": {
                    "type": "string"
                  },
                  "spots": {
                    "type": [
                      "integer",
                      "string",
                      "null"
                    ],
                    "description": "Capacity: a count if limited, null/omitted for unlimited, or the string \"unknown\" for a real limit only the actual host could state. A submitted-not-hosted event (host: false) with no count defaults to unknown."
                  },
                  "categoryId": {
                    "type": "string"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "members",
                      "link"
                    ]
                  },
                  "flyerUrl": {
                    "type": "string",
                    "description": "Confirmed MythOS upload URL owned by the caller"
                  },
                  "flyerRatio": {
                    "type": "string",
                    "enum": [
                      "portrait",
                      "square",
                      "landscape"
                    ],
                    "description": "Derived server-side from the image itself whenever the flyer is new or changed; pass a value only to correct a stored ratio on an unchanged flyer."
                  },
                  "rsvpOptions": {
                    "type": "object",
                    "description": "RSVP options: maybe, cant, approveGuests, plusOnes"
                  },
                  "submitterNote": {
                    "type": "string"
                  },
                  "requestReview": {
                    "type": "boolean",
                    "description": "Queue the event for steward review regardless of your role. Additive only — it can put an event in front of a steward, never take one out of review. Use it to watch a new automation land in the stewardship panel before letting it run unmoderated."
                  },
                  "host": {
                    "type": "boolean",
                    "description": "Is this your event? true (the default) lists you as the host. false lists you as the submitter and leaves the host slot open for the real host to claim through the stewards. Display only — you keep edit and cancel authority either way."
                  },
                  "pricing": {
                    "type": "string",
                    "enum": [
                      "free",
                      "paid",
                      "unknown"
                    ],
                    "description": "The answer to 'does this cost money?'. \"unknown\" withholds the price entirely — the page shows no price. Omitted, it derives from the cost text; with no cost text a hosted event defaults free and a submitted-not-hosted one defaults unknown."
                  },
                  "hideSubmitter": {
                    "type": "boolean",
                    "description": "With host: false, keep the \"Submitted by\" attribution off the listing. Display only — stewards and the review queue still see the submitter. Omitted keeps the stored answer on edit."
                  },
                  "hostUsername": {
                    "type": "string",
                    "description": "With host: false, attribute a named MythOS creator as the host by username, shown instead of the open claim slot. Display only. On edit, omitted keeps the stored attribution; empty string clears it."
                  },
                  "hostCommunity": {
                    "type": "boolean",
                    "description": "With host: false, show this community as host. Display only; omitted keeps the stored answer on edit."
                  },
                  "hostName": {
                    "type": "string",
                    "description": "With host: false, show an external host name. Omitted keeps the stored name; empty string clears it."
                  },
                  "hostUrl": {
                    "type": "string",
                    "description": "Optional absolute http(s) link for hostName. Omitted keeps the stored link; empty string clears it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created event, with `queued`"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded, or the community's per-member weekly cap is reached"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/queue": {
      "get": {
        "summary": "The steward review queue",
        "description": "Pending events, oldest first. Requires a moderation role.",
        "operationId": "listPendingCommunityEvents",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Pending events"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}": {
      "delete": {
        "tags": [
          "Communities"
        ],
        "summary": "Delete a community event and all dates",
        "description": "REQUIRES expectedUpdatedAt from a fresh event read. Host or community moderation role only. Removes the event from app reads while retaining a cancelled ICS record for subscribers. Series-only; use cancel for one date.",
        "operationId": "deleteCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid token or occurrence-scoped request"
          },
          "403": {
            "description": "Caller is neither a host nor a community moderation role"
          },
          "404": {
            "description": "Event missing, deleted, or inaccessible"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN; includes currentUpdatedAt"
          }
        }
      },
      "get": {
        "summary": "Read one event",
        "description": "Dates, the guest list where you may see it, your own RSVP, and the event's `updatedAt` for the hard-locked writes.",
        "operationId": "readCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "occurrenceStart",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Which date to select, epoch ms. Defaults to the next one still to come."
          }
        ],
        "responses": {
          "200": {
            "description": "The event"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "patch": {
        "summary": "Edit an event",
        "description": "Series-scoped event edit. Requires expectedUpdatedAt from readCommunityEvent. Takes the same event fields as createCommunityEvent. Changes to dtstart, dtend, timezone, or rrule are refused once RSVPs exist because RSVP rows are keyed to occurrenceStart instants.",
        "operationId": "updateCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "title",
                  "dtstart",
                  "dtend",
                  "timezone",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "actAsLibrary": {
                    "type": "string",
                    "description": "Write as this library's owner through an approved Manage Social grant. A refusal is 403 ACT_AS_DENIED and never falls back to the caller. Omit to write as yourself."
                  },
                  "title": {
                    "type": "string"
                  },
                  "dtstart": {
                    "type": "object",
                    "description": "Wall-clock start { year, month, day, hour, minute }"
                  },
                  "dtend": {
                    "type": "object"
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA zone"
                  },
                  "rrule": {
                    "type": "string",
                    "description": "RFC 5545 RRULE"
                  },
                  "place": {
                    "type": "string"
                  },
                  "address": {
                    "type": "string"
                  },
                  "url": {
                    "type": "string",
                    "description": "External link for the event (tickets, host's own page, livestream). Absolute http(s) URL only."
                  },
                  "description": {
                    "type": "string"
                  },
                  "cost": {
                    "type": "string"
                  },
                  "spots": {
                    "type": [
                      "integer",
                      "string",
                      "null"
                    ],
                    "description": "Capacity: a count if limited, null/omitted for unlimited, or the string \"unknown\" for a real limit only the actual host could state. A submitted-not-hosted event (host: false) with no count defaults to unknown."
                  },
                  "categoryId": {
                    "type": "string"
                  },
                  "visibility": {
                    "type": "string",
                    "enum": [
                      "public",
                      "members",
                      "link"
                    ]
                  },
                  "flyerUrl": {
                    "type": "string",
                    "description": "Confirmed MythOS upload URL owned by the caller. Empty string clears the flyer."
                  },
                  "flyerRatio": {
                    "type": "string",
                    "enum": [
                      "portrait",
                      "square",
                      "landscape"
                    ],
                    "description": "Derived server-side from the image itself whenever the flyer is new or changed; pass a value only to correct a stored ratio on an unchanged flyer."
                  },
                  "rsvpOptions": {
                    "type": "object",
                    "description": "RSVP options: maybe, cant, approveGuests, plusOnes"
                  },
                  "host": {
                    "type": "boolean",
                    "description": "Is the submitter the listed host? Omitted keeps the stored answer; false shows the event as submitted-not-hosted with an open host slot."
                  },
                  "expectedUpdatedAt": {
                    "type": "integer",
                    "description": "Current event updatedAt token. Mismatch returns 409 STALE_LOCK_TOKEN with currentUpdatedAt."
                  },
                  "pricing": {
                    "type": "string",
                    "enum": [
                      "free",
                      "paid",
                      "unknown"
                    ],
                    "description": "The answer to 'does this cost money?'. \"unknown\" withholds the price entirely — the page shows no price. Omitted, it derives from the cost text; with no cost text a hosted event defaults free and a submitted-not-hosted one defaults unknown."
                  },
                  "hideSubmitter": {
                    "type": "boolean",
                    "description": "With host: false, keep the \"Submitted by\" attribution off the listing. Display only — stewards and the review queue still see the submitter. Omitted keeps the stored answer on edit."
                  },
                  "hostUsername": {
                    "type": "string",
                    "description": "With host: false, attribute a named MythOS creator as the host by username, shown instead of the open claim slot. Display only. On edit, omitted keeps the stored attribution; empty string clears it."
                  },
                  "hostCommunity": {
                    "type": "boolean",
                    "description": "With host: false, show this community as host. Display only; omitted keeps the stored answer on edit."
                  },
                  "hostName": {
                    "type": "string",
                    "description": "With host: false, show an external host name. Omitted keeps the stored name; empty string clears it."
                  },
                  "hostUrl": {
                    "type": "string",
                    "description": "Optional absolute http(s) link for hostName. Omitted keeps the stored link; empty string clears it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated event, with `queued` and `changes`"
          },
          "400": {
            "description": "Invalid payload or missing expectedUpdatedAt"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "Stale token, cancelled event, or schedule edit refused because RSVPs exist"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}/rsvp": {
      "post": {
        "summary": "RSVP to one occurrence",
        "description": "Takes NO expectedUpdatedAt: an upsert keyed on (event, occurrence, uid), idempotent and re-answerable. Membership required.",
        "operationId": "rsvpCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "occurrenceStart",
                  "status"
                ],
                "properties": {
                  "occurrenceStart": {
                    "type": "integer",
                    "description": "Epoch ms"
                  },
                  "status": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "enum": [
                      "going",
                      "maybe",
                      "cant",
                      null
                    ],
                    "description": "null withdraws"
                  },
                  "plusOnes": {
                    "type": "integer"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your RSVP and the new counts"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}/guest-link": {
      "get": {
        "summary": "Read event guest link",
        "description": "Host or moderation role, with the community calendar's guestLinks setting enabled. Reads the one live per-event guest link, including token and URL because this is the steward management surface. Public token routes are deliberately not mirrored.",
        "operationId": "readCommunityEventGuestLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The live guest link, or null when disabled"
          },
          "403": {
            "description": "Unauthorized, disabled setting, or role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "post": {
        "summary": "Create or update event guest link",
        "description": "Host or moderation role. Creates, updates, or regenerates the one live guest link for this event. No expectedUpdatedAt: one not-revoked link per event is enforced by a partial unique index, and regenerate is explicitly revoke-then-insert. The link grants event attendance only, never membership.",
        "operationId": "createCommunityEventGuestLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expires": {
                    "type": "string",
                    "enum": [
                      "event-end",
                      "24h",
                      "7d",
                      "never"
                    ]
                  },
                  "maxUses": {
                    "type": "number",
                    "nullable": true,
                    "description": "10, 25, 50, or null for no limit"
                  },
                  "regenerate": {
                    "type": "boolean",
                    "description": "When true, revoke the current token before creating the new one"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing link updated"
          },
          "201": {
            "description": "Link created or regenerated"
          },
          "400": {
            "description": "Invalid expiry or use limit"
          },
          "403": {
            "description": "Unauthorized, disabled setting, or role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "The event is not open for guest links"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "delete": {
        "summary": "Revoke event guest link",
        "description": "Host or moderation role. Revokes the live guest link. Existing RSVP rows remain; holders of the old URL see that it is no longer valid. No expectedUpdatedAt because revoke is an idempotent guarded write over the one live link.",
        "operationId": "revokeCommunityEventGuestLink",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked or already disabled"
          },
          "403": {
            "description": "Unauthorized, disabled setting, or role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}/review": {
      "patch": {
        "summary": "Decide a submitted event",
        "description": "REQUIRES expectedUpdatedAt. Approving a series that changed since you read it publishes something nobody reviewed, so the token rides the update filter. Requires a moderation role.",
        "operationId": "reviewCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "decision",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "decision": {
                    "type": "string",
                    "enum": [
                      "approve",
                      "first",
                      "decline"
                    ]
                  },
                  "expectedUpdatedAt": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decided event"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN — expectedUpdatedAt was not supplied"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or the event was already decided"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}/cancel": {
      "post": {
        "summary": "Cancel one date or a series",
        "description": "REQUIRES expectedUpdatedAt. A cancellation is never a delete — the event survives so subscribed calendars learn the date is off. Host or moderation role only.",
        "operationId": "cancelCommunityEvent",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "scope",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "actAsLibrary": {
                    "type": "string",
                    "description": "Write as this library's owner through an approved Manage Social grant. A refusal is 403 ACT_AS_DENIED and never falls back to the caller. Omit to write as yourself."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "occurrence",
                      "series"
                    ]
                  },
                  "occurrenceStart": {
                    "type": "integer",
                    "description": "Required when scope is occurrence"
                  },
                  "reason": {
                    "type": "string",
                    "maxLength": 300
                  },
                  "expectedUpdatedAt": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The cancelled date or event"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN — expectedUpdatedAt was not supplied"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or the event was already decided"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/events/{eventId}/guests": {
      "patch": {
        "summary": "Approve or decline one guest",
        "description": "For an event whose host asked to approve guests. No expectedUpdatedAt — one field on a row keyed by (event, occurrence, guest), idempotent under retry.",
        "operationId": "decideCommunityEventGuest",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "description": "The event id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "uid",
                  "occurrenceStart",
                  "approved"
                ],
                "properties": {
                  "uid": {
                    "type": "string",
                    "description": "The guest's uid"
                  },
                  "occurrenceStart": {
                    "type": "integer"
                  },
                  "approved": {
                    "type": "boolean"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated RSVP"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community or event not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/vote": {
      "post": {
        "summary": "Toggle a vote on a post",
        "description": "No expectedUpdatedAt — a toggle's end state is the point.",
        "operationId": "voteCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "value"
                ],
                "properties": {
                  "value": {
                    "type": "integer",
                    "enum": [
                      1,
                      -1
                    ],
                    "description": "Sending the same value again clears the vote"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new score and your vote"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "delete": {
        "summary": "Withdraw a post vote",
        "description": "Withdraw your vote. No `expectedUpdatedAt` -- a toggle's end state is the point, and a token would re-introduce the race the toggle avoids. Withdrawing a vote you never cast is a no-op rather than an error.",
        "operationId": "clearCommunityPostVote",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new score"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/comments": {
      "get": {
        "summary": "Read a post's comment thread",
        "description": "Each comment carries its updatedAt, depth and your userVote. Mention chips pointing at memos you cannot see are redacted.",
        "operationId": "listCommunityComments",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "hot",
                "newest",
                "oldest"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The thread and your read watermark"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "post": {
        "summary": "Add a comment or reply",
        "description": "INLINE markdown only — block markdown renders literally. No expectedUpdatedAt.",
        "operationId": "addCommunityComment",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "parentCommentId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created comment"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/comments/{commentId}": {
      "patch": {
        "summary": "Edit your own comment",
        "description": "REQUIRES expectedUpdatedAt. Author-only.",
        "operationId": "editCommunityComment",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "description": "The comment id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "content",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "content": {
                    "type": "string",
                    "maxLength": 10000
                  },
                  "expectedUpdatedAt": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The edited comment"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN — expectedUpdatedAt was not supplied"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "delete": {
        "summary": "Delete a comment",
        "description": "REQUIRES expectedUpdatedAt as a QUERY PARAM, per the mutation contract's DELETE transport. Author or steward.",
        "operationId": "deleteCommunityComment",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "description": "The comment id",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN — expectedUpdatedAt was not supplied"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/comments/{commentId}/vote": {
      "post": {
        "summary": "Toggle a vote on a comment",
        "description": "Upvote or downvote a comment. Same toggle semantics as the post vote and no `expectedUpdatedAt` for the same reason. A comment inside a community you may not see answers 404 exactly as one that does not exist.",
        "operationId": "voteCommunityComment",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "description": "The comment id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "value"
                ],
                "properties": {
                  "value": {
                    "type": "integer",
                    "enum": [
                      1,
                      -1
                    ],
                    "description": "Sending the same value again clears the vote"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new score and your vote"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "delete": {
        "summary": "Withdraw a comment vote",
        "description": "Withdraw your vote on a comment. No `expectedUpdatedAt`; withdrawing a vote you never cast is a no-op.",
        "operationId": "clearCommunityCommentVote",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "commentId",
            "in": "path",
            "required": true,
            "description": "The comment id",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The new score"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/poll": {
      "post": {
        "summary": "Answer a poll",
        "description": "No expectedUpdatedAt — an upsert on (post, uid), so answering again MOVES your response. Option ids come from the post's own pollOptions.",
        "operationId": "answerCommunityPoll",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "optionId"
                ],
                "properties": {
                  "optionId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The running counts and your choice"
          },
          "400": {
            "description": "NOT_A_POLL or POLL_OPTION_UNKNOWN"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "POLL_CLOSED"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/bookmark": {
      "post": {
        "summary": "Toggle a bookmark",
        "description": "No expectedUpdatedAt. DELETE-first internally, so calling again removes it; `bookmarked` says which way it went.",
        "operationId": "bookmarkCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Bookmark state and count"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/read": {
      "post": {
        "summary": "Advance your thread-read watermark",
        "description": "Explicit rather than a side effect of reading comments. No expectedUpdatedAt.",
        "operationId": "markCommunityThreadRead",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Marked"
          },
          "403": {
            "description": "Unauthorized, or your role does not permit this action"
          },
          "404": {
            "description": "Community, post or comment not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}": {
      "get": {
        "summary": "Read one post in full",
        "description": "Through the shared enricher. Returns `updatedAt`, the lock token.",
        "operationId": "readCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The post"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "patch": {
        "summary": "Approve or reject a queued post",
        "description": "Moderation role. Optionally refiles. No expectedUpdatedAt.",
        "operationId": "reviewCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "approved",
                      "rejected"
                    ]
                  },
                  "collectionId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The decided post"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "delete": {
        "summary": "Delete a post",
        "description": "REQUIRES expectedUpdatedAt as a query param. Not reversible.",
        "operationId": "deleteCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/content": {
      "patch": {
        "summary": "Edit a community post",
        "description": "REQUIRES expectedUpdatedAt. Send any of title+content, postTags, collectionId; only fields sent change. Authors edit title and content (text posts only) for 15 minutes after posting (EDIT_WINDOW_CLOSED after), and postTags and collectionId any time; a collection that would review or refuse the post returns COLLECTION_REVIEW_REQUIRED or COLLECTION_CLOSED. Community stewards edit every field of a post the community made as itself, indefinitely. Stewards cannot retag or rewrite a member's post. The postSlug never changes.",
        "operationId": "editCommunityPost",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "title": {
                    "type": "string"
                  },
                  "content": {
                    "type": "string"
                  },
                  "postTags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Existing community topics; an empty array clears unless a topic is required"
                  },
                  "collectionId": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Collection id or slug; null unfiles unless a collection is required"
                  },
                  "confirmHiddenMentions": {
                    "type": "boolean"
                  },
                  "expectedUpdatedAt": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The edited post"
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/posts/{postId}/context": {
      "patch": {
        "summary": "Edit your own submission note",
        "description": "No expectedUpdatedAt — the submitter writes this field, or on a post the community made as itself, its creator and stewards; last write wins. Empty string removes the note.",
        "operationId": "editCommunitySubmissionNote",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postId",
            "in": "path",
            "required": true,
            "description": "The post id or postSlug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "context"
                ],
                "properties": {
                  "context": {
                    "type": "string",
                    "maxLength": 2000
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The stored note, redacted as readers will see it"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/drafts": {
      "get": {
        "summary": "Your own drafts for one community",
        "description": "Your OWN unpublished drafts for one community -- never anyone else's. A draft holds a partial post of any kind and is bounded by the same size limits publishing enforces, so it can never hold something that will be rejected on publish.",
        "operationId": "listCommunityDrafts",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Your drafts"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      },
      "post": {
        "summary": "Save a draft",
        "description": "No expectedUpdatedAt — autosave-shaped, and oversized fields truncate rather than reject.",
        "operationId": "saveCommunityDraft",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "kind",
                  "payload"
                ],
                "properties": {
                  "kind": {
                    "type": "string"
                  },
                  "payload": {
                    "type": "object"
                  },
                  "draftId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The draft id"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "409": {
            "description": "DRAFT_LIMIT_REACHED"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/communities/{slug}/drafts/{draftId}": {
      "delete": {
        "summary": "Discard a draft",
        "description": "Discard one of your own drafts. Ownership rides the delete FILTER rather than a preceding read. No `expectedUpdatedAt` -- a draft is autosave-shaped and the end state is the point.",
        "operationId": "deleteCommunityDraft",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The community slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "draftId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Discarded"
          },
          "403": {
            "description": "Unauthorized, tier or role insufficient, or not a member"
          },
          "404": {
            "description": "Not found — also the answer when it exists and you may not see it"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/fields": {
      "patch": {
        "operationId": "patchLoopFields",
        "summary": "Rewrite Trigger or Policy field lines of an owned loop",
        "description": "Staff-only. The lossless field patcher: rewrites the named `- **Field** value` lines in one section and preserves every other byte of the body. A null value removes the field's line; an absent field is inserted after the section's last field. `Status` answers to a legacy `State` line and writes the current name. Owner only; grants and act-as do not reach it. Runs the same content pipeline as update_memo_section, with CAS on header.updatedAt. Pause: {section: \"Trigger\", fields: {Status: \"paused\"}}. A schedule write (Schedule, Times, Time zone, Starts or Ends) that leaves no Time zone gets the loop owner's account zone, named in the answer's timeZoneFilled ({timeZone, note}) (with a note when it decides how Starts and Ends are read); with no account zone it answers 422 SCHEDULE_TIME_ZONE_REQUIRED. Writes that don't touch the schedule are never refused or rewritten.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt",
                  "section",
                  "fields"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "updatedAt from read_memo or read_loop."
                  },
                  "section": {
                    "type": "string",
                    "enum": [
                      "Trigger",
                      "Policy"
                    ]
                  },
                  "fields": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "string",
                      "nullable": true
                    },
                    "description": "Field name to a one-line value, or null to remove. 1 to 12 fields."
                  },
                  "acknowledgeUnlinked": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Written; returns the memo's new updatedAt like update_memo."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or a malformed section/fields body."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "SECTION_NOT_FOUND, SECTION_AMBIGUOUS, FIELD_AMBIGUOUS or INVALID_FIELD."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/inputs": {
      "get": {
        "operationId": "readLoopInputs",
        "summary": "Read a loop's form inputs",
        "description": "Staff-only. The typed fields a run of this loop is started with, and the sidecar's own lock token (`updatedAt`, null before the loop has any). Owner only.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "responses": {
          "200": {
            "description": "{ inputs: { fields, updatedAt }, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateLoopInputs",
        "summary": "Replace a loop's form inputs",
        "description": "Staff-only. Replaces the whole field list under CAS on the sidecar's own updatedAt (null before the first write). Each field: {name, label, type: text|choice|date|memo, required?, options? (choice only), loadMentions? (text only)}; names are unique per loop and are referenced as {{name}}. Writes the sidecar only: the memo body, contentVersion and header.updatedAt are untouched. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt",
                  "fields"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true,
                    "description": "inputs.updatedAt from read_loop or GET; null before the loop has any."
                  },
                  "fields": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "The whole field list, in order; [] removes every input. At most 20."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ inputs: { fields, updatedAt }, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or INVALID_INPUTS naming the bad field."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED: a device run key without Loop management."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/shelf": {
      "get": {
        "operationId": "readLoopShelf",
        "summary": "Read where a loop sits on the index",
        "description": "Staff-only. The loop's folder and pin, and the shelf sidecar's own lock token (`updatedAt`, null on a loop never filed). Owner only.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "responses": {
          "200": {
            "description": "{ shelf: { folder, pinned }, updatedAt }."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "setLoopShelf",
        "summary": "File or pin a loop on the index",
        "description": "Staff-only. Sets only the named fields (folder, pinned) under CAS on loopShelf.updatedAt (null before the first filing). copiedFrom is refused. Writes the sidecar only: the memo body, contentVersion and header.updatedAt are untouched. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true,
                    "description": "shelf.updatedAt from read_loop or GET; null before the loop was ever filed."
                  },
                  "folder": {
                    "type": "string",
                    "nullable": true,
                    "description": "Folder name (up to 48 characters), or null to unfile."
                  },
                  "pinned": {
                    "type": "boolean",
                    "description": "Pin or unpin."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ shelf: { folder, pinned }, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or a body naming neither folder nor pinned."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED: a device run key without Loop management."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/connectors/{provider}": {
      "get": {
        "operationId": "readLoopConnector",
        "summary": "Read a connector's status",
        "description": "Staff-only. The caller's connection to an app loops write to: connected, organizationId, connectedAt, updatedAt (the lock token revoke and the link need) and lastError, scrubbed. Never the token or its ciphertext. Owner only.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "missive"
              ]
            },
            "description": "The connector: missive."
          }
        ],
        "responses": {
          "200": {
            "description": "{ connector }: the status view."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or UNKNOWN_CONNECTOR."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "delete": {
        "operationId": "revokeLoopConnector",
        "summary": "Revoke a connector",
        "description": "Staff-only. Deletes the caller's credential for the connector under CAS on its updatedAt, from ?expectedUpdatedAt= or the JSON body. A loop with Destination connector fails dispatch_error until it is connected again. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "missive"
              ]
            },
            "description": "The connector: missive."
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "The connector's updatedAt; or send it in the JSON body."
          }
        ],
        "responses": {
          "200": {
            "description": "{ connector }: the status view, now not connected."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "Not staff, UNKNOWN_CONNECTOR, or NOT_CONNECTED."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/connectors/{provider}/link": {
      "post": {
        "operationId": "mintLoopConnectorLink",
        "summary": "Mint an owner link to connect or rotate a connector",
        "description": "Staff-only. An agent never handles a connector credential: this answers a single-use link, valid 15 minutes, for the signed-in owner, who opens it and pastes the token; MythOS verifies it with the app before storing it. expectedUpdatedAt null connects; the connector's updatedAt rotates; checked under CAS. Minting replaces any unused link. A credential-named body field answers 400 SECRET_FIELD_REFUSED. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "provider",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "enum": [
                "missive"
              ]
            },
            "description": "The connector: missive."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "null to connect; the connector's updatedAt to rotate."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ purpose, url, expiresAt }: the owner link."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or SECRET_FIELD_REFUSED."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "Not staff, or UNKNOWN_CONNECTOR."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/blocks": {
      "get": {
        "operationId": "listFormBlocks",
        "summary": "List your memos that hold form blocks, with their blocks",
        "description": "Staff-only. The Components library: every memo the caller wrote that holds a named form block, most recently edited first, each block in document order with how many of the caller's loops call it. Only the caller's own memos: a collaborator, grantee or act-as caller gets none of another author's blocks.",
        "responses": {
          "200": {
            "description": "{ memos }.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "memos": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "memoId": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "path": {
                            "type": "string"
                          },
                          "contentVersion": {
                            "type": "number"
                          },
                          "updatedAt": {
                            "type": "number"
                          },
                          "blocks": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "variable": {
                                  "type": "string"
                                },
                                "body": {
                                  "type": "string",
                                  "description": "The block's markdown."
                                },
                                "preview": {
                                  "type": "string",
                                  "description": "One line of its text."
                                },
                                "usedBy": {
                                  "type": "number",
                                  "description": "How many of your loops call it."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid API key."
          }
        }
      },
      "post": {
        "operationId": "createFormBlock",
        "summary": "Add an empty form block to the end of a memo you wrote",
        "description": "Staff-only. New Action into an existing memo: an empty block named the lowest free block-N goes at the end of the body, written through the same pipeline as PATCH /memos. A block in a new memo is a memo create (POST /memos with the block as content, visibility private). The memo's author only; a device's run key writes only with Loop management on.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memoId",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "memoId": {
                    "type": "string",
                    "description": "Short id in your library, dash form or slash path."
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The memo's header.updatedAt, as last read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ memoId, path, title, variable, updatedAt, contentVersion }.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "memoId": {
                      "type": "string"
                    },
                    "path": {
                      "type": "string"
                    },
                    "title": {
                      "type": "string"
                    },
                    "variable": {
                      "type": "string"
                    },
                    "updatedAt": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "contentVersion": {
                      "type": [
                        "number",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or INVALID_ARGUMENT: memoId missing or malformed."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "MEMO_NOT_FOUND: no such memo, or not the caller's."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          }
        }
      }
    },
    "/api/internal/loops/blocks/used-by": {
      "get": {
        "operationId": "readFormBlockUsedBy",
        "summary": "Read the loops that call a memo's form blocks",
        "description": "Staff-only. The loops whose record steps call each form block in a memo the caller wrote, or one block by variable: { memoId, usedBy: [{ variable, loops: [{ memoId, title, path, status, lastRunVersion }] }] }. A trashed loop no longer counts. The block's author only: anyone else, a grant included, reads it as a memo that does not exist.",
        "parameters": [
          {
            "name": "memoId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The memo holding the blocks: short id in your library, dash form or slash path."
          },
          {
            "name": "variable",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "pattern": "^[a-z][a-z0-9-]*$"
            },
            "description": "One block's variable; omit for every block a loop calls."
          }
        ],
        "responses": {
          "200": {
            "description": "{ memoId, usedBy }.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "memoId": {
                      "type": "string"
                    },
                    "usedBy": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "variable": {
                            "type": "string"
                          },
                          "loops": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "memoId": {
                                  "type": "string"
                                },
                                "title": {
                                  "type": "string"
                                },
                                "path": {
                                  "type": "string"
                                },
                                "status": {
                                  "type": "string",
                                  "description": "The loop's trigger state: live, paused or draft."
                                },
                                "lastRunVersion": {
                                  "type": [
                                    "number",
                                    "null"
                                  ],
                                  "description": "The memo contentVersion the loop's latest run read the block at; null if none has."
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_ARGUMENT: memoId missing or variable malformed."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "MEMO_NOT_FOUND: no such memo, or not the caller's."
          }
        }
      }
    },
    "/api/internal/loops/blocks/changes": {
      "post": {
        "operationId": "changeFormBlocks",
        "summary": "Rename or remove form blocks in a memo",
        "description": "Staff-only. Renames each named block's open fence or removes the block, writing the body through the same pipeline as PATCH /api/internal/memos under CAS on the memo's updatedAt. A renamed block that loops call has every calling step repointed in the same request (blockChanges: { repointed, failed }); a removed block's loops fail that step with instruction_missing until repointed. The memo write routes refuse a used block's rename or removal with 422 FORM_BLOCK_IN_USE and point here. The memo's author only; a device run key needs Loop management.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memoId",
                  "expectedUpdatedAt",
                  "changes"
                ],
                "properties": {
                  "memoId": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "updatedAt from GET /api/internal/memos."
                  },
                  "changes": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "type": "object",
                      "required": [
                        "variable"
                      ],
                      "properties": {
                        "variable": {
                          "type": "string"
                        },
                        "renameTo": {
                          "type": "string"
                        },
                        "remove": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        }
                      }
                    },
                    "description": "One per block: { variable, renameTo } or { variable, remove: true }."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ success, contentVersion, updatedAt, blockChanges? }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or INVALID_ARGUMENT (malformed changes, or a rename onto a name another block keeps)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "MEMO_NOT_FOUND: no such memo, or not the caller's."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "FORM_BLOCK_NOT_FOUND: a variable the memo doesn't hold."
          }
        }
      }
    },
    "/api/internal/loops/values/{name}": {
      "get": {
        "operationId": "readLoopValue",
        "summary": "Read a secure value's status",
        "description": "Staff-only. The key owner's value by its key: { name, secureValue: { name, displayName, provider, providerLabel, organizationId, createdAt, updatedAt, rotatedAt, lastUsedAt, lastError } | null }. Never the value, its ciphertext or its hint. Owner only.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z][A-Z0-9_]{1,63}$"
            },
            "description": "The key, as loops reference it, e.g. MISSIVE_API_KEY."
          }
        ],
        "responses": {
          "200": {
            "description": "{ name, secureValue: status | null }."
          },
          "400": {
            "description": "INVALID_VALUE_NAME."
          },
          "401": {
            "description": "Invalid API key."
          }
        }
      },
      "patch": {
        "operationId": "renameLoopValue",
        "summary": "Rename a secure value's display name",
        "description": "Staff-only. Renames the display name (1 to 80 characters, unique per owner ignoring case; a value saved before display names reads as its key) under CAS on updatedAt. The key, the sealed value, its hint and its grants are unchanged. A body field named value or like a credential answers 400 SECRET_FIELD_REFUSED. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z][A-Z0-9_]{1,63}$"
            },
            "description": "The key, as loops reference it, e.g. MISSIVE_API_KEY."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "displayName",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "displayName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "updatedAt from GET."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ secureValue: status without hint, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, INVALID_DISPLAY_NAME, INVALID_VALUE_NAME or SECRET_FIELD_REFUSED."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "VALUE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "DISPLAY_NAME_TAKEN."
          }
        }
      }
    },
    "/api/internal/loops/values/{name}/link": {
      "post": {
        "operationId": "mintLoopValueLink",
        "summary": "Mint an owner link to create or rotate a secure value",
        "description": "Staff-only. An agent never handles a secure value: this answers a single-use link, valid 15 minutes, for the signed-in owner, who opens it and pastes the value. expectedUpdatedAt null creates; the value's updatedAt rotates; checked under CAS. Minting replaces any unused link for the same name. A body field named value or like a credential answers 400 SECRET_FIELD_REFUSED. Owner only; a device run key needs Loop management. Values are read, created, rotated and revoked only by a signed-in person under /api/loops/values.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[A-Z][A-Z0-9_]{1,63}$"
            },
            "description": "The value's name, e.g. SMARTLEAD_KEY. Names the device runner relies on, and names starting MYTHOS_, CLAUDE_, LC_ or DYLD_, are refused."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": [
                      "number",
                      "null"
                    ],
                    "description": "null to create; the value's updatedAt to rotate."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ name, purpose, url, expiresAt }: the owner link."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, SECRET_FIELD_REFUSED or INVALID_VALUE_NAME."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "Not staff."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/sources/{sourceId}/signing-link": {
      "post": {
        "operationId": "mintLoopSourceSigningLink",
        "summary": "Mint an owner link to set a pushed source's signing secret",
        "description": "Staff-only until Loops launches. An agent never handles a signing secret: this answers a single-use link, valid 15 minutes, for the signed-in owner, who opens it and pastes the secret; it is sealed to that one source. expectedUpdatedAt is the source's updatedAt from list_loop_sources, checked under CAS when minted and again when the link is redeemed. Minting replaces any unused link for the source. A credential-named body field or signature answers 400 SECRET_FIELD_REFUSED. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "sourceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sourceId from list_loop_sources."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The source's updatedAt from list_loop_sources."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ purpose, url, expiresAt, signatureSet }: the owner link. purpose is set, or replace when a secret is already set."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or SECRET_FIELD_REFUSED."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management)."
          },
          "404": {
            "description": "Not staff, or SOURCE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or SOURCE_NOT_PUSHED."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/sources/{sourceId}": {
      "patch": {
        "operationId": "updateLoopSource",
        "summary": "Edit a connected trigger source",
        "description": "Staff-only. Sets the named fields (label, status, config) of one of the caller's trigger sources under CAS on the source's updatedAt, from GET /api/loops/sources or list_loop_sources. Only an edit, a rotation or a pause moves updatedAt; a poll or an arrival never does. config is the kind's whole config, validated by its connector; a memo-tag library is checked against the caller's editing grant, and a data-table-row table against the caller's access to it, on a config change or a resume. The handle never changes. No secret work: rotateSecret, signature and any credential-named config key answer 400 SECRET_FIELD_REFUSED, and the answer is the public view, which carries no secret. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "sourceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "sourceId from GET /api/loops/sources."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "The source's updatedAt."
                  },
                  "label": {
                    "type": "string",
                    "description": "Display name; empty resets it to the handle."
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "live",
                      "paused"
                    ]
                  },
                  "config": {
                    "type": "object",
                    "description": "The kind's whole config, replacing the stored one."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ source, updatedAt }: the source's public view and its next lock token."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, SECRET_FIELD_REFUSED, INVALID_STATUS, INVALID_SOURCE_CONFIG, NO_CHANGES or INVALID_BODY."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED (a device run key without Loop management), LIBRARY_ACCESS_DENIED for a memo-tag library or a data-table-row table in another library, or DATA_TABLES_TIER_REQUIRED for a data-table-row table."
          },
          "404": {
            "description": "Not staff, SOURCE_NOT_FOUND (no such source owned by the caller), LIBRARY_NOT_FOUND, or DATA_TABLE_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/draft": {
      "get": {
        "operationId": "readLoopDraft",
        "summary": "Read a loop's canvas draft",
        "description": "Staff-only. The design layer beside a loop: notes, routers, filters, links, brief and step positions (schema v2), never executable steps, with its lock token (`updatedAt`, null before the first save). With handoff=1, the exported development brief instead. Owner only.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          },
          {
            "name": "handoff",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "1 returns { handoff }, the development brief."
          }
        ],
        "responses": {
          "200": {
            "description": "{ draft: { version, updatedAt, graph }, updatedAt }, or { handoff }."
          },
          "400": {
            "description": "handoff=1 with neither a brief nor a node."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "422": {
            "description": "handoff=1 on a stored graph this surface cannot read."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "saveLoopDraft",
        "summary": "Replace a loop's canvas draft",
        "description": "Staff-only. Replaces the whole draft under CAS on loopDraft.updatedAt (null before the first save), validated by the canvas's own schema. Raises loopDraft.version and stamps a strictly rising updatedAt, so a browser save and an agent save each make the other's token stale. Never touches the memo body or steps. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt",
                  "draft"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "nullable": true,
                    "description": "updatedAt from GET; null before the first save."
                  },
                  "draft": {
                    "type": "object",
                    "description": "{ schemaVersion: 2, brief, nodes, edges, filters, stepPositions, conversation }."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ draft: { version, updatedAt, graph }, updatedAt }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN, or an invalid graph (nothing written)."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED: a device run key without Loop management."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/draft/propose": {
      "post": {
        "operationId": "proposeLoopDraft",
        "summary": "Propose canvas draft changes with the owner's model",
        "description": "Staff-only. Asks the owner's configured model for a proposed graph from a message, against the passed draft or else the saved one. Writes nothing. Spends the owner's model budget, six a minute per owner (shared with the canvas). Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "message"
                ],
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "What to change, up to 8000 characters."
                  },
                  "draft": {
                    "type": "object",
                    "description": "Optional graph to propose against instead of the saved draft."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ proposal: { message, draft }, costUsd }."
          },
          "400": {
            "description": "Invalid request, no available model, or a source memo over 32,000 characters."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED: a device run key without Loop management."
          },
          "404": {
            "description": "Not staff, or no such loop owned by the caller."
          },
          "422": {
            "description": "The saved draft cannot be read; pass a draft."
          },
          "429": {
            "description": "Six proposals a minute per owner, or rate limited."
          },
          "502": {
            "description": "The model failed or returned an invalid graph."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/record": {
      "get": {
        "operationId": "readLoopRecord",
        "summary": "Read a loop record",
        "description": "Staff-only. The record that defines a loop once it has one: { record, updatedAt, stepKeys }. ?versions=1 adds the history; ?version=n returns one snapshot. Owner only.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          },
          {
            "name": "versions",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          },
          {
            "name": "version",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The record, or one version snapshot."
          },
          "400": {
            "description": "INVALID_ARGUMENT: a bad version."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "LOOP_RECORD_NOT_FOUND (the body still defines the loop), VERSION_NOT_FOUND, or not staff."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "post": {
        "operationId": "createLoopRecord",
        "summary": "Give a loop a record",
        "description": "Staff-only. from: memo imports the body as it stands (it must compile clean); blank starts paused with one trigger step. Runs then read the record, never the body. A first write: no token. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "from"
                ],
                "properties": {
                  "from": {
                    "type": "string",
                    "enum": [
                      "memo",
                      "blank"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "{ record, updatedAt, compile }."
          },
          "400": {
            "description": "INVALID_ARGUMENT."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED."
          },
          "404": {
            "description": "LOOP_NOT_FOUND."
          },
          "409": {
            "description": "LOOP_RECORD_EXISTS."
          },
          "422": {
            "description": "LOOP_NOT_COMPILABLE with diagnostics, or LOOP_RECORD_EXTERNAL_INTERIM."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateLoopRecord",
        "summary": "Change a loop record",
        "description": "Staff-only. trigger and policy set the fields named; steps, contract and routes replace the whole list. Memos are named by id. A trigger.command meets the rename checks. CAS on the record's updatedAt. Owner only; a device run key needs Loop management. A trigger.schedule with no timeZone gets the loop owner's account zone, named in the answer's timeZoneFilled; with no account zone it answers 422 SCHEDULE_TIME_ZONE_REQUIRED.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "trigger": {
                    "type": "object"
                  },
                  "policy": {
                    "type": "object"
                  },
                  "steps": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "contract": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "routes": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ record, updatedAt, compile }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_ARGUMENT."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED."
          },
          "404": {
            "description": "LOOP_RECORD_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "422": {
            "description": "UNRESOLVED_MEMO, INVALID_COMMAND, COMMAND_TAKEN or LOOP_RECORD_EXTERNAL_INTERIM."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/record/steps": {
      "patch": {
        "operationId": "reorderLoopRecordSteps",
        "summary": "Reorder a loop record's steps",
        "description": "Staff-only. order names every current step key once (GET's stepKeys). CAS on the record's updatedAt. Owner only; a device run key needs Loop management.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt",
                  "order"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "order": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "{ record, updatedAt, compile }."
          },
          "400": {
            "description": "MISSING_LOCK_TOKEN or INVALID_ARGUMENT."
          },
          "401": {
            "description": "Invalid API key."
          },
          "403": {
            "description": "RUN_LOOP_WRITE_REFUSED."
          },
          "404": {
            "description": "LOOP_RECORD_NOT_FOUND."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/loops/{memoId}/import": {
      "post": {
        "operationId": "importLoopMemo",
        "summary": "Import an owned loop memo",
        "description": "Staff-only loop association. Requires real memo ownership; grants do not confer ownership. Changes memoType and header.updatedAt with atomic CAS; preserves content and contentVersion. Read the memo first. Already associated/unassociated states are accepted with a fresh token. Another surface type is refused.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          }
        ],
        "responses": {
          "200": {
            "description": "Association updated; returns success, memoId, memoType and updatedAt."
          },
          "400": {
            "description": "Missing or invalid expectedUpdatedAt."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or memo missing, trashed or not owned."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or MEMO_TYPE_CONFLICT."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "expectedUpdatedAt": {
                    "type": "number",
                    "description": "updatedAt from read_memo; required and finite."
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "unimportLoopMemo",
        "summary": "Unimport an owned loop memo",
        "description": "Staff-only loop association. Requires real memo ownership; grants do not confer ownership. Changes memoType and header.updatedAt with atomic CAS; preserves content and contentVersion. Read the memo first. Already associated/unassociated states are accepted with a fresh token. Another surface type is refused.",
        "parameters": [
          {
            "name": "memoId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Memo short id, compound id, or URL-encoded canonical /me/library/id."
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number",
              "description": "updatedAt from read_memo; required and finite."
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Association updated; returns success, memoId, memoType and updatedAt."
          },
          "400": {
            "description": "Missing or invalid expectedUpdatedAt."
          },
          "401": {
            "description": "Invalid API key."
          },
          "404": {
            "description": "Not staff, or memo missing, trashed or not owned."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN with currentUpdatedAt, or MEMO_TYPE_CONFLICT."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/activity": {
      "get": {
        "operationId": "getActivity",
        "summary": "GET /api/internal/activity",
        "description": "Read library activity trends, top and rising memos. Optional username targets a granted library; content-scoped grants are refused. Returns activity with metric, trends, top and rising.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/augmentation": {
      "get": {
        "operationId": "getAugmentation",
        "summary": "GET /api/internal/augmentation",
        "description": "Load augmentation and contextStats for the caller or username library, and open a context session: the response's contextSessionId is the x-mythos-context value writes require for 24 hours. Cross-library access requires a grant; content-scoped grants are refused.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/collaborators": {
      "get": {
        "operationId": "getCollaborators",
        "summary": "GET /api/internal/collaborators",
        "description": "Read, invite or remove memo collaborators. GET requires memoId and optionally username; POST requires memoId and collaboratorInput; DELETE requires memoId and collaboratorUsername. Invites also check the caller plan. Ownership or a permitted library role is required; returns collaborator details or a mutation result.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "memoId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postCollaborators",
        "summary": "POST /api/internal/collaborators",
        "description": "Read, invite or remove memo collaborators. GET requires memoId and optionally username; POST requires memoId and collaboratorInput; DELETE requires memoId and collaboratorUsername. Invites also check the caller plan. Ownership or a permitted library role is required; returns collaborator details or a mutation result.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "memoId": {},
                  "collaboratorInput": {}
                }
              },
              "description": "Read, invite or remove memo collaborators. GET requires memoId and optionally username; POST requires memoId and collaboratorInput; DELETE requires memoId and collaboratorUsername. Invites also check the caller plan. Ownership or a permitted library role is required; returns collaborator details or a mutation result."
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteCollaborators",
        "summary": "DELETE /api/internal/collaborators",
        "description": "Read, invite or remove memo collaborators. GET requires memoId and optionally username; POST requires memoId and collaboratorInput; DELETE requires memoId and collaboratorUsername. Invites also check the caller plan. Ownership or a permitted library role is required; returns collaborator details or a mutation result.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "memoId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "collaboratorUsername",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/cron/backfill-descriptions": {
      "get": {
        "operationId": "getCronBackfillDescriptions",
        "summary": "GET /api/internal/cron/backfill-descriptions",
        "description": "Backfill descriptions for opted-in libraries using bounded owner BYOK batches. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/cleanup-media-reservations": {
      "get": {
        "operationId": "getCronCleanupMediaReservations",
        "summary": "GET /api/internal/cron/cleanup-media-reservations",
        "description": "Expire abandoned media upload reservations and return the expired count. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/cleanup-pending-subscriptions": {
      "get": {
        "operationId": "getCronCleanupPendingSubscriptions",
        "summary": "GET /api/internal/cron/cleanup-pending-subscriptions",
        "description": "Delete expired pending subscriptions and purge memos past their trash retention window; returns deleted and purgedTrash. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/drain-email-queue": {
      "get": {
        "operationId": "getCronDrainEmailQueue",
        "summary": "GET /api/internal/cron/drain-email-queue",
        "description": "Drain a bounded batch from the outgoing email queue; returns send and failure counts. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/gift-ttl-sweeper": {
      "get": {
        "operationId": "getCronGiftTtlSweeper",
        "summary": "GET /api/internal/cron/gift-ttl-sweeper",
        "description": "Clear stale non-claiming gift locks. Oracle gifts no longer expire; stuck claiming gifts require operator review. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/notification-milestones": {
      "get": {
        "operationId": "getCronNotificationMilestones",
        "summary": "GET /api/internal/cron/notification-milestones",
        "description": "Sweep memo traffic milestones and emit deduplicated notifications; returns scanned and emitted. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/reconcile-sitemap-shards": {
      "get": {
        "operationId": "getCronReconcileSitemapShards",
        "summary": "GET /api/internal/cron/reconcile-sitemap-shards",
        "description": "Reconcile the sitemap shard registry under its lease lock, outside crawler requests. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/recover-stuck-imports": {
      "get": {
        "operationId": "getCronRecoverStuckImports",
        "summary": "GET /api/internal/cron/recover-stuck-imports",
        "description": "Recover stale subscriber-import jobs under a claim, resuming the runner or marking unrecoverable jobs failed. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/run-loops": {
      "get": {
        "operationId": "getCronRunLoops",
        "summary": "GET /api/internal/cron/run-loops",
        "description": "Run the loop scheduler, recover stuck runs, resume approvals, poll sources and sweep external leases. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/steward-queue-alerts": {
      "get": {
        "operationId": "getCronStewardQueueAlerts",
        "summary": "GET /api/internal/cron/steward-queue-alerts",
        "description": "Sweep waiting moderation queues and emit bounded steward notifications. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/cron/verify-promoted-sitemaps": {
      "get": {
        "operationId": "getCronVerifyPromotedSitemaps",
        "summary": "GET /api/internal/cron/verify-promoted-sitemaps",
        "description": "Verify promoted sitemap URLs and report per-URL checks. Failures are recorded in Sentry; HTTP 200 contains the actual verdict. Service-only GET with side effects; requires x-internal-key. No account API key or request body.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/drip/enroll": {
      "post": {
        "operationId": "postDripEnroll",
        "summary": "POST /api/internal/drip/enroll",
        "description": "Enroll a user in the onboarding drip. Service-only: requires x-drip-secret, not an account API key. JSON uid and email are required; returns success.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "uid": {},
                  "email": {}
                }
              },
              "description": "Enroll a user in the onboarding drip. Service-only: requires x-drip-secret, not an account API key. JSON uid and email are required; returns success."
            }
          }
        },
        "security": [
          {
            "DripWorkerKey": []
          }
        ]
      }
    },
    "/api/internal/drip/process": {
      "post": {
        "operationId": "postDripProcess",
        "summary": "POST /api/internal/drip/process",
        "description": "Process queued onboarding drip work. Service-only: requires x-drip-secret, not an account API key. No request body required; returns success and worker data.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "DripWorkerKey": []
          }
        ]
      }
    },
    "/api/internal/events/{eventId}/cancel": {
      "post": {
        "operationId": "postEventsEventidCancel",
        "summary": "POST /api/internal/events/{eventId}/cancel",
        "description": "Cancel an event as its authorized creator. JSON expectedUpdatedAt is required; optional cancellation fields follow the creator-event contract. Returns the cancellation result or stale-token conflict.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/events/{eventId}/guest-link": {
      "get": {
        "operationId": "getEventsEventidGuestLink",
        "summary": "GET /api/internal/events/{eventId}/guest-link",
        "description": "Read, create/regenerate or revoke a creator event guest link. POST accepts regenerate. Uses creator-event authorization; returns the guest-link operation result. This existing route does not require expectedUpdatedAt.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postEventsEventidGuestLink",
        "summary": "POST /api/internal/events/{eventId}/guest-link",
        "description": "Read, create/regenerate or revoke a creator event guest link. POST accepts regenerate. Uses creator-event authorization; returns the guest-link operation result. This existing route does not require expectedUpdatedAt.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "delete": {
        "operationId": "deleteEventsEventidGuestLink",
        "summary": "DELETE /api/internal/events/{eventId}/guest-link",
        "description": "Read, create/regenerate or revoke a creator event guest link. POST accepts regenerate. Uses creator-event authorization; returns the guest-link operation result. This existing route does not require expectedUpdatedAt.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/events/{eventId}/guests": {
      "patch": {
        "operationId": "patchEventsEventidGuests",
        "summary": "PATCH /api/internal/events/{eventId}/guests",
        "description": "Decide an event guest request using the creator-event guest decision JSON contract. Creator authorization applies; returns the guest operation result. See docs/api/internal.md Decide a Guest.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/events/{eventId}": {
      "get": {
        "operationId": "getEventsEventid",
        "summary": "GET /api/internal/events/{eventId}",
        "description": "Read an event (optional occurrenceStart query), or edit it. PATCH requires expectedUpdatedAt from a fresh event read and permitted event fields. Creator-event permissions and CAS apply; returns the event operation result.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "occurrenceStart",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "patch": {
        "operationId": "patchEventsEventid",
        "summary": "PATCH /api/internal/events/{eventId}",
        "description": "Read an event (optional occurrenceStart query), or edit it. PATCH requires expectedUpdatedAt from a fresh event read and permitted event fields. Creator-event permissions and CAS apply; returns the event operation result.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/events/{eventId}/rsvp": {
      "post": {
        "operationId": "postEventsEventidRsvp",
        "summary": "POST /api/internal/events/{eventId}/rsvp",
        "description": "RSVP as the authenticated caller using the event RSVP JSON contract. Creator-event access applies; returns the RSVP operation result. See docs/api/internal.md RSVP to a Creator Event.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "eventId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/events": {
      "get": {
        "operationId": "getEvents",
        "summary": "GET /api/internal/events",
        "description": "List creator events with required creator and optional year/month query parameters, or create an event from its JSON definition. Uses the creator-event agent guard and the existing creation entitlement. See docs/api/internal.md Creator Events for the complete event fields.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "creator",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "year",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "month",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postEvents",
        "summary": "POST /api/internal/events",
        "description": "List creator events with required creator and optional year/month query parameters, or create an event from its JSON definition. Uses the creator-event agent guard and the existing creation entitlement. See docs/api/internal.md Creator Events for the complete event fields.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/journal/line": {
      "patch": {
        "operationId": "patchJournalLine",
        "summary": "PATCH /api/internal/journal/line",
        "description": "Edit a single daily memo line. Query date is required; username is optional. JSON anchor, operation, content and expectedUpdatedAt are required; tags optional. Grant capability and dailyMemoAccess apply. Ambiguous or missing anchors are refused; stale tokens return 409.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "anchor": {},
                  "operation": {},
                  "content": {},
                  "tags": {},
                  "expectedUpdatedAt": {}
                }
              },
              "description": "Edit a single daily memo line. Query date is required; username is optional. JSON anchor, operation, content and expectedUpdatedAt are required; tags optional. Grant capability and dailyMemoAccess apply. Ambiguous or missing anchors are refused; stale tokens return 409."
            }
          }
        }
      }
    },
    "/api/internal/journal/section": {
      "patch": {
        "operationId": "patchJournalSection",
        "summary": "PATCH /api/internal/journal/section",
        "description": "Edit a daily memo section. Query date is required; username is optional. JSON heading, operation, content and expectedUpdatedAt are required; tags optional. Grant capability and dailyMemoAccess apply. Missing or ambiguous headings return 422; stale tokens return 409.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "heading": {},
                  "operation": {},
                  "content": {},
                  "tags": {},
                  "expectedUpdatedAt": {}
                }
              },
              "description": "Edit a daily memo section. Query date is required; username is optional. JSON heading, operation, content and expectedUpdatedAt are required; tags optional. Grant capability and dailyMemoAccess apply. Missing or ambiguous headings return 422; stale tokens return 409."
            }
          }
        }
      }
    },
    "/api/internal/memos/{id}/related": {
      "get": {
        "operationId": "getMemosIdRelated",
        "summary": "GET /api/internal/memos/{id}/related",
        "description": "Traverse related memos for id through accessible graph edges. Optional depth, direction, includeContext, limit, linkType, tags and tagMode narrow the traversal. Grants and content scope apply; returns related memo context.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "depth",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeContext",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "linkType",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagMode",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/{id}/section": {
      "get": {
        "operationId": "getMemosIdSection",
        "summary": "GET /api/internal/memos/{id}/section",
        "description": "Read a memo section by heading or index query parameter. Access is checked against the source memo and library scope. Returns section content and metadata; this is a read-only endpoint.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "heading",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "index",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/{id}/suggestions/{suggestionId}": {
      "patch": {
        "operationId": "patchMemosIdSuggestionsSuggestionid",
        "summary": "PATCH /api/internal/memos/{id}/suggestions/{suggestionId}",
        "description": "Review a suggestion with JSON status. Source memo ownership or edit-capable library access is required; returns success and status.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "suggestionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "status": {}
                }
              },
              "description": "Review a suggestion with JSON status. Source memo ownership or edit-capable library access is required; returns success and status."
            }
          }
        }
      }
    },
    "/api/internal/memos/{id}/suggestions": {
      "post": {
        "operationId": "postMemosIdSuggestions",
        "summary": "POST /api/internal/memos/{id}/suggestions",
        "description": "Create an agent suggestion with JSON type, payload and agentId, or list suggestions with optional status. Source memo access and the relevant read/edit grant capability are enforced.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {},
                  "payload": {},
                  "agentId": {}
                }
              },
              "description": "Create an agent suggestion with JSON type, payload and agentId, or list suggestions with optional status. Source memo access and the relevant read/edit grant capability are enforced."
            }
          }
        }
      },
      "get": {
        "operationId": "getMemosIdSuggestions",
        "summary": "GET /api/internal/memos/{id}/suggestions",
        "description": "Create an agent suggestion with JSON type, payload and agentId, or list suggestions with optional status. Source memo access and the relevant read/edit grant capability are enforced.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/backlinks": {
      "get": {
        "operationId": "getMemosBacklinks",
        "summary": "GET /api/internal/memos/backlinks",
        "description": "List memos and daily memos mentioning the supplied id query parameter; limit is optional. Source memo library authorization and content scope apply. Returns lean backlink records.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/discover": {
      "get": {
        "operationId": "getMemosDiscover",
        "summary": "GET /api/internal/memos/discover",
        "description": "Search platform-public memos by title or tag with q and optional limit. Empty query returns no rows. Applies the public substance threshold; does not grant access to private libraries.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/history/restore": {
      "post": {
        "operationId": "restoreMemoVersion",
        "summary": "POST /api/internal/memos/history/restore",
        "description": "Restore a memo to one of its version snapshots with JSON memoId, versionId and expectedUpdatedAt; username optional. Only the fields that differ are written, as one CAS write through the shared content core, and a fresh version row keeps the replaced state. Link lint only suggests. Owner or a grant that can edit, inside its content scope; community-owned and daily memos are refused. Returns restored:false when the memo already matches the version.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN: the memo changed since it was read; the response carries currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memoId",
                  "versionId",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "memoId": {
                    "type": "string"
                  },
                  "versionId": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "username": {
                    "type": "string"
                  }
                }
              },
              "description": "Restore a memo to one of its version snapshots with JSON memoId, versionId and expectedUpdatedAt; username optional. Only the fields that differ are written, as one CAS write through the shared content core, and a fresh version row keeps the replaced state. Link lint only suggests. Owner or a grant that can edit, inside its content scope; community-owned and daily memos are refused. Returns restored:false when the memo already matches the version."
            }
          }
        }
      }
    },
    "/api/internal/memos/trash": {
      "get": {
        "operationId": "listTrashedMemos",
        "summary": "GET /api/internal/memos/trash",
        "description": "List the library's trash newest-first with optional username and limit (default 50, max 100). Each row carries id, path, title, trashedAt, purgeAfter (30 days after trashing) and updatedAt, the token a restore needs. Owner, or a grant that can read, within its content scope.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          }
        ]
      },
      "post": {
        "operationId": "restoreTrashedMemo",
        "summary": "POST /api/internal/memos/trash",
        "description": "Restore one trashed memo with JSON memoId and expectedUpdatedAt from the trash listing; username optional. The compare-and-swap rides the write filter. Owner, or a grant that can delete, within its content scope; a memo outside the caller's reach answers 404.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "409": {
            "description": "STALE_LOCK_TOKEN: the memo changed since it was read; the response carries currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "memoId",
                  "expectedUpdatedAt"
                ],
                "properties": {
                  "memoId": {
                    "type": "string"
                  },
                  "expectedUpdatedAt": {
                    "type": "number"
                  },
                  "username": {
                    "type": "string"
                  }
                }
              },
              "description": "Restore one trashed memo with JSON memoId and expectedUpdatedAt from the trash listing; username optional. The compare-and-swap rides the write filter. Owner, or a grant that can delete, within its content scope; a memo outside the caller's reach answers 404."
            }
          }
        }
      }
    },
    "/api/internal/memos/history": {
      "get": {
        "operationId": "getMemosHistory",
        "summary": "GET /api/internal/memos/history",
        "description": "List memo version history with required memoId, optional username, limit and before, or read a single version with versionId. Source memo or daily-memo permissions apply; returns version snapshots or paginated history.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "memoId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "versionId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/line": {
      "patch": {
        "operationId": "patchMemosLine",
        "summary": "PATCH /api/internal/memos/line",
        "description": "Edit one memo line using query id and JSON zone, anchor, operation, content, expectedUpdatedAt; tags and acknowledgeUnlinked are optional. Applies grant capability, atomic CAS and link lint. Body writes also honor the live editing room gate. A body write that deletes most of the memo returns 422 LARGE_DELETION unless confirmLargeDeletion is true.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "zone": {},
                  "anchor": {},
                  "operation": {},
                  "content": {},
                  "tags": {},
                  "expectedUpdatedAt": {},
                  "acknowledgeUnlinked": {},
                  "confirmLargeDeletion": {}
                }
              },
              "description": "Edit one memo line using query id and JSON zone, anchor, operation, content, expectedUpdatedAt; tags and acknowledgeUnlinked are optional. Applies grant capability, atomic CAS and link lint. Body writes also honor the live editing room gate. A body write that deletes most of the memo returns 422 LARGE_DELETION unless confirmLargeDeletion is true."
            }
          }
        }
      }
    },
    "/api/internal/memos/move": {
      "post": {
        "operationId": "postMemosMove",
        "summary": "POST /api/internal/memos/move",
        "description": "Relocate a memo between owned or authorized libraries. JSON memoId and destinationLibrary are required; addAsCollaborator, createRedirect and dryRun are optional. Real moves require expectedUpdatedAt; dryRun does not write. Returns relocation details including embedding outcome.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "memoId": {},
                  "destinationLibrary": {},
                  "addAsCollaborator": {},
                  "createRedirect": {},
                  "dryRun": {},
                  "expectedUpdatedAt": {}
                }
              },
              "description": "Relocate a memo between owned or authorized libraries. JSON memoId and destinationLibrary are required; addAsCollaborator, createRedirect and dryRun are optional. Real moves require expectedUpdatedAt; dryRun does not write. Returns relocation details including embedding outcome."
            }
          }
        }
      }
    },
    "/api/internal/memos/rebuild-links": {
      "post": {
        "operationId": "postMemosRebuildLinks",
        "summary": "POST /api/internal/memos/rebuild-links",
        "description": "Rebuild the graph link index for username (or the configured default library). Cross-library use requires a grant. Returns rebuild counts; this is a maintenance write, not a content edit.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/memos/reindex": {
      "post": {
        "operationId": "postMemosReindex",
        "summary": "POST /api/internal/memos/reindex",
        "description": "Scan or rebuild missing embeddings in the caller library. JSON dryRun, limit and includeDailies are optional. Uses the owner embedding provider; dryRun reports cost estimates without embedding. Returns batch progress or NO_EMBEDDING_PROVIDER.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dryRun": {},
                  "limit": {},
                  "includeDailies": {}
                }
              },
              "description": "Scan or rebuild missing embeddings in the caller library. JSON dryRun, limit and includeDailies are optional. Uses the owner embedding provider; dryRun reports cost estimates without embedding. Returns batch progress or NO_EMBEDDING_PROVIDER."
            }
          }
        }
      }
    },
    "/api/internal/memos/section": {
      "patch": {
        "operationId": "patchMemosSection",
        "summary": "PATCH /api/internal/memos/section",
        "description": "Edit one memo section using query id and JSON zone, heading, operation, content and expectedUpdatedAt; tags and acknowledgeUnlinked optional. Applies grant capability, CAS and link lint. Body writes honor live editing rooms. Missing or ambiguous headings return 422. A body write that deletes most of the memo returns 422 LARGE_DELETION unless confirmLargeDeletion is true.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "zone": {},
                  "heading": {},
                  "operation": {},
                  "content": {},
                  "tags": {},
                  "expectedUpdatedAt": {},
                  "acknowledgeUnlinked": {},
                  "confirmLargeDeletion": {}
                }
              },
              "description": "Edit one memo section using query id and JSON zone, heading, operation, content and expectedUpdatedAt; tags and acknowledgeUnlinked optional. Applies grant capability, CAS and link lint. Body writes honor live editing rooms. Missing or ambiguous headings return 422. A body write that deletes most of the memo returns 422 LARGE_DELETION unless confirmLargeDeletion is true."
            }
          }
        }
      }
    },
    "/api/internal/memos/semantic-search": {
      "get": {
        "operationId": "getMemosSemanticSearch",
        "summary": "GET /api/internal/memos/semantic-search",
        "description": "Semantic search within the caller or username library. Requires q; topK, tags, tagMode and includeDailies optional. Two or more tags require explicit tagMode. Grants are resolved before search and content scope is intersected after retrieval. Daily memo search is owner-only. Returns scored memos with chunks.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "topK",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tags",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tagMode",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeDailies",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/newsletter/broadcast/send": {
      "post": {
        "operationId": "postNewsletterBroadcastSend",
        "summary": "POST /api/internal/newsletter/broadcast/send",
        "description": "Service-only broadcast delivery, not an owner API-key endpoint. JSON broadcastId is required. Refuses an already-sent broadcast; enforces the creator monthly limit and sends to its stored segment. Returns recipientCount. The service secret may be carried in x-internal-key, x-mythos-key or Bearer.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "broadcastId": {}
                }
              },
              "description": "Service-only broadcast delivery, not an owner API-key endpoint. JSON broadcastId is required. Refuses an already-sent broadcast; enforces the creator monthly limit and sends to its stored segment. Returns recipientCount. The service secret may be carried in x-internal-key, x-mythos-key or Bearer."
            }
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/newsletter/digest": {
      "post": {
        "operationId": "postNewsletterDigest",
        "summary": "POST /api/internal/newsletter/digest",
        "description": "Service-only digest worker. Optional JSON type is daily, weekly or monthly (default daily). Queues eligible subscriber digests and returns counts. The service secret may be carried in x-internal-key, x-mythos-key or Bearer; account API keys are not accepted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {}
                }
              },
              "description": "Service-only digest worker. Optional JSON type is daily, weekly or monthly (default daily). Queues eligible subscriber digests and returns counts. The service secret may be carried in x-internal-key, x-mythos-key or Bearer; account API keys are not accepted."
            }
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/newsletter/send": {
      "post": {
        "operationId": "postNewsletterSend",
        "summary": "POST /api/internal/newsletter/send",
        "description": "Service-only memo update delivery. JSON memoId and creatorUid are required. Non-public memos, unmatched topics and ineligible recipients are skipped. Returns delivery counts. The service secret may be carried in x-internal-key, x-mythos-key or Bearer; account API keys are not accepted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "memoId": {},
                  "creatorUid": {}
                }
              },
              "description": "Service-only memo update delivery. JSON memoId and creatorUid are required. Non-public memos, unmatched topics and ineligible recipients are skipped. Returns delivery counts. The service secret may be carried in x-internal-key, x-mythos-key or Bearer; account API keys are not accepted."
            }
          }
        },
        "security": [
          {
            "InternalServiceKey": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/error-csv": {
      "get": {
        "operationId": "getNewsletterSubscribersBulkJobidErrorCsv",
        "summary": "GET /api/internal/newsletter/subscribers/bulk/{jobId}/error-csv",
        "description": "Download invalid, duplicate and failed rows of an owned validated import as text/csv using a session JWT. Missing or unvalidated jobs return 404.",
        "responses": {
          "200": {
            "description": "Error rows CSV.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/import": {
      "post": {
        "operationId": "postNewsletterSubscribersBulkJobidImport",
        "summary": "POST /api/internal/newsletter/subscribers/bulk/{jobId}/import",
        "description": "Import an owned validated CSV job using a session JWT. Requires creator newsletter access; atomically claims the validated job, then runs the import. Returns completed counts or 503 when paused for watchdog recovery. No request body required.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/mapping": {
      "post": {
        "operationId": "postNewsletterSubscribersBulkJobidMapping",
        "summary": "POST /api/internal/newsletter/subscribers/bulk/{jobId}/mapping",
        "description": "Set column mapping for an owned CSV import job using a session JWT. JSON email mapping is required; firstName, lastName and topics are optional. Accepts uploaded, mapped, configured or validated jobs; returns discovered topic values.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {},
                  "firstName": {},
                  "lastName": {},
                  "topics": {}
                }
              },
              "description": "Set column mapping for an owned CSV import job using a session JWT. JSON email mapping is required; firstName, lastName and topics are optional. Accepts uploaded, mapped, configured or validated jobs; returns discovered topic values."
            }
          }
        },
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/settings": {
      "post": {
        "operationId": "postNewsletterSubscribersBulkJobidSettings",
        "summary": "POST /api/internal/newsletter/subscribers/bulk/{jobId}/settings",
        "description": "Configure an owned mapped CSV job using a session JWT. Requires defaultScope, frequency and digestMode; accepts defaultTopics, bypassDoubleOptIn, skipWelcomeEmail, topicResolutions and newTopics. Returns topic resolutions and advances the job to configured.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "defaultScope": {},
                  "defaultTopics": {},
                  "frequency": {},
                  "digestMode": {},
                  "bypassDoubleOptIn": {},
                  "skipWelcomeEmail": {},
                  "topicResolutions": {},
                  "newTopics": {}
                }
              },
              "description": "Configure an owned mapped CSV job using a session JWT. Requires defaultScope, frequency and digestMode; accepts defaultTopics, bypassDoubleOptIn, skipWelcomeEmail, topicResolutions and newTopics. Returns topic resolutions and advances the job to configured."
            }
          }
        },
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/status": {
      "get": {
        "operationId": "getNewsletterSubscribersBulkJobidStatus",
        "summary": "GET /api/internal/newsletter/subscribers/bulk/{jobId}/status",
        "description": "Read an owned CSV import job using a session JWT. Returns status, validation, result, cursor and configuration/preview metadata. Missing or unowned jobs return 404.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/{jobId}/validate": {
      "post": {
        "operationId": "postNewsletterSubscribersBulkJobidValidate",
        "summary": "POST /api/internal/newsletter/subscribers/bulk/{jobId}/validate",
        "description": "Validate an owned configured CSV job using a session JWT. Requires stored mapping/settings and creator newsletter access. Performs address and duplicate checks, stores validated rows and returns validation counts. No request body required.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/newsletter/subscribers/bulk/upload": {
      "post": {
        "operationId": "postNewsletterSubscribersBulkUpload",
        "summary": "POST /api/internal/newsletter/subscribers/bulk/upload",
        "description": "Upload a CSV as multipart file using the creator session JWT. Maximum 5000 rows and a daily upload quota for non-admins. Stores a bulk import job and returns jobId, headers and preview information. Account API keys do not authenticate this browser route.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "SessionBearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": [
                  "file"
                ],
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/internal/sessions": {
      "post": {
        "operationId": "postSessions",
        "summary": "POST /api/internal/sessions",
        "description": "POST starts an agent session with required JSON agentId and purpose; returns sessionId with 201. GET lists the latest 50 sessions and requires a real admin via API key or session JWT.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agentId": {},
                  "purpose": {}
                }
              },
              "description": "POST starts an agent session with required JSON agentId and purpose; returns sessionId with 201. GET lists the latest 50 sessions and requires a real admin via API key or session JWT."
            }
          }
        }
      },
      "get": {
        "operationId": "getSessions",
        "summary": "GET /api/internal/sessions",
        "description": "POST starts an agent session with required JSON agentId and purpose; returns sessionId with 201. GET lists the latest 50 sessions and requires a real admin via API key or session JWT.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "SessionBearer": []
          }
        ]
      }
    },
    "/api/internal/account/settings": {
      "get": {
        "operationId": "getAccountSettings",
        "summary": "GET /api/internal/account/settings",
        "description": "Read the caller's own account defaults — today `timeZone`, the IANA zone new loop schedules start in — with `updatedAt`, the lock token (0 before the first write).",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "updateAccountSettings",
        "summary": "PATCH /api/internal/account/settings",
        "description": "Update the caller's own account defaults under compare-and-swap. Body: `expectedUpdatedAt` (required; 0 before the first write) and `timeZone` (IANA zone, or null to clear). Unknown fields are refused. Stale token returns 409 STALE_LOCK_TOKEN with currentUpdatedAt. A loop run's key is refused.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "409": {
            "description": "Stale lock token; the response carries currentUpdatedAt."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/tag-metadata": {
      "get": {
        "operationId": "getTagMetadata",
        "summary": "GET /api/internal/tag-metadata",
        "description": "Read all tag metadata for the authenticated account. Returns metadata; does not accept a target username.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      }
    },
    "/api/internal/tags/{tag}": {
      "put": {
        "operationId": "putTagsTag",
        "summary": "PUT /api/internal/tags/{tag}",
        "description": "GET returns public memos for the tag plus authenticated-account metadata. PUT and its PATCH alias update description (max 200 chars), displayName, explainerMemoId or generated. Optional username checks grant tags capability and content scope; metadata is stored under the caller uid. Explainer must be owned by the caller.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {},
                  "displayName": {},
                  "explainerMemoId": {},
                  "generated": {}
                }
              },
              "description": "GET returns public memos for the tag plus authenticated-account metadata. PUT and its PATCH alias update description (max 200 chars), displayName, explainerMemoId or generated. Optional username checks grant tags capability and content scope; metadata is stored under the caller uid. Explainer must be owned by the caller."
            }
          }
        }
      },
      "patch": {
        "operationId": "patchTagsTag",
        "summary": "PATCH /api/internal/tags/{tag}",
        "description": "GET returns public memos for the tag plus authenticated-account metadata. PUT and its PATCH alias update description (max 200 chars), displayName, explainerMemoId or generated. Optional username checks grant tags capability and content scope; metadata is stored under the caller uid. Explainer must be owned by the caller.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {},
                  "displayName": {},
                  "explainerMemoId": {},
                  "generated": {}
                }
              },
              "description": "GET returns public memos for the tag plus authenticated-account metadata. PUT and its PATCH alias update description (max 200 chars), displayName, explainerMemoId or generated. Optional username checks grant tags capability and content scope; metadata is stored under the caller uid. Explainer must be owned by the caller."
            }
          }
        }
      },
      "get": {
        "operationId": "getTagsTag",
        "summary": "GET /api/internal/tags/{tag}",
        "description": "GET returns public memos for the tag plus authenticated-account metadata. PUT and its PATCH alias update description (max 200 chars), displayName, explainerMemoId or generated. Optional username checks grant tags capability and content scope; metadata is stored under the caller uid. Explainer must be owned by the caller.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "tag",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/tags": {
      "get": {
        "operationId": "getTags",
        "summary": "GET /api/internal/tags",
        "description": "GET enumerates target library tags. POST renames (action, oldName, newName) or merges (action, sourceTag, targetTag) with optional username and dryRun. Real rename/merge requires expectedUpdatedAt; dryRun is read-only. Applies library authorization and tags capability.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postTags",
        "summary": "POST /api/internal/tags",
        "description": "GET enumerates target library tags. POST renames (action, oldName, newName) or merges (action, sourceTag, targetTag) with optional username and dryRun. Real rename/merge requires expectedUpdatedAt; dryRun is read-only. Applies library authorization and tags capability.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/tasks": {
      "get": {
        "operationId": "getTasks",
        "summary": "GET /api/internal/tasks",
        "description": "List tasks with optional username, completed, limit and sort; POST creates a task with required text and optional notes, dueDate, dueTime, repeat and username. PATCH requires query id and JSON expectedUpdatedAt, and accepts completed or editable task fields. Standalone tasks support CAS; embedded updates return EMBEDDED_TASK_LOCK_DEFERRED.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "completed",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postTasks",
        "summary": "POST /api/internal/tasks",
        "description": "List tasks with optional username, completed, limit and sort; POST creates a task with required text and optional notes, dueDate, dueTime, repeat and username. PATCH requires query id and JSON expectedUpdatedAt, and accepts completed or editable task fields. Standalone tasks support CAS; embedded updates return EMBEDDED_TASK_LOCK_DEFERRED.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "patchTasks",
        "summary": "PATCH /api/internal/tasks",
        "description": "List tasks with optional username, completed, limit and sort; POST creates a task with required text and optional notes, dueDate, dueTime, repeat and username. PATCH requires query id and JSON expectedUpdatedAt, and accepts completed or editable task fields. Standalone tasks support CAS; embedded updates return EMBEDDED_TASK_LOCK_DEFERRED.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/templates": {
      "get": {
        "operationId": "getTemplates",
        "summary": "GET /api/internal/templates",
        "description": "Read templates using optional username, id or name, type, kind and default filters. POST requires name and content for template kind. PATCH uses query id and JSON expectedUpdatedAt plus changed fields; DELETE uses query id and expectedUpdatedAt. Grants and templates capability apply; system templates cannot be deleted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "default",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "post": {
        "operationId": "postTemplates",
        "summary": "POST /api/internal/templates",
        "description": "Read templates using optional username, id or name, type, kind and default filters. POST requires name and content for template kind. PATCH uses query id and JSON expectedUpdatedAt plus changed fields; DELETE uses query id and expectedUpdatedAt. Grants and templates capability apply; system templates cannot be deleted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "patch": {
        "operationId": "patchTemplates",
        "summary": "PATCH /api/internal/templates",
        "description": "Read templates using optional username, id or name, type, kind and default filters. POST requires name and content for template kind. PATCH uses query id and JSON expectedUpdatedAt plus changed fields; DELETE uses query id and expectedUpdatedAt. Grants and templates capability apply; system templates cannot be deleted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      },
      "delete": {
        "operationId": "deleteTemplates",
        "summary": "DELETE /api/internal/templates",
        "description": "Read templates using optional username, id or name, type, kind and default filters. POST requires name and content for template kind. PATCH uses query id and JSON expectedUpdatedAt plus changed fields; DELETE uses query id and expectedUpdatedAt. Grants and templates capability apply; system templates cannot be deleted.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "expectedUpdatedAt",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ]
      }
    },
    "/api/internal/username-redirect": {
      "get": {
        "operationId": "getUsernameRedirect",
        "summary": "GET /api/internal/username-redirect",
        "description": "Public lookup of a previous username. Optional username query; returns redirect false, or redirect true with newUsername. Does not require an API key.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": []
      }
    },
    "/api/internal/username-status": {
      "get": {
        "operationId": "getUsernameStatus",
        "summary": "GET /api/internal/username-status",
        "description": "Public, IP-rate-limited username state lookup. Optional username query; missing input returns absent. Returns the username state without requiring an API key.",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "username",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": []
      }
    },
    "/api/internal/webhooks": {
      "post": {
        "operationId": "postWebhooks",
        "summary": "POST /api/internal/webhooks",
        "description": "POST registers an owner webhook with JSON url (public HTTPS), events (non-empty supported event list) and secret; returns webhookId with 201. GET lists sanitized registrations and DELETE takes query id. Reads/deletes are owner-scoped for API keys, or platform-wide for real admins (API key or session JWT).",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "201": {
            "description": "Created."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        }
      },
      "get": {
        "operationId": "getWebhooks",
        "summary": "GET /api/internal/webhooks",
        "description": "POST registers an owner webhook with JSON url (public HTTPS), events (non-empty supported event list) and secret; returns webhookId with 201. GET lists sanitized registrations and DELETE takes query id. Reads/deletes are owner-scoped for API keys, or platform-wide for real admins (API key or session JWT).",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "SessionBearer": []
          }
        ]
      },
      "delete": {
        "operationId": "deleteWebhooks",
        "summary": "DELETE /api/internal/webhooks",
        "description": "POST registers an owner webhook with JSON url (public HTTPS), events (non-empty supported event list) and secret; returns webhookId with 201. GET lists sanitized registrations and DELETE takes query id. Reads/deletes are owner-scoped for API keys, or platform-wide for real admins (API key or session JWT).",
        "responses": {
          "200": {
            "description": "Successful operation; response fields are described above."
          },
          "400": {
            "description": "Invalid request."
          },
          "401": {
            "description": "Missing or invalid authentication."
          },
          "403": {
            "description": "Insufficient permission."
          },
          "404": {
            "description": "Resource not found or inaccessible."
          },
          "429": {
            "description": "Rate limited."
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "security": [
          {
            "ApiKeyAuth": []
          },
          {
            "SessionBearer": []
          }
        ]
      }
    }
  }
}