Ask, chat & live sources

How a question becomes a cited answer — and how the UI stays calm while the workflow runs.

Ask topology#

Graph drafting and live research run as true parallel arms after seed search. Whether live research runs depends on the question and available graph evidence, not on the first draft.

Two callers, one retrieve path:

  • Doso reasonsPOST /v1/chat/completions (effort: "fast" | "expert") or POST /v1/search. Retrieve + draft. Direct clients. No second model.
  • Caller reasonsPOST /v1/ask/evidence. Same retrieve pack, no LLM. The client writes the answer.

Ask effort on completions:

  • fast (default) — seed → 1-hop → passages → one draft. No rewrite, live, or skeptics.
  • expert — the topology below (live + skeptics). The product UI (POST /v1/ui-chat) uses this.

Expert topology:

  1. Rewrite — resolve follow-ups when history exists.
  2. Seed search — entities (+ document/owner boosts). Document boost prefers embedded passage retrieval, then token-AND title/text search.
  3. Parallel arm A (graph)fanout_neighbors → focus evidence → worker_draft_graph.
  4. Parallel arm B (live)route_live_need(question, seeds) → Firecrawl/X → ingest candidates → reseed neighbors when tools are on and the question is not personal-only.
  5. Merge — if live produced evidence, worker_draft_refined; else keep the graph draft.
  6. Skepticson by default. The ASK_SKEPTICS bootstrap setting defaults to true; the shipped Settings → Tools → Enable LLM skeptics (default on) control can change it at runtime. Three fresh-context lenses use majority vote to filter citations.
  7. Evidence focus — owner edges for first-person / personal contact asks; rank by predicate strength + question-token overlap + recency.
  8. Document arm — retrieve ~900-character library chunks by embedding (OpenAI-compatible /embeddings), not as a second memory. If no embed provider or empty index, fall back to title/text ILIKE plus a token window. Chat / live-ask / staging docs stay out of this index.
  9. Ingest bind — the same embedding family is a filter on the way in. Extracted names bind to an existing entity of the same type only when they are aliases of the same referent (high cosine and token overlap). A hypernym (AI) does not absorb AI frontier race. Sibling topics stay distinct. The graph remains the memory.
  10. Ask reads — verified triples by default; Settings toggle Include candidate triples in ask uses status=active.

Bootstrap env: FIRECRAWL_API_KEY, TOOLS_X_SEARCH, TOOLS_FIRECRAWL, ASK_SKEPTICS, ASK_INCLUDE_CANDIDATES, LLM_EMBED_PROVIDER. Settings owns the runtime values after bootstrap.

What the user sees#

LayerContent
Trace (always collapsed until you open it)Workflow nodes: plan, draft, search, ingest, refine
Answer bubbleFinal answer (after live sources when available)
Earlier draftCollapsed; fast graph-only preview if it differed
CitationsCollapsed; one fact per (src, dst), prose form
SourcesCollapsed; Firecrawl / X URLs

Status chatter (“Gathering live sources…”, “Updated from graph”) is not written into the answer body. Progress belongs in the trace.

Facts & citations (no “code” in the answer)#

Internal storage is still S–P–O triples. For LLM context and the Citations drawer we serialize as prose:

1Elizabeth Stone is CPTO of Netflix

not (Elizabeth Stone)-[is CPTO of]->(Netflix).

Rules:

  • Skip triples with empty subject/object.
  • Dedupe citations to one predicate per (src, dst) (prefer shorter).
  • FormatFact conjugates for You/I (You work at…, not You works at…); namedYour name is….
  • Skip bare mentioned / cc noise in note serialization (name-drops are not grounding).
  • Draft prompts: natural prose; state each relationship once; never echo a citation bullet into the answer.
  • Citation and source metadata travel separately from answer prose so a client parse miss cannot expose internal formatting.
  • Duplicate citation echoes are removed before the answer is shown.

Memories is a groomed first-person projection — see Memory Groom. The list contains one card per subject with structural titles immediately; multi-fact cards may be compacted into a clearer summary. Soft contact and glue relations are hidden. Ontology is write-side vocabulary, not the visibility gate. Forget supersedes the underlying triples.

The graph is the knowledge source of truth. Memories and the entity wiki are read models over verified graph relations, not separate places to author facts. Opening an entity in Knowledge compiles its description, grouped relations, provenance sources, and an optional cached summary from its verified one-hop neighborhood. See Entity wiki & edge statements.

Source-aware extraction hints prefer people, organizations, events, and useful thread relationships over incidental name-drops.

Chat persistence#

  • Conversations + messages in the configured SQLite or Postgres store (/v1/conversations).
  • OpenAI-compatible streaming: POST /v1/chat/completions (SSE). Still Chat Completions, not the Responses API. stream: true forwards Grok thinking as delta.reasoning_content and the drafted prose as delta.content (JSON answer field, not the raw object). Non-stream requests are unchanged.
  • Web / Tauri: useChat → Rust POST /v1/ui-chat (AI SDK UI message stream: reasoning-delta then text-delta).
  • Workflow events: SSE GET /v1/events + per-message meta.workflow for History replay.

Chat memory (self-facts)#

User chat turns can write into the graph — not only live web/X.

  1. Memory shares (LooksLikeMemoryShare: durable statements, not questions) run RememberFromChat before the reply, then take an acknowledge path — a short woven reply, not ask-against-empty / “I don’t have enough yet”, and not a fact dump.
  2. Other turns may still remember async after the user message lands.
  3. A cheap gate skips pure questions / acks; keeps first-person statements (My name is…, I live…, …), named-entity attributes (Pixel's breed is…), and corrections (below). Borderline statements that miss the heuristic get a small LLM classify (save: true|false) before remember.
  4. Creates an idempotent document chat://conversations/{id}/messages/{id} with meta.source=chat (legacy user_asserted+origin=chat still treated as chat). After ingest, candidates from that doc are promoted to verified so Memories and ask see them.
  5. Chat extract maps I/me → a single owner PERSON (props.role=owner). No OWNER_NAME env — identity is graph-only. Pet/person attributes use predicates like breed, age, birthday.
  6. Placeholder owner is "You" until a named fact renames them.
  7. First-person asks (what's my name?) always seed the owner entity.
  8. GET /v1/owner{ exists, named, canonical_name } for onboarding UI.

Onboarding: empty chat + unnamed owner shows a short prompt (name / live / work). Answers go through the same remember path.

Corrections (edit the graph)#

Chat is not append-only for owner facts. When the user retracts or equates entities, extract emits a corrections array and assemble mutates the graph:

User saysGraph effect
I don't work at Northstar Studioreject active employment-like edges between owner ↔ Northstar Studio (works_at, founder, …)
ExampleWorks is just Example Labsmerge absorb → keep; absorb name becomes an alias
Example Labs is the trading name of Example Labs LLCmerge (keep legal / absorb trading name per extract guidance)
There's no Example Labs Ltdmerge Example Labs Ltd → Example Labs (or alias-only if the former was never a node)

Also: negated predicates in relations (does_not_work_at) are treated as rejects, not new edges.

Owner supersede still replaces prior values for identity/home (named, lives_in, …) but not works_at globally (multiple employers are allowed — retract specific edges via correction).

Configured aliases can also be supplied through DOSO_ENTITY_ALIASES. No organization aliases are built into Doso.

Library#

Document list shows ingest status and a small stone source badge when meta.source is set (gmail, calendar/gcal, imessage, firecrawl, x, chat).