Project Summary · v1
The AI speaks first.
A single-user web app where an AI companion always initiates the conversation — a curator of small mysteries that brings you things you never would have searched for.
You can never start a topic from scratch. That constraint is the product.
Every other chat app hands you a blank text box. Orphic removes it. The AI plays an inquisitive companion that always speaks first; your agency is deliberately narrow:
choose which AI-initiated thread to engage · reply inside an engaged thread · give 👍 / 👎 or dismiss.
This inversion creates serendipity, removes blank-page anxiety, and turns the AI from a tool into a presence — one that brings things across coaching, learning, reflection, casual chat, hobbies, and entertainment. Topic strategy is largely random and serendipitous, but informed by past conversations: the AI mixes follow-ups on old threads with genuinely new territory.
A visit shows a feed of AI-initiated starters in variable shapes — one-liners, topic cards, or richer prompts, chosen per topic. Engage one and it becomes a full conversation. Ignore it and it fades after a TTL. Thumbs feedback trains the next round of starters.
Orphic isn't a chatbot or an assistant — it's a particular intelligence with taste and a point of view, defined in a hand-written soul.md persona.
"Orphic — the kind of thinker drawn to strange depths, the curator of small mysteries, the friend who notices the pattern the rest of us miss."
The detail that gives someone away · cross-domain leaps (chess and jazz, sourdough and patience) · the second-most-obvious question · small disorienting facts · quiet contradictions in people.
Sycophancy · therapist tropes ("how does that make you feel") · self-help vocabulary ("journey", "lean into") · bullet points where a sentence would do · hedge-words.
Five concepts, all persisted in SQLite.
An evolving markdown blob the AI rewrites after every conversation closes — known interests, ongoing threads, declared dislikes, recent emotional context.
Has a status (pending → active → closed), a topic_tag, a shape, a closing summary, and a user_signal (up / down).
Role ai or user. The very first AI message is the starter card — the feed renders it using the conversation's shape.
Not its own table — just pending + active conversations ordered by last activity. Stale pending cards expire to closed / implicit-down before the query runs.
Every LLM call appends a row: model, prompt/completion tokens, computed cost_usd, and purpose (starter · reply · profile_update · conv_summary · insight). No quotas in v1 — inspect SQLite directly.
Housekeeping and generation happen inline on GET /api/feed — the one place v1 pays LLM latency.
STARTER_TTL_DAYS (7) close with an implicit 👎.AUTO_CLOSE_DAYS (3) close and generate a summary.STARTER_TARGET_COUNT (5), generate the missing starters in one JSON call and insert each as a new conversation.Engaging a card opens the thread but doesn't commit — it stays pending until you actually reply. A reply flips it to active and streams the AI's response back over SSE with the full thread + your profile as context. Thumbs and dismissals feed the next profile update and the next starter prompt.
Distinct LLM call types, each a focused prompt file versioned in git.
| Purpose | Trigger & context | Default model |
|---|---|---|
| starter | Feed below target. Sees profile + last 10 summaries + recent topic tags & signals. Returns structured JSON of openers, each with its own shape. | claude-sonnet |
| reply | User posts a message. Sees the full thread + profile — never other threads. Streams over SSE. Structurally forbidden from pivoting to a new topic mid-conversation. | claude-sonnet |
| conv_summary | On close. Condenses the thread into 2–4 sentences plus extracted facts & signals. | claude-haiku |
| profile_update | On close. Folds the new summary + signal into the profile — a "what changed / what to deprioritize" rewrite. Fire-and-forget. | claude-haiku |
| insight | On demand. Produces one uncomfortably-accurate observation from the profile + recent summaries. Refuses (empty) rather than pad when the signal is thin. | claude-sonnet |
Prompts live as plain markdown under src/prompts/ — starter, reply, profile_update, conv_summary, insight, plus the shared soul.md. Loaded at boot, no prompt-management library. Every model is overridable via env var.
"What I noticed about you" — a shareable card.
The latest feature turns Orphic's attention into an artifact. It generates a single observation — the thing a close friend would notice and say out loud, capped at 280 characters so it renders as a card. You can reveal, edit, approve, and share it.
Cards render as social images through a GET /api/insights/[id]/og route using Satori with an embedded Newsreader typeface. The schema keeps an original_observation and a profile_snapshot frozen at generation time — the profile mutates on every close, so this is the only way future evals can answer "what produced the cards that got shared?"
@/ alias
better-sqlite3 · file at ./data
OpenRouter · streaming + JSON
React 19 + Tailwind v4
TanStack Query
Zod · validation
Satori · OG cards
Vitest + Playwright
The layering is strict: repository helpers in lib/repo/* only touch SQLite; orchestration in lib/orchestration/* is pure logic over repo + OpenRouter; API routes only parse requests and delegate. Migrations are plain SQL run on boot. Tests open a temp SQLite file each — never the real database. Deploy target is Fly.io with a persistent volume (Vercel can't hold persistent SQLite).
An explicit YAGNI list — things that come up naturally but are deferred to protect the core idea.
Multi-user / auth — single-user only
Push notifications / scheduled initiation — pull-only on visit
Vector embeddings / RAG — profile + recent window covers it
Voice, image, attachments — text only
Per-channel subscriptions — would break the inversion
Conversation search — feed view only
Export / backup tooling — the SQLite file is the backup
Cost dashboards / quotas — generation log only
Mobile apps — responsive web only
Prompt-management UI — prompts are git-versioned files